MSDN 异常文档查找






4.71/5 (9投票s)
本文特别关注成员异常文档的创建;这是文档流程中一个经常被低估的部分。
引言
近年来,许多开发团队中出现了一种令人担忧的趋势。我所指的趋势基于“代码应自我注释”的理念。这种心态认为,优雅源于简洁,而代码在没有被任意注释、签名花体框和装饰其内容的冗余阐述所污染时,才最美丽、最富表现力。自我注释代码理念的一个关键支柱是,名称应该清晰、简洁或在其约定中显而易见。例如,一个加密类(Cryptography class)的 Encrypt 函数显然是用来加密一个值并返回密文响应的。为什么要通过解释参数、返回结果以及在错误条件下可能抛出的异常来降低此例程的简洁性呢?在包含 Decrypt 和 Encrypt 函数的加密对象(Cryptographic object)的简单示例中,可以认为函数、变量和参数的名称仅提供了对该事物是什么的最低限度描述。它缺乏其他开发人员想要了解的细节,例如如何使用该函数、什么可能导致函数失败,以及对象/成员设计过程中涉及的思考过程。简单来说,代码本身是你代码正在解决问题的“如何”部分,即使是最“干净”的命名约定也无法推断出“为何”的部分。
我能欣赏代码之美,以及编写代码作为一种独特而令人满意的技术艺术表现形式。然而,没有注释的代码几乎无法被任何非原始作者维护,在极端情况下,甚至原始开发人员也无法维护。与开发者沟通的唯一真正机制是通过文档。特别是摘要(summary)、说明(remarks)、示例(example)、参数(param)、返回(return)和异常(exception)文档标记。这种程度的文档丰富了协作的可能性,因为其他开发者可以通过 HTMLHelp 或 Visual Studio 的 IntelliSense 功能一窥你的对象模型,而无需深入研究实际的代码库。
下文将假设其读者在一个团队环境中工作,开发职责是共享的,并且所有团队成员都应能够修改、支持和更新任何其他团队成员所产生的可交付成果。文章还将假设读者的开发团队实现了多种代码文档帮助框架之一(例如 NDoc、Castle、PHPDoc 等)。
本文特别关注成员异常文档的创建;这是文档流程中一个经常被低估的部分。
你还文档化吗,哥们?
“你说什么呢,我可是Xtreme Programmer。
文档是给菜鸟、小孩和wannabe 准备的。”
-- DEFCON 2008 出席者
让我们来看一个相对常规的函数。一个自我注释且干净的函数。它的名称、参数和输出对任何阅读代码的开发人员来说都显而易见。
public virtual string Decrypt(string cipher, string privateKey)
{
if (String.IsNullOrEmpty(privateKey))
throw new NullReferenceException("Private key cannot be empty or null.");
if (privateKey.Length < 24)
throw new InvalidOperationException("Private key must be 24 chars (192 bit).");
var toEncryptArray = Convert.FromBase64String(cipher);
var keyArray = Encoding.UTF8.GetBytes(privateKey);
byte[] resultArray;
var result = string.Empty;
using (var tdes = new TripleDESCryptoServiceProvider())
{
tdes.Key = keyArray;
tdes.Mode = CipherMode.ECB;
tdes.Padding = PaddingMode.PKCS7;
var cTransform = tdes.CreateDecryptor();
resultArray = cTransform.TransformFinalBlock(toEncryptArray, 0, toEncryptArray.Length);
tdes.Clear();
result = Encoding.UTF8.GetString(resultArray);
}
// Paranoid verification
var verifyCipher = Encrypt(result);
if (verifyCipher != cipher)
throw new CryptographicException("Bad Data. Paranoid verification failure.");
return result;
}
查看上面的代码,您可能很快就能确定没有 try 语句,并且如果未满足特定条件,该例程会故意抛出几个异常,例如 `privateKey` 小于 192 位或为空。这在查看代码时相当明显。至少您知道您的应用程序(调用者)将需要实现 try/catch 功能来处理潜在的异常抛出。
现在,请看同一个对象类中的一个常见 **Try* ** 包装函数。
public virtual bool TryDecrypt(string cipher, string privateKey, out string value, out Exception exception)
{
bool result = false;
exception = null;
value = String.Empty;
try
{
value = Decrypt(cipher, privateKey);
result = true;
}
catch (Exception e)
{
exception = e;
}
return result;
}
同样,与该函数包装的函数一样,您可以清楚地看到整个函数都受到舒适的 try/catch 结构保护,因此如果您不想处理方法抛出的潜在错误,这就是外部调用者想要使用的函数。该方法干净整洁,具有简单易读的名称,任何人都可以理解。很明显,该方法是为了让开发人员在无需处理低级 `Decrypt()` 函数可能抛出的所有异常的情况下解密值。
现在我们退一步,考虑这些功能如何在实际世界中使用。它们肯定会被编译成一个代码库,例如:Baileysoft.Security.Cryptography.dll。这些库很可能经过数字签名,对其进行更改可能会触发某种变更管理工作流程。重点是,可重用代码通常不能轻易地进行分析和检查,也不应该如此。作为开发人员,您专注于您正在工程的产品的业务规则,而不必过多关注您所引用的所有底层机制。
现在想象一下,我们正在处理一个需要加密的解决方案(这几乎是所有事情)。我们已经添加了我们库的引用,并且正在处理解密部分。从 IntelliSense(通过将鼠标悬停在 Decrypt 成员上)我们可以看到,我们什么都没有。没有描述,没有记录的异常,也没有关于该函数的详细信息。
这意味着我们可以假设该函数不会抛出异常,因此我们不需要实现 try/catch 逻辑!
当然,这是一个错误的假设——该成员没有得到适当的文档记录。
通过在 Visual Studio 中使用鼠标悬停在库的 Decrypt 函数的各个调用上,我们看到了代码可能抛出错误的所有地方。
命名空间、类、方法和参数都很干净、易读,并且通常易于理解。真正的问题是我们想使用一个代码库,它将在多个任务关键的客户解决方案中提供关键的安全服务,但我们不知道如何正确实现它。Visual Studio IntelliSense 没有提供异常列表这一事实应该向我们表明,该例程的设计方式是它不能将异常向上抛出到调用堆栈。在这种情况下,函数没有文档,所以我们只能猜测是否需要实现 try/catch 异常处理。
让我们回去记录这个成员可能抛出的所有异常,以及一些额外的细节来帮助我们的团队成员。除了异常元素,我们还将充实整个文档足迹。足迹包括摘要、参数详细信息、返回信息,以及可能最受赞赏的……一个关于如何正确使用该函数的完整工作示例。
/// <summary>
/// Decrypts a cipher-text value.
/// </summary>
/// <param name="cipher">The encrypted cipher hash string text.</param>
/// <returns>string</returns>
/// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
/// <exception cref="System.ArgumentOutOfRangeException">The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.</exception>
/// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
/// <exception cref="System.InvalidOperationException">The exception that is thrown when a method call is invalid for the object's current state. Details: Private key must be 24 chars (192 bit).</exception>
/// <exception cref="System.Text.EncoderFallbackException">The exception that is thrown when an encoder fallback operation fails. This class cannot be inherited.</exception>
/// <exception cref="System.Security.Cryptography.CryptographicException">The exception that is thrown when an error occurs during a cryptographic operation.</exception>
/// <return><see cref="string"/></return>
/// <example>
/// <code>
/// <![CDATA[
/// Encryption crypto = new Encryption();
/// crypto.PrivateKey = "yh89wyFa544DJLfXXdMY6AP2";
///
/// string hash = "Mjdz0Q1MIesS2EAIlEXbsrnZRBja01SfnfVoswgiOKd3w3w/kK7i/w==";
/// string result = String.Empty;
///
/// try
/// {
/// result = crypto.Decrypt(hash);
/// Console.WriteLine("Decrypted String: {0}", result);
/// }
/// catch (Exception ex)
/// {
/// Console.WriteLine("An error occurred: {0}", ex.Message);
/// }
/// ]]>
/// </code>
/// </example>
public virtual string Decrypt(string cipher)
{
return Decrypt(cipher, this.PrivateKey);
}
异常标签中的值是从哪里来的?
通过在 Visual Studio 中将鼠标悬停在例程的各个调用上,我能够识别出哪些异常可能从我的 _Decrypt()_ 函数(及其重载)中抛出。
这些值从何而来?
/// <exception cref="System.NullReferenceException">The exception that is thrown when there is an attempt to dereference a null object reference.</exception>
/// <exception cref="System.ArgumentOutOfRangeException">The exception that is thrown when the value of an argument is outside the allowable range of values as defined by the invoked method.</exception>
/// <exception cref="System.ArgumentNullException">The exception that is thrown when a null reference (Nothing in Visual Basic) is passed to a method that does not accept it as a valid argument.</exception>
/// <exception cref="System.InvalidOperationException">The exception that is thrown when a method call is invalid for the object's current state. Details: Private key must be 24 chars (192 bit).</exception>
/// <exception cref="System.Text.EncoderFallbackException">The exception that is thrown when an encoder fallback operation fails. This class cannot be inherited.</exception>
/// <exception cref="System.Security.Cryptography.CryptographicException">The exception that is thrown when an error occurs during a cryptographic operation.</exception>
我们以前的做法是导航到 MSDN 并像这样替换异常名称
// Address
https://msdn.microsoft.com/en-us/library/${name}(VS.90).aspx
// Becomes
https://msdn.microsoft.com/en-us/library/System.Security.Cryptography.CryptographicException(VS.90).aspx
一旦我们有了异常描述,我们就必须将其粘贴到正确格式化的 `<exception cref="FullyQualifiedTypeName">The exception description from MSDN.</exception>` 中。
本文提供的工具将允许您输入异常名称,它会为您获取描述,并将格式化的字符串复制到剪贴板,以便您可以直接粘贴到现有的注释块中(例如,`<exception cref="FullyQualifiedTypeName">The exception description from MSDN.</exception>`)。
ReSharper 7 和 Agent Johnson
如果您觉得为工具输入每个异常然后将获取的数据粘贴到 Visual Studio 的注释块中太麻烦,那么您可能需要考虑使用ReSharper 和Agent Johnson 插件。此插件允许您只需单击鼠标即可立即文档化方法抛出的所有异常!
!! 警告!! 警告!! 警告!!
Agent Johnson 似乎与 ReSharper V7.1 以外的任何版本都不兼容。
这意味着您只能使用 VS2012。
ReSharper 8/9 和 Exceptional
有一个适用于 ReSharper 8 和 9 的可用异常文档工具,名为 Exceptional,它复制了 Agent Johnson 的功能。
http://exceptional.codeplex.com/
HTML 帮助文档
除了 Visual Studio IntelliSense 集成外,您的异常文档还将出现在您的 HTML Help 框架实现中。请参阅下面的截图,清楚地向开发人员识别可以抛出的异常。
太穷买不起 ReSharper?
我包含了一个可以从 Visual Studio 快速启动的小程序,用于从 MSDN 获取异常描述,并将生成的格式化异常 XML 字符串复制到您的剪贴板。
设置 C# 异常查找工具
步骤 1:添加外部工具
在 Visual Studio 中导航到 Tools > External Tools...
步骤 2:配置外部工具
在对话框中,添加一个名称并浏览到 ExceptionDescriptionFinder.exe 的位置。
步骤 3:运行工具
在编写代码时,通过 Tools > C# Exception Lookup 打开工具。
步骤 4:用法
下面您可以看到我已经确定 `System.UnauthorizedAccessException` 是抛出的异常之一。我在下拉列表中输入异常名称并单击 Enter。该工具从 MSDN 获取异常详细信息,并将 XML 字符串复制到剪贴板。然后我按 CTRL+V 将完整的 XML 字符串粘贴到方法签名上方。
展望未来
毫无疑问,使用我提供的工具很繁琐。可惜,职场改进或效率提升的预算中很少有资金。如果有人有时间深入研究,这里有一些链接,可能允许创建 Visual Studio 扩展或全面的工具,以动态发现方法抛出的异常并将它们注入文档花体框中。
资源
- Agent Johnson 源代码:https://code.google.com/p/agentjohnsonplugin/
- Exception Reflector 源代码:https://github.com/stevesheldon/ExceptionReflector
- ILReader 源代码:http://blogs.msdn.com/b/haibo_luo/archive/2006/11/06/system-reflection-based-ilreader.aspx
历史
- 2015-09-08:初始发布
- 2015-09-08:添加了关于 Exceptional R# 插件的说明