C# 多少空检查就足够了?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/302736/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 22:21:54  来源:igfitidea点击:

How much null checking is enough?

c#javac++design-by-contract

提问by James Schek

What are some guidelines for when it is notnecessary to check for a null?

什么是当它的一些准则不是必要的检查空?

A lot of the inherited code I've been working on as of late has null-checks ad nauseam. Null checks on trivial functions, null checks on API calls that state non-null returns, etc. In some cases, the null-checks are reasonable, but in many places a null is not a reasonable expectation.

我最近一直在处理的许多继承代码都有令人作呕的空检查。对琐碎函数进行空检查,对声明非空返回的 API 调用进行空检查等。在某些情况下,空检查是合理的,但在许多地方,空不是合理的期望。

I've heard a number of arguments ranging from "You can't trust other code" to "ALWAYS program defensively" to "Until the language guarantees me a non-null value, I'm always gonna check." I certainly agree with many of those principles up to a point, but I've found excessive null-checking causes other problems that usually violate those tenets. Is the tenacious null checking really worth it?

我听过很多争论,从“你不能相信其他代码”到“总是防御性地编程”到“直到语言保证我有一个非空值,我总是会检查”。我当然同意其中的许多原则,但我发现过多的空检查会导致其他通常违反这些原则的问题。顽强的空检查真的值得吗?

Frequently, I've observed codes with excess null checking to actually be of poorer quality, not of higher quality. Much of the code seems to be so focused on null-checks that the developer has lost sight of other important qualities, such as readability, correctness, or exception handling. In particular, I see a lot of code ignore the std::bad_alloc exception, but do a null-check on a new.

通常,我观察到具有过多空检查的代码实际上质量较差,而不是质量较高。大部分代码似乎过于关注空值检查,以至于开发人员忽略了其他重要品质,例如可读性、正确性或异常处理。特别是,我看到很多代码忽略了 std::bad_alloc 异常,但对new.

In C++, I understand this to some extent due to the unpredictable behavior of dereferencing a null pointer; null dereference is handled more gracefully in Java, C#, Python, etc. Have I just seen poor-examples of vigilant null-checking or is there really something to this?

在 C++ 中,由于取消引用空指针的不可预测行为,我在某种程度上理解了这一点;在 Java、C#、Python 等中,null 取消引用处理得更优雅。我是否刚刚看到了警惕的 null 检查的糟糕例子,或者真的有什么问题吗?

This question is intended to be language agnostic, though I am mainly interested in C++, Java, and C#.

这个问题旨在与语言无关,尽管我主要对 C++、Java 和 C# 感兴趣。



Some examples of null-checking that I've seen that seem to be excessiveinclude the following:

我所看到的一些似乎过度的空检查示例包括以下内容:



This example seems to be accounting for non-standard compilers as C++ spec says a failed new throws an exception. Unless you are explicitly supporting non-compliant compilers, does this make sense? Does this make anysense in a managed language like Java or C# (or even C++/CLR)?

这个例子似乎考虑了非标准编译器,因为 C++ 规范说失败的 new 会引发异常。除非您明确支持不合规的编译器,否则这有意义吗?这是否任何像Java或C#(甚至C ++ / CLR)托管的语感?

try {
   MyObject* obj = new MyObject(); 
   if(obj!=NULL) {
      //do something
   } else {
      //??? most code I see has log-it and move on
      //or it repeats what's in the exception handler
   }
} catch(std::bad_alloc) {
   //Do something? normally--this code is wrong as it allocates
   //more memory and will likely fail, such as writing to a log file.
}


Another example is when working on internal code. Particularly, if it's a small team who can define their own development practices, this seems unnecessary. On some projects or legacy code, trusting documentation may not be reasonable... but for new code that you or your team controls, is this really necessary?

另一个例子是在处理内部代码时。特别是,如果是一个可以定义自己的开发实践的小团队,这似乎没有必要。在某些项目或遗留代码中,信任文档可能不合理……但对于您或您的团队控制的新代码,这真的有必要吗?

If a method, which you can see and can update (or can yell at the developer who is responsible) has a contract, is it still necessary to check for nulls?

如果一个你可以看到并且可以更新(或者可以对负责的开发人员大喊大叫)的方法有一个合同,是否仍然需要检查空值?

//X is non-negative.
//Returns an object or throws exception.
MyObject* create(int x) {
   if(x<0) throw;
   return new MyObject();
}

try {
   MyObject* x = create(unknownVar);
   if(x!=null) {
      //is this null check really necessary?
   }
} catch {
   //do something
}


When developing a private or otherwise internal function, is it really necessary to explicitly handle a null when the contract calls for non-null values only? Why would a null-check be preferable to an assert?

在开发私有或其他内部函数时,是否真的有必要在合约仅调用非空值时显式处理空值?为什么空检查比断言更可取?

(obviously, on your public API, null-checks are vital as it's considered impolite to yell at your users for incorrectly using the API)

(显然,在您的公共 API 上,空检查至关重要,因为对您的用户大喊大叫不正确地使用 API 被认为是不礼貌的)

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value, or -1 if failed
int ParseType(String input) {
   if(input==null) return -1;
   //do something magic
   return value;
}

Compared to:

相比:

//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value
int ParseType(String input) {
   assert(input!=null : "Input must be non-null.");
   //do something magic
   return value;
}

采纳答案by Steve Jessop

First note that this a special case of contract-checking: you're writing code that does nothing other than validate at runtime that a documented contract is met. Failure means that some code somewhere is faulty.

首先请注意,这是合同检查的一种特殊情况:您正在编写的代码除了在运行时验证已记录的合同是否得到满足之外什么都不做。失败意味着某处的某些代码有问题。

I'm always slightly dubious about implementing special cases of a more generally useful concept. Contract checking is useful because it catches programming errors the first time they cross an API boundary. What's so special about nulls that means they're the only part of the contract you care to check? Still,

我总是有点怀疑实现一个更普遍有用的概念的特殊情况。契约检查很有用,因为它会在它们第一次跨越 API 边界时捕获编程错误。空值有什么特别之处,意味着它们是您需要检查的合同的唯一部分?仍然,

On the subject of input validation:

关于输入验证的主题:

null is special in Java: a lot of Java APIs are written such that null is the only invalid value that it's even possible to pass into a given method call. In such cases a null check "fully validates" the input, so the full argument in favour of contract checking applies.

null 在 Java 中很特殊:编写了许多 Java API,因此 null 是唯一一个甚至可能传递给给定方法调用的无效值。在这种情况下,空检查“完全验证”了输入,因此支持合同检查的完整参数适用。

In C++, on the other hand, NULL is only one of nearly 2^32 (2^64 on newer architectures) invalid values that a pointer parameter could take, since almost all addresses are not of objects of the correct type. You can't "fully validate" your input unless you have a list somewhere of all objects of that type.

另一方面,在 C++ 中,NULL 只是指针参数可以采用的近 2^32 个(在较新的体系结构上为 2^64 个)无效值中的一个,因为几乎所有地址都不是正确类型的对象。除非您在某处拥有该类型所有对象的列表,否则您无法“完全验证”您的输入。

The question then becomes, is NULL a sufficiently common invalid input to get special treatment that (foo *)(-1)doesn't get?

那么问题就变成了,NULL 是否是一个足够常见的无效输入来获得(foo *)(-1)无法获得的特殊待遇?

Unlike Java, fields don't get auto-initialized to NULL, so a garbage uninitialized value is just as plausible as NULL. But sometimes C++ objects have pointer members which are explicitly NULL-inited, meaning "I don't have one yet". If your caller does this, then there is a significant class of programming errors which can be diagnosed by a NULL check. An exception may be easier for them to debug than a page fault in a library they don't have the source for. So if you don't mind the code bloat, it might be helpful. But it's your caller you should be thinking of, not yourself - this isn't defensive coding, because it only 'defends' against NULL, not against (foo *)(-1).

与 Java 不同,字段不会自动初始化为 NULL,因此垃圾未初始化值与 NULL 一样合理。但有时 C++ 对象具有指针成员,这些成员显式为 NULL 初始化,意思是“我还没有”。如果您的调用者这样做,那么有一类重要的编程错误可以通过 NULL 检查来诊断。对他们来说,异常可能比他们没有源的库中的页面错误更容易调试。因此,如果您不介意代码膨胀,它可能会有所帮助。但你应该考虑的是你的调用者,而不是你自己——这不是防御性编码,因为它只“防御”NULL,而不是 (foo *)(-1)。

If NULL isn't a valid input, you could consider taking the parameter by reference rather than pointer, but a lot of coding styles disapprove of non-const reference parameters. And if the caller passes you *fooptr, where fooptr is NULL, then it has done nobody any good anyway. What you're trying to do is squeeze a bit more documentation into the function signature, in the hope that your caller is more likely to think "hmm, might fooptr be null here?" when they have to explicitly dereference it, than if they just pass it to you as a pointer. It only goes so far, but as far as it goes it might help.

如果 NULL 不是有效输入,您可以考虑通过引用而不是指针获取参数,但许多编码风格不赞成非常量引用参数。如果调用者传递给您 *fooptr,其中 fooptr 为 NULL,那么无论如何它对任何人都没有任何好处。您要做的是将更多文档压缩到函数签名中,希望您的调用者更有可能认为“嗯,这里的 fooptr 可能为空吗?” 当他们必须显式取消引用它时,而不是将它作为指针传递给您。它只能走这么远,但就其发展而言,它可能会有所帮助。

I don't know C#, but I understand that it's like Java in that references are guaranteed to have valid values (in safe code, at least), but unlike Java in that not all types have a NULL value. So I'd guess that null checks there are rarely worth it: if you're in safe code then don't use a nullable type unless null is a valid input, and if you're in unsafe code then the same reasoning applies as in C++.

我不知道 C#,但我知道它就像 Java 一样,保证引用具有有效值(至少在安全代码中),但与 Java 不同的是,并非所有类型都具有 NULL 值。所以我猜空检查很少值得:如果你在安全代码中,那么不要使用可空类型,除非 null 是一个有效的输入,如果你在不安全的代码中,那么同样的推理适用于在 C++ 中。

On the subject of output validation:

关于输出验证的主题:

A similar issue arises: in Java you can "fully validate" the output by knowing its type, and that the value isn't null. In C++, you can't "fully validate" the output with a NULL check - for all you know the function returned a pointer to an object on its own stack which has just been unwound. But if NULL is a common invalid return due to the constructs typically used by the author of the callee code, then checking it will help.

出现了类似的问题:在 Java 中,您可以通过了解其类型来“完全验证”输出,并且该值不为空。在 C++ 中,您不能使用 NULL 检查来“完全验证”输出 - 众所周知,该函数返回了一个指向其自身堆栈上刚刚展开的对象的指针。但是,如果由于被调用者代码的作者通常使用的构造,NULL 是常见的无效返回,那么检查它会有所帮助。

In all cases:

在所有情况下:

Use assertions rather than "real code" to check contracts where possible - once your app is working, you probably don't want the code bloat of every callee checking all its inputs, and every caller checking its return values.

在可能的情况下使用断言而不是“真实代码”来检查合同 - 一旦您的应用程序正常运行,您可能不希望每个被调用者检查其所有输入以及每个调用者检查其返回值的代码膨胀。

In the case of writing code which is portable to non-standard C++ implementations, then instead of the code in the question which checks for null and also catches the exception, I'd probably have a function like this:

在编写可移植到非标准 C++ 实现的代码的情况下,而不是问题中检查 null 并捕获异常的代码,我可能有这样的函数:

template<typename T>
static inline void nullcheck(T *ptr) { 
    #if PLATFORM_TRAITS_NEW_RETURNS_NULL
        if (ptr == NULL) throw std::bad_alloc();
    #endif
}

Then as one of the list of things you do when porting to a new system, you define PLATFORM_TRAITS_NEW_RETURNS_NULL (and maybe some other PLATFORM_TRAITS) correctly. Obviously you can write a header which does this for all the compilers you know about. If someone takes your code and compiles it on a non-standard C++ implementation that you know nothing about, they're fundamentally on their own for bigger reasons than this, so they'll have to do it themselves.

然后,作为移植到新系统时要做的事情之一,您可以正确定义 PLATFORM_TRAITS_NEW_RETURNS_NULL(可能还有其他一些 PLATFORM_TRAITS)。显然,您可以编写一个头文件,为您知道的所有编译器执行此操作。如果有人使用您的代码并在您一无所知的非标准 C++ 实现上编译它,那么出于比这更大的原因,他们基本上是靠自己的,所以他们必须自己做。

回答by MetroidFan2002

If you write the code and its contract, you are responsible for using it in terms of its contract and ensuring the contract is correct. If you say "returns a non-null" x, then the caller should not check for null. If a null pointer exception then occurs with that reference/pointer, it is your contract that is incorrect.

如果您编写代码及其合同,则您有责任按照其合同使用它并确保合同正确。如果您说“返回非空值”x,则调用者不应检查空值。如果该引用/指针出现空指针异常,则是您的合同不正确。

Null checking should only go to the extreme when using a library that is untrusted, or does not have a proper contract. If it is your development team's code, stress that the contracts must not be broken, and track down the person who uses the contract incorrectly when bugs occur.

空检查应该只在使用不受信任或没有适当合同的库时走极端。如果是你的开发团队的代码,强调合同不能被破坏,并在出现错误时追踪错误使用合同的人。

回答by dkretz

It's widely known that there are procedure-oriented people (focus on doing things the right way) and results-oriented people (get the right answer). Most of us lie somewhere in the middle. Looks like you've found an outlier for procedure-oriented. These people would say "anything's possible unless you understand things perfectly; so prepare for anything." For them, what you see is done properly. For them if you change it, they'll worry because the ducks aren't all lined up.

众所周知,有面向过程的人(专注于以正确的方式做事)和面向结果的人(得到正确的答案)。我们大多数人都在中间的某个地方。看起来您已经找到了面向过程的异常值。这些人会说“除非你完全理解,否则一切皆有可能;所以要做好准备。” 对他们来说,你所看到的都是正确的。对他们来说,如果你改变它,他们会担心,因为鸭子不是都排成一列的。

When working on someone else's code, I try to make sure I know two things.
1. What the programmer intended
2. Why they wrote the code the way they did

在处理别人的代码时,我尽量确保我知道两件事。
1. 程序员的意图
2. 为什么他们按照他们的方式编写代码

For following up on Type A programmers, maybe this helps.

对于跟进 A 类程序员,也许这会有所帮助。

So "How much is enough" ends up being a social question as much as a technical question - there's no agreed-upon way to measure it.

因此,“多少才足够”最终既是一个技术问题,又是一个社会问题——没有商定的方法来衡量它。

(It drives me nuts too.)

(它也让我发疯。)

回答by JoshBerke

One thing to remember that your code that you write today while it may be a small team and you can have good documentation, will turn into legacy code that someone else will have to maintain. I use the following rules:

需要记住的一件事是,您今天编写的代码虽然可能是一个小团队并且您可以拥有良好的文档,但将变成其他人必须维护的遗留代码。我使用以下规则:

  1. If I'm writing a public API that will be exposed to others, then I will do null checks on all reference parameters.

  2. If I'm writing an internal component to my application, I write null checks when I need to do something special when a null exists, or when I want to make it very clear. Otherwise I don't mind getting the null reference exception since that is also fairly clear what is going on.

  3. When working with return data from other peoples frameworks, I only check for null when it is possible and valid to have a null returned. If their contract says it doesn't return nulls, I won't do the check.

  1. 如果我正在编写一个公开给其他人的公共 API,那么我将对所有引用参数进行空检查。

  2. 如果我正在为我的应用程序编写一个内部组件,当我需要在 null 存在时做一些特殊的事情时,或者当我想非常清楚时,我会编写 null 检查。否则我不介意得到空引用异常,因为这也很清楚发生了什么。

  3. 在处理来自其他人的框架的返回数据时,我只在返回空值可能且有效时检查空值。如果他们的合同说它不返回空值,我不会做检查。

回答by Aaron

When you can specify which compiler is being used, for system functions such as "new" checking for null is a bug in the code. It means that you will be duplicating the error handling code. Duplicate code is often a source of bugs because often one gets changed and the other doesn't. If you can not specify the compiler or compiler versions, you should be more defensive.

当您可以指定正在使用哪个编译器时,对于诸如“new”之类的系统函数,检查 null 是代码中的一个错误。这意味着您将复制错误处理代码。重复的代码通常是错误的来源,因为通常一个被更改而另一个没有。如果你不能指定编译器或编译器版本,你应该更加防御。

As for internal functions, you should specify the contract and make sure that contract is enforce via unit tests. We had a problem in our code a while back where we either threw an exception or returned null in case of a missing object from our database. This just made things confusing for the caller of the api so we went through and made it consistant throughout the entire code base and removed the duplicate checks.

至于内部功能,您应该指定合约并确保通过单元测试执行合约。不久前,我们在代码中遇到了一个问题,我们要么抛出异常,要么在数据库中缺少对象的情况下返回 null。这只会让 api 的调用者感到困惑,所以我们通过并使其在整个代码库中保持一致并删除了重复检查。

The important thing (IMHO) is to not have duplicate error logic where one branch will never be invoked. If you can never invoke code, then you can't test it, and you will never know if it is broken or not.

重要的是(恕我直言)没有重复的错误逻辑,其中一个分支永远不会被调用。如果你永远不能调用代码,那么你就无法测试它,你永远不会知道它是否被破坏了。

回答by djuth

Part of this depends on how the code is used -- if it is a method available only within a project vs. a public API, for example. API error checking requires something stronger than an assertion.

这部分取决于代码的使用方式——例如,它是仅在项目中可用的方法还是公共 API。API 错误检查需要比断言更强大的东西。

So while this is fine within a project where it's supported with unit tests and stuff like that:

因此,虽然这在支持单元测试和类似内容的项目中很好:

internal void DoThis(Something thing)
{
    Debug.Assert(thing != null, "Arg [thing] cannot be null.");
    //...
}

in a method where you don't have control over who calls it, something like this may be better:

在您无法控制谁调用它的方法中,这样的方法可能会更好:

public void DoThis(Something thing)
{
    if (thing == null)
    {
        throw new ArgumentException("Arg [thing] cannot be null.");
    }
    //...
}

回答by Tom Hawtin - tackline

Lower level code should check use from higher level code. Usually this means checking arguments, but it can mean checking return values from upcalls. Upcall arguments need not be checked.

较低级别的代码应该检查来自较高级别代码的使用。通常这意味着检查参数,但也可能意味着检查调用的返回值。不需要检查上行参数。

The aim is to catch bugs in immediate and obvious ways, as well as documenting the contract in code that does not lie.

目的是以直接和明显的方式捕获错误,并在不说谎的代码中记录合同。

回答by Nazgob

NULL checking in general is evil as it's add a small negative token to the code testability. With NULL checks everywhere you can't use "pass null" technique and it will hit you when unit testing. It's better to have unit test for the method than null check.

NULL 检查通常是邪恶的,因为它为代码可测试性添加了一个小的负面标记。使用 NULL 检查无处不在,你不能使用“pass null”技术,它会在单元测试时击中你。对方法进行单元测试比空检查更好。

Check out decent presentation on that issue and unit testing in general by Misko Hevery at http://www.youtube.com/watch?v=wEhu57pih5w&feature=channel

http://www.youtube.com/watch?v=wEhu57pih5w&feature=channel 上查看Misko Hevery 关于该问题和单元测试的体面介绍

回答by RichieHH

Personally I think null testing is unnnecessary in the great majority of cases. If new fails or malloc fails you have bigger issues and the chance of recovering is just about nil in cases where you're not writing a memory checker! Also null testing hides bugs a lot in the development phases since the "null" clauses are frequently just empty and do nothing.

我个人认为在大多数情况下不需要空测试。如果 new 失败或 malloc 失败,您会遇到更大的问题,并且在您没有编写内存检查器的情况下,恢复的机会几乎为零!此外,空测试在开发阶段隐藏了很多错误,因为“空”子句通常只是空的,什么也不做。

回答by fizzer

It depends on the situation. The rest of my answer assumes C++.

这取决于实际情况。我的其余答案假设 C++。

  • I never test the return value of new since all the implementations I use throw bad_alloc on failure. If I see a legacy test for new returning null in any code I'm working on, I cut it out and don't bother to replace it with anything.
  • Unless small minded coding standards prohibit it, I assert documented preconditions. Broken code which violates a published contract needs to fail immediately and dramatically.
  • If the null arises from a runtime failure which isn't due to broken code, I throw. fopen failure and malloc failure (though I rarely if ever use them in C++) would fall into this category.
  • I don't attempt to recover from allocation failure. Bad_alloc gets caught in main().
  • If the null test is for an object which is collaborator of my class, I rewrite the code to take it by reference.
  • If the collaborator really might not exist, I use the Null Objectdesign pattern to create a placeholder to fail in well defined ways.
  • 我从不测试 new 的返回值,因为我使用的所有实现都在失败时抛出 bad_alloc。如果我在我正在处理的任何代码中看到 new 返回 null 的旧测试,我就会将其删除,并且不会费心用任何东西替换它。
  • 除非心胸狭隘的编码标准禁止它,否则我断言记录在案的先决条件。违反已发布合同的损坏代码需要立即且显着地失败。
  • 如果 null 是由不是由于代码损坏的运行时故障引起的,我会抛出。fopen 失败和 malloc 失败(尽管我很少在 C++ 中使用它们)将属于这一类。
  • 我不会尝试从分配失败中恢复。Bad_alloc 在 main() 中被捕获。
  • 如果空测试是针对我班级的合作者的对象,我会重写代码以通过引用获取它。
  • 如果合作者真的可能不存在,我会使用空对象设计模式来创建一个占位符,以明确定义的方式失败。

回答by Mark Ransom

Older versions of Microsoft C++ (and probably others) did not throw an exception for failed allocations via new, but returned NULL. Code that had to run in both standard-conforming and older versions would have the redundant checking that you point out in your first example.

旧版本的 Microsoft C++(可能还有其他版本)不会因 new 分配失败而引发异常,而是返回 NULL。必须在符合标准的版本和旧版本中运行的代码将具有您在第一个示例中指出的冗余检查。

It would be cleaner to make all failed allocations follow the same code path:

让所有失败的分配都遵循相同的代码路径会更清晰:

if(obj==NULL)
    throw std::bad_alloc();