If you're just going to sit there doing nothing, at least do nothing correctly (2024)
If you’re just going to sit there doing nothing, at least do nothing correctly (2024)
如果你注定要无所作为,至少要以正确的方式“无所作为”(2024)
There may be times where you need to make an API do nothing. It’s important to have it do nothing in the correct way. For example, Windows has an extensive printing infrastructure. But that infrastructure does not exist on Xbox. What should happen if an app tries to print on an Xbox? Well, the wrong thing to do is to have the printing functions throw a NotSupportedException.
有时你可能需要让某个 API “什么都不做”。重要的是,要以正确的方式让它“什么都不做”。例如,Windows 拥有庞大的打印基础设施,但 Xbox 上并不存在这套设施。如果一个应用程序尝试在 Xbox 上打印,应该发生什么?显然,错误的做法是让打印函数抛出 NotSupportedException。
The app that the user installed on the Xbox was probably tested primarily, if not exclusively, on a PC, where printing is always available. When run on an Xbox, the exception will probably go unhandled, and the app will crash. Even if the app tried to catch the exception, it would probably display a message like “Oops. That went badly. Call support and provide this incident code.” 用户在 Xbox 上安装的应用程序,其测试环境很可能主要(甚至完全)是在 PC 上进行的,而 PC 上总是支持打印的。当它在 Xbox 上运行时,该异常很可能无法被处理,导致程序崩溃。即使应用程序尝试捕获该异常,它也可能会显示类似“糟糕,出错了。请联系技术支持并提供此事件代码”的消息。
A better design for “supporting” printing on Xbox is to have the printing functions succeed, but report that there are no printers installed. With this behavior, when the app tries to print, it will ask the user to select a printer, and show an empty list. The user realizes, “Oh, there are no printers,” and cancels the printing request. 对于在 Xbox 上“支持”打印,更好的设计是让打印函数执行成功,但报告当前没有安装打印机。在这种行为模式下,当应用程序尝试打印时,它会要求用户选择打印机,并显示一个空列表。用户会意识到“哦,没有打印机”,然后取消打印请求。
To deal with apps that get fancy and say “Oh, you have no printers installed, let me help you install one,” the function for installing a printer can return immediately with a result code that means “The user cancelled the operation.” The idea here is to have the printing functions all behave in a manner perfectly consistent with printing being fully supported, yet mysteriously there is never a printer to print to. 为了应对那些“自作聪明”的应用程序(比如提示“哦,你没有安装打印机,让我帮你安装一个”),安装打印机的函数可以立即返回一个表示“用户取消了操作”的结果代码。这里的核心思想是:让所有打印函数表现得就像打印功能被完全支持一样,但诡异的是,永远找不到可用的打印机。
Now, you probably also want to add a function to check whether printing even works at all. Apps can use this function to hide the Print button from their UI if they are running on a system that doesn’t support printing at all. But naïve apps that assume that printing works will still behave in a reasonable manner: You’re just on a system that doesn’t have any printers and all attempts to install a printer are ineffective. 当然,你可能还需要添加一个函数来检查打印功能是否可用。应用程序可以使用此函数,在不支持打印的系统上隐藏 UI 中的“打印”按钮。但那些天真地假设打印功能一定可用的应用程序,依然会以合理的方式运行:它们只会认为当前系统没有任何打印机,且所有安装打印机的尝试都无效。
The name we use to describe this “do nothing” behavior is “inert”. The API surface still exists and functions according to its specification, but it also does nothing. The important thing is that it does nothing in a way that is consistent with its documentation and is least likely to create problems with existing code. 我们用“惰性”(inert)来描述这种“什么都不做”的行为。API 接口依然存在,并按照规范运行,但它实际上什么都没做。关键在于,它以一种符合文档说明且最不容易对现有代码造成干扰的方式来“无所作为”。
Another example is the retirement of an API that has a variety of functions for creating widget handles, other functions that accept widget handles, and a function for closing widget handles. The team that was doing the retirement originally proposed making the API inert as follows: 另一个例子是某个 API 的退役,该 API 包含多种创建组件句柄(widget handles)的函数、接收句柄的函数,以及关闭句柄的函数。负责退役工作的团队最初建议将该 API 设置为如下的“惰性”状态:
HRESULT CreateWidget(_Out_ HWIDGET* widget) {
*widget = nullptr;
return S_OK;
}
// Every widget is documented to have at least one alias,
// so we have to produce one dummy alias (empty string).
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;
}
// Inert widgets cannot be enabled or disabled.
HRESULT EnableWidget(HWIDGET widget, BOOL value) {
return E_HANDLE;
}
HRESULT Close(HWIDGET widget) {
RETURN_HR_IF(E_INVALIDARG, widget != nullptr);
return S_OK;
}
I pointed out that having CreateWidget succeed but return a null pointer is going to confuse apps. “The call succeeded, but I didn’t get a valid handle back?” I even found some of their own test code that checked whether the handle was null to determine whether the call succeeded, rather than checking the return value.
我指出,让 CreateWidget 执行成功却返回一个空指针会让应用程序感到困惑。“调用成功了,但我没拿到有效的句柄?”我甚至发现他们自己的一些测试代码是通过检查句柄是否为空来判断调用是否成功的,而不是检查返回值。
I also pointed out that having EnableWidget return “invalid handle” is also going to create confusion. An app calls CreateWidget, and it succeeds, and it takes that handle (which is presumably valid) and tries to use it to enable a widget, and it’s told “That handle isn’t valid.” How can that be? “I asked for a widget, and you gave me one, and then when I showed it to you, you said, ‘That’s not a widget.’ This API is gaslighting me!”
我还指出,让 EnableWidget 返回“无效句柄”也会造成困惑。应用程序调用 CreateWidget 成功后,拿着那个(理应有效的)句柄去启用组件,结果却被告知“该句柄无效”。这怎么可能?“我向你要组件,你给了我一个,结果当我拿给你看时,你却说‘这不是组件’。这个 API 在对我进行煤气灯效应(gaslighting)式的精神操控!”
I looked through the existing documentation for their API and found that a documented return value is ERROR_CANCELLED to mean that the user cancelled the creation of the widget. Therefore, apps are already dealing with the possibility of widgets not being created due to conditions outside their control, so we can take advantage of that: Any time the app tries to create a widget, just say “Nope, the, uh, user cancelled, yeah, that’s what happened.”
我查阅了该 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;
}
Now we have a proper inert API surface. If you try to create a widget, we tell you that we couldn’t because the user cancelled. Since all attempts to create a widget fail, there is no such thing as a valid widget handle, and any time you try to use one, we tell you that the handle is invalid. This also avoids the problem of having to produce dummy aliases for widgets. Since there are no widgets, there is no legitimate case where an app could ask a widget for its aliases. 现在我们拥有了一个规范的“惰性”API 接口。如果你尝试创建组件,我们会告诉你因为用户取消而无法创建。由于所有创建组件的尝试都会失败,因此根本不存在有效的组件句柄;每当你尝试使用句柄时,我们都会告诉你该句柄无效。这也避免了必须为组件生成虚拟别名的问题。既然根本没有组件,那么应用程序也就没有理由去查询组件的别名了。
Bonus chatter: To clear up some confusion: The idea here is that the printing API has always existed on desktop, where printing is supported, and the “get me the list of printers” function is documented not to throw an exception. If you want to port the printing API to Xbox, how do you do it in a way that allows existing desktop apps to continue to run on Xbox? The inert behavior is completely truthful: There are no printers on an Xbox. Nobody expects the answer to the question, “How many printers are there?” to be “How dare you ask me such a thing!” Another scenario where you need to create an inert API surface is if you want to retire an existing… 补充说明:为了消除一些困惑,这里的核心思想是:打印 API 一直存在于桌面端,且桌面端支持打印,“获取打印机列表”函数在文档中明确规定不会抛出异常。如果你想将打印 API 移植到 Xbox,如何才能让现有的桌面应用程序在 Xbox 上继续运行?这种“惰性”行为是完全诚实的:Xbox 上确实没有打印机。没人会希望当问出“有多少台打印机?”时,得到的回答是“你怎么敢问我这种问题!”。另一种需要创建“惰性”API 接口的场景是,当你想要退役一个现有的……