正确使用断言和异常

Appropriate usage of assertions and exceptions

我已经阅读了一些内容,试图弄清楚何时适当地使用断言和异常,但我仍然缺少一些全局的东西。可能我只是需要更多的经验,所以我想举一些简单的例子来更好地理解在什么情况下我应该使用什么。

示例 1:让我们从 class 无效值的典型情况开始。例如我有以下 class,其中两个字段都必须为正数:

class Rectangle{
    private int height;
    private int length;

    public int Height{
        get => height;
        set{
            //avoid to put negative heights
        }
    }
    //same thing for length
}

请注意,在此示例中我不是在谈论如何处理用户输入,因为我可以为此做一个简单的控制流程。虽然,我正面临这样的想法,即其他地方可能存在一些意外错误,我希望检测到这一点,因为我不想要一个具有无效值的对象。所以我可以:

问题:我理解断言和异常的含义了吗?另外,您能否举一个例子,其中 处理 异常是有用的(因为 发生之前您无法控制的事情)?我不知道除了我提到的情况之外还会发生什么,但我显然仍然缺乏这方面的经验。
稍微扩展一下我的问题:我可以想到抛出异常的各种原因,例如 NullReferenceExceptionIndexOutOfBoundsException,IO 异常 DirectoryNotFoundExceptionFileNotFoundException,等等。虽然,除了简单地停止程序(在这种情况下,不应该使用断言吗?)或给出问题发生位置的简单消息。我知道即使这是有用的,异常也意味着分类 "errors" 并提供如何修复它们的线索。虽然, 是一个简单的消息,但它们真的对 有用吗? 这听起来很可疑,所以我会坚持使用 "I've never faced a proper situation, 'cause of experience" 咒语。

示例 2:现在让我们使用第一个示例来讨论用户输入。如我所料,我不会使用异常来检查值是否为正,因为这是一个简单的控制流。但是如果用户输入一个字母会发生什么?我应该在这里处理异常(可能是一个简单的 ArgumentException)并在 catch 块中给出消息吗?或者它也可以通过控制流来避免(检查输入是否为 int 类型或类似的类型)?

感谢任何能解开我挥之不去的疑惑的人。

我是这么看的。断言是给程序员的。例外是针对用户的。你可以在你的代码中有你期望特定值的地方。然后你可以进行断言,例如:

public int Age
{
    get { return age; }
    set
    {
        age = value;
        Debug.Assert(age == value);
    }
}

这只是一个例子。所以如果 age != value 也不例外。但是 "hey programmer, something strange may have happened, look at this part of code".

当应用程序不知道如何在特定情况下做出反应时,您可以使用异常。例如:

public int Divide(int a, int b)
{
    Debug.Assert(b != 0); //this is for you as a programmer, but if something bad happened, user won't see this assert, but application also doesn't know what to do in situation like this, so you will add:

    if(b == 0)
        throw SomeException();
}

并且 SomeException 可能会在您的应用程序的其他地方处理。

Throw an ArgumentOutOfRangeException to basically do the same thing? This feels wrong, so I should use that only if I know I'm going to handle it somewhere. Though, if I know where to handle the exception, shouldn't I fix the problem where it lies?

你的推理在这里很好,但相当不对。你挣扎的原因是因为 C# 中的 four 事物使用异常:

  • 愚蠢的例外。愚蠢的异常类似于 "invalid argument" 当调用者可能知道参数无效时 。如果抛出一个愚蠢的异常,那么调用者有一个应该修复的错误。你永远不会在测试用例之外有一个 catch(InvalidArgumentException) 因为它永远不应该被投入生产。这些异常的存在是为了帮助您的调用者编写正确的代码,方法是在他们犯错时大声告诉他们。

  • 令人烦恼的异常 是愚蠢的异常,其中调用者不知道参数无效。这些是 API 中的设计缺陷,应予以消除。他们要求您用 try-catches 包装 API 调用以捕获看起来应该避免而不是捕获的异常。如果您发现您正在编写 API 要求调用者将调用包装在 try-catch 中,那么您做错了什么。

  • 致命异常 是线程中止、内存不足等异常。发生了可怕的事情,该过程无法继续。抓住这些没有什么意义,因为你可以做很多事情来改善这种情况,而且你可能会使情况变得更糟。

  • 外生异常类似于"the network cable is unplugged"。您希望网络电缆已插入;不是,也没有办法早点去查是不是,因为查的时间和使用的时间不一样;电缆可以在这两次之间拔掉。你必须抓住这些。

既然你知道了四种异常是什么,你就可以看出异常和断言之间的区别了。

断言在逻辑上必须始终为真,如果不是,那么您就有一个应该修复的错误。您永远不会断言 网络电缆已插入。您永远不会断言 调用者提供的值不为空。应该 never 是导致断言触发的测试用例;如果有,则测试用例发现了一个错误。

您断言在就地排序算法运行后,非空数组中的最小元素位于开头。应该没有办法可以是假的,如果有,你有一个错误。所以断言这个事实。

相比之下,throw 是一个语句,每个语句都应该有一个测试用例来执行它。 "This API throws when passed null by a buggy caller" 是其合同的一部分,该合同应该是可测试的。如果您发现自己编写的 throw 语句没有 possible 测试用例来验证它们是否抛出,请考虑将其更改为断言。

最后,永远不要传递无效参数然后捕获愚蠢的异常。如果您正在处理用户输入,那么 UI 层应该验证输入在句法上是否有效,即数字应该是数字。 UI 层不应将可能未经审查的用户代码传递给更深层次的 API,然后处理由此产生的异常。