如果你只是坐在那里什么都不做,至少也要正确地什么都不做

Lobsters Hottest 新闻

摘要

这篇文章来自 The Old New Thing,解释了使API变得'惰性'的概念——以一种不破坏现有应用的方式'什么都不做'——并使用在Xbox上支持打印和淘汰widget API等例子。

<p><a href="https://lobste.rs/s/2xm9a2/if_you_re_just_going_sit_there_doing">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/05/20 06:24

# 如果你只是坐在那里什么都不做,至少要用正确的方式什么都不做 - 旧事新说 原文链接:https://devblogs.microsoft.com/oldnewthing/20240216-00?p=109409 有时候你可能需要让某个 API 什么都不做。关键是要用正确的方式实现“什么都不做”。 例如,Windows 拥有庞大的打印基础设施,但 Xbox 上没有这套设施。如果某个应用试图在 Xbox 上打印,会发生什么? 错误做法是让打印函数抛出 `NotSupportedException`。用户在 Xbox 上安装的应用很可能(即使不是完全)是在 PC 上测试的,而 PC 上始终支持打印。在 Xbox 上运行时,异常很可能未被处理,导致应用崩溃。即使应用尝试捕获异常,它可能也会弹出类似“糟糕,出错了。请联系支持并提供此事件代码”的消息。 在 Xbox 上“支持”打印的更好设计是:让打印函数成功返回,但报告没有安装任何打印机。这样一来,当应用尝试打印时,会要求用户选择打印机,并显示一个空列表。用户会意识到“哦,没有打印机”,然后取消打印请求。 为了应对那些“多管闲事”的应用——它们会说“您没有安装打印机,让我帮您安装一个”——安装打印机的函数可以立即返回一个表示“用户取消了操作”的结果代码。 这里的思路是:让所有打印函数的行为完全符合打印功能完全支持的情形,但神秘的是,永远没有可打印的打印机。 当然,你可能还想添加一个函数来检查打印功能是否可用。应用可以使用这个函数,在运行于根本不支持打印的系统上时,隐藏用户界面中的打印按钮。但那些天真地假设打印功能正常工作的应用,仍然会以合理的方式表现:你只是在一个没有任何打印机的系统中,而且所有安装打印机的尝试都无效。 我们用来描述这种“什么都不做”行为的术语是“惰性”(inert)。 API 表面仍然存在并按规范运作,但同样也什么都不做。关键是以与其文档一致且最不可能给现有代码带来问题的方式实现“什么都不做”。 另一个例子是某个 API 的退役。该 API 有多个用于创建构件句柄的函数、一些接受构件句柄的函数,以及一个用于关闭构件句柄的函数。负责退役的团队最初提议按如下方式让该 API 变得“惰性”: ``` HRESULT CreateWidget(_Out_ HWIDGET* widget) { *widget = nullptr; return S_OK; } // 每个构件在文档中至少有一个别名, // 所以我们必须生成一个虚设别名(空字符串)。 HRESULT GetWidgetAliases( _Out_writes_to_(capacity, *actual) PWSTR* aliases, UINT capacity, _Out_ UINT* actual) { *actual = 0; RETURN_HR_IF( HRESULT_FROM_WIN32(ERROR_MORE_DATA), capacity < 1); aliases[0] = make_cotaskmem_string_nothrow(L"").release(); RETURN_IF_NULL_ALLOC(aliases[0]); *actual = 1; return S_OK; } // 惰性构件不能启用或禁用。 HRESULT EnableWidget(HWIDGET widget, BOOL value) { return E_HANDLE; } HRESULT Close(HWIDGET widget) { RETURN_HR_IF(E_INVALIDARG, widget != nullptr); return S_OK; } ``` 我指出,让 `CreateWidget` 成功但返回空指针会迷惑应用。“调用成功了,但我没有拿到有效的句柄?”我甚至发现他们自己的测试代码是通过检查句柄是否为空来判断调用是否成功,而不是检查返回值。 我还指出,让 `EnableWidget` 返回“无效句柄”同样会造成混乱。应用调用 `CreateWidget` 并成功返回,然后拿着这个(假定有效的)句柄去启用构件,却被告知“该句柄无效”。这怎么解释?“我向你要了一个构件,你给了我一个,然后当我把它拿给你看时,你却说‘这不是构件’。这个 API 在对我进行煤气灯效应!” 我查阅了他们 API 的现有文档,发现有一个文档化的返回值 `ERROR_CANCELLED` 表示用户取消了构件的创建。因此,应用已经能够处理由于不可控原因未能创建构件的情况,所以我们可以利用这一点:每当应用尝试创建构件时,直接说“不,那个,用户取消了,没错,就是这样”。 ``` HRESULT CreateWidget(_Out_ HWIDGET* widget) { *widget = nullptr; return HRESULT_FROM_WIN32(ERROR_CANCELLED); } HRESULT GetWidgetAliases( _Out_writes_to_(capacity, *actual) PWSTR* aliases, UINT capacity, _Out_ UINT* actual) { *actual = 0; return E_HANDLE; } HRESULT EnableWidget(HWIDGET widget, BOOL value) { return E_HANDLE; } HRESULT Close(HWIDGET widget) { return E_HANDLE; } ``` 现在我们有了一个正确的惰性 API 表面。 如果你尝试创建一个构件,我们会告诉你无法创建,因为用户取消了。由于所有创建构件的尝试都失败,根本不存在有效的构件句柄,所以任何时候你试图使用一个句柄,我们都会告诉你该句柄无效。 这也避免了必须为构件生成虚设别名的问题。因为根本没有构件,应用在任何情况下都不可能向构件询问其别名。 **额外闲聊**:为了消除一些混淆:这里的思路是,打印 API 在桌面上一直存在,打印功能得到支持,而“获取打印机列表”函数在文档中明确表示不会抛出异常。如果你想将打印 API 移植到 Xbox,如何在允许现有桌面应用继续在 Xbox 上运行的同时实现这种移植?惰性行为完全符合事实:Xbox 上没有打印机。没有人期望“有多少台打印机?”这个问题的答案是“你竟敢问我这种问题!” 另一种需要创建惰性 API 表面的情况是,如果你想退役一个现有 API。如何让 API 的行为与其协定一致,同时又什么都不做? ### 分类 ### 主题 ## 作者 Raymond Chen Raymond 参与 Windows 的发展已有 30 多年。2003 年,他创办了一个名为“The Old New Thing”的网站,其受欢迎程度远远超出了他最疯狂的想象——这一发展至今仍让他毛骨悚然。该网站后来催生了一本书,巧合的是书名也是《The Old New Thing》(Addison Wesley,2007 年)。他偶尔会出现在 Windows Dev Docs 的 Twitter 账号上,讲述一些不传达任何有用信息的故事。

相似文章

不要自己造轮子…

Lobsters Hottest

作者将“不要自己造轮子”的原则扩展到Web开发领域,反对自定义实现滚动、链接导航、文本选择等浏览器原生行为。

选择无聊技术与创新实践

Hillel Wayne — Computer Things

文章认为,团队应选择无聊且已被充分理解的技术以确保可靠性,同时可以在开发实践上自由创新,比如TCR(测试&&提交||回滚),这些实践更易于采纳和放弃,没有长期维护负担。

除了解决问题,什么办法都试过了

Hacker News Top

一篇博客文章,描述了行业资深人士发出的代码滥用警告如何导致关键工作流失败,但同事们没有修复滥用问题,而是提出了各种变通方案,比如添加更多输出处理器或抑制警告——这凸显了工程领域普遍回避解决根本问题的倾向。