6

标题可能无法真正解释我真正想表达的意思,也想不出一种方法来描述我的意思。

我想知道在使用函数之前检查函数接受的参数是否为空值或空值是否是一种好习惯。我有这个函数,它只是像这样包装一些哈希创建。

Public Shared Function GenerateHash(ByVal FilePath As IO.FileInfo) As String
        If (FilePath Is Nothing) Then
            Throw New ArgumentNullException("FilePath")
        End If

        Dim _sha As New Security.Cryptography.MD5CryptoServiceProvider
        Dim _Hash = Convert.ToBase64String(_sha.ComputeHash(New IO.FileStream(FilePath.FullName, IO.FileMode.Open, IO.FileAccess.Read)))
        Return _Hash
    End Function

如您所见,我只是将 IO.Fileinfo 作为参数,在函数开始时我正在检查以确保它不是什么都不是。

我想知道这是一种好的做法,还是应该让它到达实际的哈希器,然后抛出异常,因为它是空的。?

谢谢。

4

10 回答 10

20

一般来说,我建议在使用公共函数/方法之前验证所有参数是一种很好的做法,并且尽早失败而不是在执行一半函数之后失败。在这种情况下,抛出异常是正确的。

根据您的方法正在做什么,早期失败可能很重要。如果您的方法正在更改您的类上的实例数据,您不希望它更改一半的数据,然后遇到 null 并引发异常,因为您的对象的数据可能处于中间且可能无效的状态。

如果您使用的是 OO 语言,那么我建议验证公共方法的参数是必不可少的,但对于私有和受保护的方法则不太重要。我的理由是你不知道公共方法的输入是什么——任何其他代码都可以创建你的类的一个实例并调用它的公共方法,并传入意外/无效的数据。但是,私有方法是从类内部调用的,并且该类应该已经验证了内部传递的任何数据。

于 2008-10-13T22:39:00.627 回答
3

我最喜欢的 C++ 技术之一是对 NULL 指针进行 DEBUG_ASSERT。这是高级程序员(以及 const 正确性)向我灌输的,这是我在代码审查期间最严格的事情之一。我们从来没有在没有首先断言它不为空的情况下取消引用一个指针。

调试断言仅对调试目标有效(它在发布中被剥离),因此您在生产中没有额外的开销来测试数千个 if。通常它会抛出异常或触发硬件断点。我们甚至有系统会抛出一个带有文件/行信息的调试控制台和一个忽略断言的选项(一次或无限期地用于会话)。这是一个非常棒的调试和 QA 工具(我们会在测试人员屏幕上获得带有断言的屏幕截图以及如果忽略程序是否继续的信息)。

我建议在代码中声明所有不变量,包括意外的空值。如果 if 的性能成为问题,请找到一种方法来有条件地编译并使它们在调试目标中保持活动状态。就像源代码控制一样,这种技术救了我的命比它给我带来的痛苦更多(这是任何开发技术中最重要的试金石)。

于 2008-10-13T22:40:38.737 回答
2

是的,最好在方法的开头验证所有参数并引发适当的异常,例如 ArgumentException、ArgumentNullException 或 ArgumentOutOfRangeException。

如果方法是私有的,只有你程序员可以传递无效的参数,那么你可以选择断言每个参数是有效的(Debug.Assert)而不是抛出。

于 2008-10-13T22:40:27.877 回答
1

如果 NULL 是不可接受的输入,则抛出异常。你自己,就像你在你的样本中所做的那样,所以这个信息是有帮助的。

处理 NULL 输入的另一种方法是依次响应 NULL。取决于函数的类型——在上面的例子中,我会保留异常。

于 2008-10-13T22:33:21.167 回答
1

如果它用于面向外部的 API,那么我会说您要检查每个参数,因为输入不可信。

但是,如果它只是在内部使用,那么输入应该能够被信任,并且您可以为自己节省一堆不会为软件增加价值的代码。

于 2008-10-13T22:36:53.390 回答
1

您应该根据您在该函数中对它们的值所做的一组假设检查所有参数。

就像在您的示例中一样,如果您的函数的 null 参数没有任何意义,并且您假设使用您的函数的任何人都会知道这一点,那么传递一个 null 参数会显示某种错误并采取某种措施(例如. 抛出异常)。而且,如果您使用断言(正如 James Fassett 进来并在我面前说的 ;-)),它们在发布版本中不会花费您任何费用。(它们在调试版本中也几乎没有花费你)

同样的事情适用于任何其他假设。

如果生成错误,跟踪错误会比将其留给某些标准库例程抛出异常更容易。您将能够提供更多有用的上下文信息。

它超出了这个问题的范围,但您确实需要公开您的函数所做的假设 - 例如,通过您的函数的注释标题。

于 2008-10-13T22:42:50.807 回答
1

根据实用程序员由 Andrew Hunt 和 David Thomas 撰写,调用者有责任确保它提供有效的输入。因此,您现在必须选择是否将空输入视为有效。除非将 null 视为有效输入具有特定意义(例如,如果您正在测试相等性,将 null 视为合法输入可能是个好主意),我会认为它无效。这样,您的程序在遇到错误输入时会更快失败。如果您的程序将遇到错误情况,您希望它尽快发生。如果您的函数无意中传递了一个空值,您应该将其视为一个错误,并做出相应的反应(即,您应该考虑使用杀死程序的断言,而不是抛出异常,直到您释放程序)。

经典契约设计:输入正确,输出正确。如果输入错误,则存在错误。(如果输入正确但输出错误,则存在错误。这是给我的。)

于 2008-10-14T00:15:46.187 回答
1

我将根据布赖恩早些时候提供的合同建议为出色的设计添加一些详细说明(粗体)......

“按合同设计”的原则要求您定义调用者可以传入的内容(输入值的有效域),然后对于任何有效输入,方法/提供者将做什么。

对于内部方法,您可以将 NULL 定义为有效输入参数的域之外。在这种情况下,您将立即断言输入参数值不是 NULL。 此合约规范的关键见解是,任何传入 NULL 值的调用都是CALLER'S BUG,断言语句抛出的错误是正确的行为。

现在,虽然定义非常明确且简洁,但如果您将方法公开给外部/公共调用者,您应该问自己,这是我/我们真正想要的合同吗?可能不是。在公共接口中,您可能会接受 NULL(从技术上讲,在该方法接受的输入域中),但随后拒绝优雅地处理带有返回消息。(更多的工作来满足自然更复杂的面向客户的需求。)

在任何一种情况下,您所追求的是一个从调用者和提供者的角度处理所有情况的协议,而不是大量的分散测试,这些测试会使评估合同的完整性或缺乏完整性变得困难条件覆盖。

于 2008-10-14T02:31:47.113 回答
0

大多数时候,只要你确定异常不会被忽略,让它抛出异常是非常合理的。

但是,如果您可以向其中添加一些内容,那么将异常包装为更准确的异常并重新抛出它并没有什么坏处。解码“NullPointerException”将比“IllegalArgumentException(“必须提供文件路径”)”(或其他)花费更长的时间。

最近我一直在一个平台上工作,在这个平台上你必须在测试之前运行一个混淆器。每个堆栈跟踪看起来都像猴子在随机输入废话,所以我养成了一直检查我的参数的习惯。

我希望在变量和参数上看到“可为空”或“非空”修饰符,以便编译器可以为您检查。

于 2008-10-13T22:39:16.350 回答
0

如果您正在编写公共 API,请帮助调用者帮助他们快速找到错误并检查有效输入。

如果您正在编写调用者(或调用者的调用者)可能不受信任的 API,请检查有效输入,因为它具有良好的安全性。

如果您的 API 只能由受信任的调用者访问,例如 C# 中的“内部”,那么不要觉得您必须编写所有额外的代码。它不会对任何人有用。

于 2008-10-13T22:41:26.660 回答