System.Diagnostics.Process 的假设性重新设计,以避免对仅在你调用 Start 时才有效的属性产生混淆
摘要
一篇博客文章,提议重新设计 .NET 中的 System.Diagnostics.Process 类,将仅对已启动进程有效的属性分离到一个新类中,旨在减少 API 混淆。
<p>不久前,我指出<a title="进程如何读取自己的标准输出?" href="https://devblogs.microsoft.com/oldnewthing/20251205-00/?p=111843"> <code>Process.<wbr />StandardOutput</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<!-- -->StartResult</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<!-- -->StartResult</code> 而不是 <code>Process</code>。这样,对于你未启动的进程,就不可能对标准句柄进行任何操作:如果你没有启动该进程,你就没有 <code>Process<!-- -->StartResult</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>StartInfo</code> 属性。它有两个作用:</p>
<ul>
<li>在调用 <code>Start</code> 方法之前,它提供了一个方便的预制 <code>Process<!-- -->StartInfo</code>。</li>
<li>在调用 <code>Start</code> 方法之后,它保存了传递给 <code>Start</code> 方法的参数副本。</li>
</ul>
<p>第一个作用只是为了照顾那些懒得写 <code>new</code> 关键字的人。所以不要偷懒。写 <code>new Process<!-- -->StartInfo()</code>。</p>
<p>第二个作用并不会告诉你任何你不知道的事情,因为你是最初将参数传递给 <code>Start</code> 方法的人。如果这些参数对你很重要,你可以自己保存它们。</p>
<p>删除 <code>StartInfo</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 账号上,讲述一些不传递任何有用信息的故事。
相似文章
关于控制CreateProcess继承哪些句柄的补充说明
Raymond Chen介绍了一种使用辅助进程的技术,可以精确控制新进程继承哪些句柄,避免同一进程中其他组件意外继承句柄。
理解在试图绕过规则时规则背后的原理
本文来自微软的《老东西》博客,解释了Windows内核回调函数最佳实践背后的原理,特别是为什么阻塞或等待工作项会违背其目的,并通过一个关于驱动程序导致系统挂起的警示故事来说明。
为何不根据链接的SDK来改变API行为?
本文以Windows的CoInitializeSecurity为例,探讨了根据链接的SDK版本改变API行为的陷阱。讨论了DLL版本不匹配和尾调用优化等问题使这种方法复杂化。
从零编写调试器
本文开启了一个使用Rust从零构建调试器的系列,首先介绍如何使用操作系统调试API附加到Windows进程。
改进 C# 内存安全
微软宣布对 C# 16 中的 unsafe 关键字进行重新设计,以强制执行内存安全契约,使 unsafe 操作变得可见并由编译器强制执行,预览版将在 .NET 11 中发布,正式版在 .NET 12 中发布。