System.Diagnostics.Process 的假设性重新设计,以避免对仅在你调用 Start 时才有效的属性产生混淆

The Old New Thing (Raymond Chen) 新闻

摘要

一篇博客文章,提议重新设计 .NET 中的 System.Diagnostics.Process 类,将仅对已启动进程有效的属性分离到一个新类中,旨在减少 API 混淆。

<p>不久前,我指出<a title="进程如何读取自己的标准输出?" href="https://devblogs.microsoft.com/oldnewthing/20251205-00/?p=111843"> <code>Process.<wbr />Standard­Output</code> 属性是一个吸引人的麻烦</a>,因为它仅对调用过 <code>Start</code> 的 <code>Process</code> 对象有效。你不能随便抓取一个 <code>Process</code> 对象然后尝试访问其标准句柄。</p> <p>评论中的其他人提出了他们消除混淆的想法。下面是我的想法。原则是 <code>Process</code> 对象的属性和方法应对 <code>Process</code> 类的所有实例都有效。如果某个属性或方法仅在特定条件下有效,则应将其移至仅在条件满足时才能访问的地方,或者如果它没有价值则完全删除。</p> <p>标准句柄是三个仅对由静态 <code>Start</code> 方法创建的 <code>Process</code> 对象有意义的属性。还有四个与这些标准句柄相关的方法,以及两个事件。将它们全部移到一个新类中,命名为 <code>Process­<!-- -->Start­Result</code>:</p> <pre>class Process<!-- -->StartResult { public Process Process { get; } public System.IO.StreamWriter StandardInput { get; } public System.IO.StreamWriter StandardOutput { get; } public System.IO.StreamWriter StandardError { get; } public void BeginOutputReadLine(); public void CancelOutputReadLine(); public event DataReceivedEventHandler? OutputDataReceived; public void BeginErrorReadLine(); public void CancelErrorReadLine(); public event DataReceivedEventHandler? ErrorDataReceived; } </pre> <p>更改所有 <code>Start</code> 方法重载的签名,使其返回 <code>Process­<!-- -->Start­Result</code> 而不是 <code>Process</code>。这样,对于你未启动的进程,就不可能对标准句柄进行任何操作:如果你没有启动该进程,你就没有 <code>Process­<!-- -->Start­Result</code>。这消除了原始尝试中让进程读取自己标准输出时存在的混淆。</p> <p>这遵循了<a title="如何设计一个类,使得方法必须按特定顺序调用?" href="https://devblogs.microsoft.com/oldnewthing/20190318-00/?p=102324">我之前写过的一个原则</a>:为了强制开发者按特定顺序做事,让第二步依赖于第一步产生的某些东西。在这种情况下,我们想要强制开发者在使用标准句柄之前调用 <code>Start</code>,因此我们将与标准句柄相关的成员放在只有通过调用 <code>Start</code> 才能获得的对象上。</p> <p>接下来,完全删除 <code>Start­Info</code> 属性。它有两个作用:</p> <ul> <li>在调用 <code>Start</code> 方法之前,它提供了一个方便的预制 <code>Process­<!-- -->Start­Info</code>。</li> <li>在调用 <code>Start</code> 方法之后,它保存了传递给 <code>Start</code> 方法的参数副本。</li> </ul> <p>第一个作用只是为了照顾那些懒得写 <code>new</code> 关键字的人。所以不要偷懒。写 <code>new Process­<!-- -->Start­Info()</code>。</p> <p>第二个作用并不会告诉你任何你不知道的事情,因为你是最初将参数传递给 <code>Start</code> 方法的人。如果这些参数对你很重要,你可以自己保存它们。</p> <p>删除 <code>Start­Info</code> 可以避免混淆:其中的属性描述的是你想要启动的进程,还是已经启动的进程?(而且通常,它两者都不描述!)</p> <p>我认为这解决了 <code>Process</code> 类正确使用中最主要的混淆来源。</p> <p>这篇文章《<a href="https://devblogs.microsoft.com/oldnewthing/20260525-00/?p=112351">System.Diagnostics.Process 的假设性重新设计,以避免对仅在你调用 Start 时才有效的属性产生混淆</a>》最先出现在 <a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a> 上。</p>
查看原文
查看缓存全文

缓存时间: 2026/05/26 08:50

# System.Diagnostics.Process 的假设性重新设计:避免混淆那些只有在你调用 Start 后才有效的属性 - The Old New Thing 来源:https://devblogs.microsoft.com/oldnewthing/20260525-00?p=112351 不久前,我曾指出 `Process.StandardOutput` 属性是一个“诱人麻烦”(https://devblogs.microsoft.com/oldnewthing/20251205-00/?p=111843),因为它只对你自己调用 `Start` 创建的 `Process` 对象有效。你无法随意抓取一个旧的 `Process` 对象并尝试访问它的标准句柄。 评论中的其他人也提出了消除这种混淆的想法。以下是我的方案。核心原则是:`Process` 对象的属性和方法应对所有 `Process` 类的实例都有效。如果某个属性或方法仅在特定条件下有效,那么要么将其移到一个只有满足条件才能访问的地方,要么在它没有价值时完全移除。 标准句柄是三个仅对通过静态 `Start` 方法创建的 `Process` 对象有意义的属性。另外还有四个与这些标准句柄相关的方法,以及两个事件。将它们全部移到一个新类中,命名为 `ProcessStartResult`: ``` class ProcessStartResult { public Process Process { get; } public System.IO.StreamWriter StandardInput { get; } public System.IO.StreamWriter StandardOutput { get; } public System.IO.StreamWriter StandardError { get; } public void BeginOutputReadLine(); public void CancelOutputReadLine(); public event DataReceivedEventHandler? OutputDataReceived; public void BeginErrorReadLine(); public void CancelErrorReadLine(); public event DataReceivedEventHandler? ErrorDataReceived; } ``` 修改所有 `Start` 方法重载的签名,使它们返回 `ProcessStartResult` 而不是 `Process`。现在,对于你没有启动的进程,你无法对标准句柄做任何事情:如果你没有启动进程,你就没有 `ProcessStartResult`。这消除了原先试图让进程读取自己的标准输出时存在的混淆。 这遵循了我之前写过的一个原则(https://devblogs.microsoft.com/oldnewthing/20190318-00/?p=102324):为了迫使开发者按特定顺序执行操作,让第二步依赖于第一步产生的结果。在本例中,我们希望迫使开发者在使用标准句柄之前调用 `Start`,因此我们将与标准句柄相关的成员放在一个只能通过调用 `Start` 才能获得的对象上。 接下来,完全移除 `StartInfo` 属性。它有两个用途: - 在调用 `Start` 方法之前,它提供了一个预先配置好的 `ProcessStartInfo` 便利对象。 - 在调用 `Start` 方法之后,它保存了你传递给 `Start` 方法的参数副本。 第一个用途只是为懒得写 `new` 关键字的人提供便利。所以不要偷懒,直接写 `new ProcessStartInfo()`。 第二个用途并不会告诉你任何你早已知道的信息,因为参数本身就是你传递给 `Start` 方法的。如果这些参数对你很重要,你可以自己保存它们。 移除 `StartInfo` 可以避免混淆:它的属性描述的是你想要启动的进程,还是已经启动的进程?(而且很多时候,它两者都不是!) 我认为这解决了 `Process` 类使用中最大的混淆源。 ### 分类 ### 主题 ## 作者 Raymond Chen Raymond 参与 Windows 演变已超过 30 年。2003 年,他创办了名为“The Old New Thing”的网站,其受欢迎程度远超他最大胆的想象——这一发展至今仍让他感到毛骨悚然。该网站还催生了一本书,巧合的是书名也是《The Old New Thing》(Addison Wesley 2007)。他偶尔出现在 Windows Dev Docs 的 Twitter 账号上,讲述一些不传递任何有用信息的故事。

相似文章

理解在试图绕过规则时规则背后的原理

The Old New Thing (Raymond Chen)

本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。

为何不根据链接的SDK来改变API行为?

The Old New Thing (Raymond Chen)

本文以Windows的CoInitializeSecurity为例,探讨了根据链接的SDK版本改变API行为的陷阱。讨论了DLL版本不匹配和尾调用优化等问题使这种方法复杂化。

从零编写调试器

Hacker News Top

本文开启了一个使用Rust从零构建调试器的系列,首先介绍如何使用操作系统调试API附加到Windows进程。

改进 C# 内存安全

Hacker News Top

微软宣布对 C# 16 中的 unsafe 关键字进行重新设计,以强制执行内存安全契约,使 unsafe 操作变得可见并由编译器强制执行,预览版将在 .NET 11 中发布,正式版在 .NET 12 中发布。