我应该在 private/internal 方法中使用空参数吗?
Should I throw on null parameters in private/internal methods?
我正在编写一个库,其中包含多个 public 类 和方法,以及库本身使用的多个私有或内部 类 和方法。
在 public 方法中,我有一个空检查和一个像这样的抛出:
public int DoSomething(int number)
{
if (number == null)
{
throw new ArgumentNullException(nameof(number));
}
}
但这让我开始思考,我应该将参数空值检查添加到什么级别?我是否也开始将它们添加到私有方法中?我应该只为 public 方法做吗?
虽然您标记了 language-agnostic
,但在我看来它可能不存在 一般 响应。
值得注意的是,在您的示例中,您暗示了参数:因此,对于接受提示的语言,在您可以采取任何操作之前,它会在进入函数后立即触发错误。
在这种情况下,唯一的解决方案是在调用您的函数之前检查参数...但是由于您正在编写一个库,所以这没有意义!
另一方面,在没有提示的情况下,检查函数内部仍然很现实。
所以在反思的这一步,我已经建议放弃暗示了。
现在让我们回到您的确切问题:应该检查到什么级别?
对于给定的数据片段,它只会发生在它可以 "enter" 的最高级别(同一数据可能多次出现),因此从逻辑上讲,它只涉及 public 方法。
这是理论。但也许您计划了一个庞大、复杂的库,因此可能不容易确保确定注册所有 "entry points".
在这种情况下,我建议相反:考虑只在所有地方应用你的控件,然后只在你清楚地看到它重复的地方省略它。
希望对您有所帮助。
在我看来,您应该始终检查 "invalid" 数据 - 独立于它是私有方法还是 public 方法。
从另一个角度看...为什么仅仅因为方法是私有的就可以处理无效的东西?没有道理,对吧?始终尝试使用防御性编程,您的生活会更快乐 ;-)
归根结底,对此没有统一的共识。因此,我不会给出是或否的答案,而是尝试列出做出此决定的考虑因素:
空检查会使您的代码膨胀。如果您的过程简洁,则它们开头的空守卫可能会构成整个过程的重要部分,而不会表达该过程的目的或行为。
Null 检查表达了一个先决条件。如果一个方法在其中一个值为 null 时将失败,那么在顶部进行 null 检查是向不经意的 reader 演示这一点的好方法,而他们不必寻找取消引用的位置。为了改进这一点,人们经常使用名称如 Guard.AgainstNull
的辅助方法,而不必每次都编写检查。
私有方法中的检查不可测试。通过在您的代码中引入一个您无法完全遍历的分支,您就不可能完全测试该方法。这与测试记录 class 的行为以及 class 的代码存在以提供该行为的观点相冲突。
让 null 通过的严重程度取决于具体情况。通常,如果一个 null 确实 进入该方法,它会在几行之后被取消引用,你会得到一个 NullReferenceException
。这并不比抛出 ArgumentNullException
更不清晰。另一方面,如果该引用在被取消引用之前被传递了很多次,或者如果抛出 NRE 会使事情处于混乱状态,那么尽早抛出就更为重要了。
一些库,如 .NET 的代码契约,允许一定程度的静态分析,这可以为您的检查增加额外的好处。
如果您正在与其他人一起开展项目,那么现有的团队或项目标准可能涵盖了这一点。
以下是我的看法:
一般情况
一般来说,最好在使用方法处理它们之前检查是否存在任何无效输入 稳健性 原因 - 就这样吧private, protected, internal, protected internal, or public
方法。虽然这种方法需要 一些性能成本,但在大多数情况下,这样做是值得的,而不是花费 更多 时间来调试和修补代码稍后。
但是严格来说...
然而,严格来说,并不总是需要这样做。某些方法,通常是 private
方法,可以保留 而不进行 任何输入检查,前提是您有 完整 保证没有 单个 调用具有无效输入 的方法。这可能会给您带来 一些性能优势 ,特别是如果该方法被 频繁调用以执行一些基本 computation/action。对于这种情况,检查输入有效性 可能会显着影响 性能。
Public 方法
现在 public
方法更加棘手。这是因为,更严格地说,虽然访问修饰符单独可以告诉谁可以使用这些方法,但它不能 告诉谁 将 使用这些方法。此外,它也无法告诉 如何 将使用这些方法(即,是否将在给定范围内使用无效输入调用这些方法)。
最终决定因素
虽然代码中方法的访问修饰符可以提示如何使用这些方法,但最终将由人类使用这些方法,这取决于人类他们将如何使用它们以及使用什么输入。因此,在极少数情况下,可能有一个 public
方法仅在某些 private
范围内调用,而在该 private
范围内,public
的输入在调用 public
方法之前保证方法有效。
在这种情况下,即使访问修饰符是 public
,也没有任何 real 需要检查无效输入,除了 robust 设计原因。为什么会这样?因为有人完全知道何时和如何调用方法!
这里我们可以看到,也不能保证public
方法总是需要检查无效输入。如果 public
方法是这样,那么 protected, internal, protected internal, and private
方法也一定是这样。
结论
所以,总而言之,我们可以说几句话来帮助我们做出决定:
- 通常,出于稳健设计的原因,最好检查任何无效输入,前提是性能不受影响。 任何 类型的访问修饰符都是如此。
- 可以跳过无效输入检查如果这样做可以显着提高性能,前提是还可以保证调用方法的范围始终是为方法提供有效输入。
private
方法通常是我们跳过这种检查的地方,但不能保证我们不能对 public
方法也这样做
- 人类 是最终使用这些方法的人。不管访问修饰符如何暗示方法的使用,方法的实际使用和调用方式取决于编码人员。因此,我们只能说general/good实践,而不是限制它是唯一做的方法。
如果您不是库开发人员,请不要在代码中采取防御措施
改为编写单元测试
事实上,即使你正在开发一个库,大多数时候也会抛出:BAD
1.在 int
上测试 null
绝不能在 c# 中完成 :
它会引发警告 CS4072,因为它总是错误的。
2。抛出 Exception 意味着它是异常的:异常且罕见。
它不应该在生产代码中引发。特别是因为异常堆栈跟踪遍历可能是一项 cpu 密集型任务。而且你永远无法确定异常会在哪里被捕获,如果它被捕获并记录或者只是默默地忽略(在杀死你的一个后台线程之后),因为你不控制用户代码。 c# 中没有 "checked exception"(如 java),这意味着您永远不知道 - 如果没有很好的记录 - 给定方法可能引发什么异常。顺便说一句,那种文档必须与代码保持同步,这并不总是容易做到(增加维护成本)。
3。异常会增加维护成本。
由于异常是在 运行 时间和特定条件下抛出的,所以它们可能在开发过程的后期才被发现。正如您可能已经知道的那样,在开发过程中检测到错误的时间越晚,修复的成本就越高。我什至看到异常引发代码进入生产代码并且一周没有引发,只是为了以后每天引发(杀死生产。哎呀!)。
4.抛出无效输入意味着您无法控制输入。
public 库方法就是这种情况。但是,如果您可以在编译时使用另一种类型(例如像 int 这样的不可空类型)检查它,那么这是可行的方法。当然,由于他们 public,检查输入是他们的责任。
想象一下用户使用他认为有效的数据,然后由于副作用,堆栈跟踪深处的方法抛出 ArgumentNullException
。
- 他会有什么反应?
- 他如何应对?
- 方便留言解释吗?
5.私有和内部方法永远不应该抛出与其输入相关的异常。
您可能会在代码中抛出异常,因为外部组件(可能是数据库、文件或其他)行为异常,您无法保证您的库将继续 运行 在其当前状态下正确运行。
创建方法 public 并不意味着它应该(只是它可以)从您的库外部调用 (Look at Public versus Published from Martin Fowler)。使用 IOC、接口、工厂并仅发布用户需要的内容,同时使整个库 classes 可用于单元测试。 (或者你可以使用 InternalsVisibleTo
机制)。
6.在没有任何解释消息的情况下抛出异常是在取笑用户
无需提醒工具坏了会有什么感觉,也不知道如何修复它。是的,我知道。你来SO问个问题...
7.输入无效意味着它会破坏您的代码
如果您的代码可以生成具有该值的有效输出,那么它不是无效的,您的代码应该管理它。添加一个单元测试来测试这个值。
8.从用户角度思考:
你喜欢当你使用的库抛出异常来砸你的脸吗?喜欢:"Hey, it's invalid, you should have known that!"
即使从您的角度来看 - 根据您对库内部知识的了解,输入无效,您如何向用户解释它(友善礼貌):
- 清晰的文档(在 Xml 文档和体系结构摘要中可能会有所帮助)。
- 与图书馆一起发布 xml 文档。
- 在异常中清除错误解释。
- 给出选择:
看字典class,你喜欢什么?你认为什么电话最快?什么调用可以引发异常?
Dictionary<string, string> dictionary = new Dictionary<string, string>();
string res;
dictionary.TryGetValue("key", out res);
或
var other = dictionary["key"];
9.为什么不使用 Code Contracts ?
这是避免丑陋 if then throw
并将契约与实现隔离开来的优雅方式,允许同时为不同的实现重用契约。您甚至可以将合约发布给您的图书馆用户,进一步向他解释如何使用图书馆。
作为结论,即使您可以轻松使用 throw
,即使您在使用 .Net Framework 时遇到异常引发,那 并不 意味着它可以不小心使用。
您图书馆的 public 接口值得严格检查前提条件,因为您应该预料到您图书馆的用户会犯错误并意外违反前提条件。帮助他们了解您图书馆中发生的事情。
你库中的私有方法不需要这样的运行时检查,因为你自己调用它们。您可以完全控制所传递的内容。如果你因为害怕搞砸而想添加检查,那么使用断言。它们会发现您自己的错误,但不会影响运行时的性能。
这是一个偏好问题。但是请考虑为什么要检查 null 或者检查有效输入。这可能是因为您想让图书馆的使用者知道 he/she 什么时候使用不当。
假设我们在库中实现了 class PersonList
。此列表只能包含 Person
类型的对象。我们还在 PersonList
上实施了一些操作,因此我们不希望它包含任何空值。
考虑此列表的 Add
方法的以下两个实现:
实施 1
public void Add(Person item)
{
if(_size == _items.Length)
{
EnsureCapacity(_size + 1);
}
_items[_size++] = item;
}
实施 2
public void Add(Person item)
{
if(item == null)
{
throw new ArgumentNullException("Cannot add null to PersonList");
}
if(_size == _items.Length)
{
EnsureCapacity(_size + 1);
}
_items[_size++] = item;
}
假设我们采用实施 1
- 现在可以在列表中添加空值
- 所有 列表上实现的操作必须处理这些空值
- 如果我们应该在我们的操作中检查并抛出异常,当 he/she 调用其中一个操作时,消费者将收到有关异常的通知,并且在这种状态下它会非常不清楚 he/she 做错了(采用这种方法没有任何意义)。
如果我们改为选择实施 2,我们会确保我们库的输入具有我们 class 对其进行操作所需的质量。这意味着我们只需要在这里处理它,然后我们就可以在实现其他操作时忘记它。
当 he/she 在 .Add
上而不是在 he/she 上获得 ArgumentNullException
时,he/she 正在以错误的方式使用库,这也会让消费者更加清楚.Sort
或类似。
总而言之,我的偏好是在消费者提供参数且未由库的 private/internal 方法处理时检查有效参数。这基本上意味着我们必须检查 constructors/methods 中的参数 public 并接受参数。我们的 private
/internal
方法只能从我们的 public 方法中调用,它们已经检查了输入,这意味着我们可以开始了!
验证输入时也应考虑使用 Code Contracts。
我正在编写一个库,其中包含多个 public 类 和方法,以及库本身使用的多个私有或内部 类 和方法。
在 public 方法中,我有一个空检查和一个像这样的抛出:
public int DoSomething(int number)
{
if (number == null)
{
throw new ArgumentNullException(nameof(number));
}
}
但这让我开始思考,我应该将参数空值检查添加到什么级别?我是否也开始将它们添加到私有方法中?我应该只为 public 方法做吗?
虽然您标记了 language-agnostic
,但在我看来它可能不存在 一般 响应。
值得注意的是,在您的示例中,您暗示了参数:因此,对于接受提示的语言,在您可以采取任何操作之前,它会在进入函数后立即触发错误。
在这种情况下,唯一的解决方案是在调用您的函数之前检查参数...但是由于您正在编写一个库,所以这没有意义!
另一方面,在没有提示的情况下,检查函数内部仍然很现实。
所以在反思的这一步,我已经建议放弃暗示了。
现在让我们回到您的确切问题:应该检查到什么级别? 对于给定的数据片段,它只会发生在它可以 "enter" 的最高级别(同一数据可能多次出现),因此从逻辑上讲,它只涉及 public 方法。
这是理论。但也许您计划了一个庞大、复杂的库,因此可能不容易确保确定注册所有 "entry points".
在这种情况下,我建议相反:考虑只在所有地方应用你的控件,然后只在你清楚地看到它重复的地方省略它。
希望对您有所帮助。
在我看来,您应该始终检查 "invalid" 数据 - 独立于它是私有方法还是 public 方法。
从另一个角度看...为什么仅仅因为方法是私有的就可以处理无效的东西?没有道理,对吧?始终尝试使用防御性编程,您的生活会更快乐 ;-)
归根结底,对此没有统一的共识。因此,我不会给出是或否的答案,而是尝试列出做出此决定的考虑因素:
空检查会使您的代码膨胀。如果您的过程简洁,则它们开头的空守卫可能会构成整个过程的重要部分,而不会表达该过程的目的或行为。
Null 检查表达了一个先决条件。如果一个方法在其中一个值为 null 时将失败,那么在顶部进行 null 检查是向不经意的 reader 演示这一点的好方法,而他们不必寻找取消引用的位置。为了改进这一点,人们经常使用名称如
Guard.AgainstNull
的辅助方法,而不必每次都编写检查。私有方法中的检查不可测试。通过在您的代码中引入一个您无法完全遍历的分支,您就不可能完全测试该方法。这与测试记录 class 的行为以及 class 的代码存在以提供该行为的观点相冲突。
让 null 通过的严重程度取决于具体情况。通常,如果一个 null 确实 进入该方法,它会在几行之后被取消引用,你会得到一个
NullReferenceException
。这并不比抛出ArgumentNullException
更不清晰。另一方面,如果该引用在被取消引用之前被传递了很多次,或者如果抛出 NRE 会使事情处于混乱状态,那么尽早抛出就更为重要了。一些库,如 .NET 的代码契约,允许一定程度的静态分析,这可以为您的检查增加额外的好处。
如果您正在与其他人一起开展项目,那么现有的团队或项目标准可能涵盖了这一点。
以下是我的看法:
一般情况
一般来说,最好在使用方法处理它们之前检查是否存在任何无效输入 稳健性 原因 - 就这样吧private, protected, internal, protected internal, or public
方法。虽然这种方法需要 一些性能成本,但在大多数情况下,这样做是值得的,而不是花费 更多 时间来调试和修补代码稍后。
但是严格来说...
然而,严格来说,并不总是需要这样做。某些方法,通常是 private
方法,可以保留 而不进行 任何输入检查,前提是您有 完整 保证没有 单个 调用具有无效输入 的方法。这可能会给您带来 一些性能优势 ,特别是如果该方法被 频繁调用以执行一些基本 computation/action。对于这种情况,检查输入有效性 可能会显着影响 性能。
Public 方法
现在 public
方法更加棘手。这是因为,更严格地说,虽然访问修饰符单独可以告诉谁可以使用这些方法,但它不能 告诉谁 将 使用这些方法。此外,它也无法告诉 如何 将使用这些方法(即,是否将在给定范围内使用无效输入调用这些方法)。
最终决定因素
虽然代码中方法的访问修饰符可以提示如何使用这些方法,但最终将由人类使用这些方法,这取决于人类他们将如何使用它们以及使用什么输入。因此,在极少数情况下,可能有一个 public
方法仅在某些 private
范围内调用,而在该 private
范围内,public
的输入在调用 public
方法之前保证方法有效。
在这种情况下,即使访问修饰符是 public
,也没有任何 real 需要检查无效输入,除了 robust 设计原因。为什么会这样?因为有人完全知道何时和如何调用方法!
这里我们可以看到,也不能保证public
方法总是需要检查无效输入。如果 public
方法是这样,那么 protected, internal, protected internal, and private
方法也一定是这样。
结论
所以,总而言之,我们可以说几句话来帮助我们做出决定:
- 通常,出于稳健设计的原因,最好检查任何无效输入,前提是性能不受影响。 任何 类型的访问修饰符都是如此。
- 可以跳过无效输入检查如果这样做可以显着提高性能,前提是还可以保证调用方法的范围始终是为方法提供有效输入。
private
方法通常是我们跳过这种检查的地方,但不能保证我们不能对public
方法也这样做- 人类 是最终使用这些方法的人。不管访问修饰符如何暗示方法的使用,方法的实际使用和调用方式取决于编码人员。因此,我们只能说general/good实践,而不是限制它是唯一做的方法。
如果您不是库开发人员,请不要在代码中采取防御措施
改为编写单元测试
事实上,即使你正在开发一个库,大多数时候也会抛出:BAD
1.在 int
上测试 null
绝不能在 c# 中完成 :
它会引发警告 CS4072,因为它总是错误的。
2。抛出 Exception 意味着它是异常的:异常且罕见。
它不应该在生产代码中引发。特别是因为异常堆栈跟踪遍历可能是一项 cpu 密集型任务。而且你永远无法确定异常会在哪里被捕获,如果它被捕获并记录或者只是默默地忽略(在杀死你的一个后台线程之后),因为你不控制用户代码。 c# 中没有 "checked exception"(如 java),这意味着您永远不知道 - 如果没有很好的记录 - 给定方法可能引发什么异常。顺便说一句,那种文档必须与代码保持同步,这并不总是容易做到(增加维护成本)。
3。异常会增加维护成本。
由于异常是在 运行 时间和特定条件下抛出的,所以它们可能在开发过程的后期才被发现。正如您可能已经知道的那样,在开发过程中检测到错误的时间越晚,修复的成本就越高。我什至看到异常引发代码进入生产代码并且一周没有引发,只是为了以后每天引发(杀死生产。哎呀!)。
4.抛出无效输入意味着您无法控制输入。
public 库方法就是这种情况。但是,如果您可以在编译时使用另一种类型(例如像 int 这样的不可空类型)检查它,那么这是可行的方法。当然,由于他们 public,检查输入是他们的责任。
想象一下用户使用他认为有效的数据,然后由于副作用,堆栈跟踪深处的方法抛出 ArgumentNullException
。
- 他会有什么反应?
- 他如何应对?
- 方便留言解释吗?
5.私有和内部方法永远不应该抛出与其输入相关的异常。
您可能会在代码中抛出异常,因为外部组件(可能是数据库、文件或其他)行为异常,您无法保证您的库将继续 运行 在其当前状态下正确运行。
创建方法 public 并不意味着它应该(只是它可以)从您的库外部调用 (Look at Public versus Published from Martin Fowler)。使用 IOC、接口、工厂并仅发布用户需要的内容,同时使整个库 classes 可用于单元测试。 (或者你可以使用 InternalsVisibleTo
机制)。
6.在没有任何解释消息的情况下抛出异常是在取笑用户
无需提醒工具坏了会有什么感觉,也不知道如何修复它。是的,我知道。你来SO问个问题...
7.输入无效意味着它会破坏您的代码
如果您的代码可以生成具有该值的有效输出,那么它不是无效的,您的代码应该管理它。添加一个单元测试来测试这个值。
8.从用户角度思考:
你喜欢当你使用的库抛出异常来砸你的脸吗?喜欢:"Hey, it's invalid, you should have known that!"
即使从您的角度来看 - 根据您对库内部知识的了解,输入无效,您如何向用户解释它(友善礼貌):
- 清晰的文档(在 Xml 文档和体系结构摘要中可能会有所帮助)。
- 与图书馆一起发布 xml 文档。
- 在异常中清除错误解释。
- 给出选择:
看字典class,你喜欢什么?你认为什么电话最快?什么调用可以引发异常?
Dictionary<string, string> dictionary = new Dictionary<string, string>();
string res;
dictionary.TryGetValue("key", out res);
或
var other = dictionary["key"];
9.为什么不使用 Code Contracts ?
这是避免丑陋 if then throw
并将契约与实现隔离开来的优雅方式,允许同时为不同的实现重用契约。您甚至可以将合约发布给您的图书馆用户,进一步向他解释如何使用图书馆。
作为结论,即使您可以轻松使用 throw
,即使您在使用 .Net Framework 时遇到异常引发,那 并不 意味着它可以不小心使用。
您图书馆的 public 接口值得严格检查前提条件,因为您应该预料到您图书馆的用户会犯错误并意外违反前提条件。帮助他们了解您图书馆中发生的事情。
你库中的私有方法不需要这样的运行时检查,因为你自己调用它们。您可以完全控制所传递的内容。如果你因为害怕搞砸而想添加检查,那么使用断言。它们会发现您自己的错误,但不会影响运行时的性能。
这是一个偏好问题。但是请考虑为什么要检查 null 或者检查有效输入。这可能是因为您想让图书馆的使用者知道 he/she 什么时候使用不当。
假设我们在库中实现了 class PersonList
。此列表只能包含 Person
类型的对象。我们还在 PersonList
上实施了一些操作,因此我们不希望它包含任何空值。
考虑此列表的 Add
方法的以下两个实现:
实施 1
public void Add(Person item)
{
if(_size == _items.Length)
{
EnsureCapacity(_size + 1);
}
_items[_size++] = item;
}
实施 2
public void Add(Person item)
{
if(item == null)
{
throw new ArgumentNullException("Cannot add null to PersonList");
}
if(_size == _items.Length)
{
EnsureCapacity(_size + 1);
}
_items[_size++] = item;
}
假设我们采用实施 1
- 现在可以在列表中添加空值
- 所有 列表上实现的操作必须处理这些空值
- 如果我们应该在我们的操作中检查并抛出异常,当 he/she 调用其中一个操作时,消费者将收到有关异常的通知,并且在这种状态下它会非常不清楚 he/she 做错了(采用这种方法没有任何意义)。
如果我们改为选择实施 2,我们会确保我们库的输入具有我们 class 对其进行操作所需的质量。这意味着我们只需要在这里处理它,然后我们就可以在实现其他操作时忘记它。
当 he/she 在 .Add
上而不是在 he/she 上获得 ArgumentNullException
时,he/she 正在以错误的方式使用库,这也会让消费者更加清楚.Sort
或类似。
总而言之,我的偏好是在消费者提供参数且未由库的 private/internal 方法处理时检查有效参数。这基本上意味着我们必须检查 constructors/methods 中的参数 public 并接受参数。我们的 private
/internal
方法只能从我们的 public 方法中调用,它们已经检查了输入,这意味着我们可以开始了!
验证输入时也应考虑使用 Code Contracts。