65.9K
CodeProject 正在变化。 阅读更多。
Home

错误,家族中的害群之马

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.50/5 (4投票s)

2018年11月14日

CPOL

10分钟阅读

viewsIcon

5979

审视开发家族中所谓的“害群之马”,即编译器错误和异常

引言

在这篇文章中,我们将审视开发家族中所谓的“害群之马”,即错误,具体来说是编译器错误异常

我将它们视为害群之马的原因是,它们常常声名狼藉,尽管它们存在的目的是让我们警惕和避免其他风险。可以将它们类比为我们的身体感知到的疼痛,如果某处疼痛,那很可能对您不利,但并非总是如此。

鉴于上述情况,当您遇到错误或异常时,请尝试找出原因,而不是仅仅从互联网或同事那里盲目地实施解决方案。对于异常,即使看似可以解决问题,也不要随意地捕获所有异常然后不做任何处理。

它们不仅应该被接受,我还鼓励在应用程序的整个生命周期中使用和抛出自己的错误。并且请不要只抛出一个新的Exception,花点时间根据情况使用内置的或者自己的。

首先,我需要声明一个免责声明:我非常厌恶泛化,这意味着本文中我们将讨论的所有内容都应谨慎对待,并根据具体情况运用您的判断。

编译器错误

我们最近在关于关键字的用法和滥用的文章中讨论过这个问题,当时我们讨论了为什么我们无法实例化abstract类。现在我们将看看一些常见的编译器错误,并尝试找出它们背后的原因。

运算符“Operator”不能应用于类型“type”和“type”的操作数

在过去(C++ 和 C)和现在(JavaScript)中,您可以在 `if` 子句中使用任何值,除 `null` 或 `0` 之外的任何值都表示 `true`。但 `if(1)` 在代码的上下文中到底意味着什么?它很难理解,因为很多东西都可以表示 `true`,这使得您想做什么存在各种不确定性。

当编译器对您想做什么感到困惑时,就会出现此错误。当您尝试将 `true` 与 `false` 相加,或者 `PersonA` 大于 `PersonB` 时,它意味着什么?

有两种方法可以解决此错误:一种是使用正确的语法,例如,如果您有一个条件,请确保它求值为 `true` 或 `false`。第二种方法是让编译器理解您的意图,例如,如果我们想比较 `PersonA` 和 `PersonB`(或任何其他自定义类),那么您需要实现一个适当的运算符重载,以便编译器知道您通过比较两个类(或者更准确地说,两个类型)的意图(例如,您可以比较 `PersonA` 和值 `18`,并实际表示它需要比较年龄,尽管如果您真的想这样做,为什么不实现一个自定义方法并明确说明呢?)。

运算符“operator”在类型“type1”和“type2”的操作数上存在歧义

此错误与前一个错误属于同一类,编译器对您想实现的目标感到困惑。例如,如果您尝试将 `BicicleA` 与 `CarB` 相加,结果会是什么?如果您尝试将 `Bridge` 类型的实例分配给 `Fridge` 类型的变量会怎样?

此错误出现有两个考虑因素:第一个是语义上的,您尝试进行的操作真的有意义吗?第二个考虑因素是内存占用,每种类型都定义了一个模板,说明它需要多少内存以及如何布局,每个位属于哪个部分。当然,在内存中,一切都是字节和位,但它们的含义相同吗?它们可用吗?

要解决此问题,您需要进行类型转换,让编译器知道您的意图。当然,如果我们讨论的是自定义类型,那么您需要实现显式或隐式类型转换。

“member”由于其保护级别而无法访问

如果我们遵循了我们在关键字用法和滥用第一部分中讨论的关于正确使用访问修饰符的建议,那么您会更频繁地遇到此错误,但这只是告诉您您正在违反关于正确封装成员的规则。

解决方案是提高该成员的权限,或者更好的是,考虑您最初为什么需要它。最终可能会得出提高权限的相同解决方案,但这也可能促使您考虑其他替代方案。

控件无法从一个 Case 标签(“label”)贯穿到另一个

此错误比 Java 或 C++ 等其他语言(如果我没记错的话)更具体地指向 C#。

在那些语言中,您可以有一个 switch 语句调用一个 case,然后由于缺乏流程终止(例如使用 `return`、`break`、`throw` 或 `continue`)而贯穿到另一个 case。

有时,这会带来好处,但在大多数情况下,它会在后期导致错误,因为编译器没有为我们提供支持,阻止我们做一些愚蠢的事情,比如忘记流程控制。

使用了未赋值的局部变量“name”

我记得我在学校学习 C++ 时,您需要记住初始化声明的每一个变量,否则,当您尝试使用它时,它将输出垃圾值(它实际上是该变量所占用的内存,但唉!)。

由于这个编译器规则,我从未遇到过变量中出现垃圾值的问题。解决方案很简单:在使用变量之前对其进行初始化,否则编译器不允许。

找不到类型或命名空间名称“type/namespace”(是否缺少 using 指令或程序集引用?)

这个错误在开发人员键入他们想要的内容但忘记添加 `using` 语句时非常常见。我承认,自从我使用 Resharper 以来,我很少遇到这个问题。

其背后的原因是,我们正在尝试使用编译器不知道的东西。

编译器错误总结

有关所有编译器错误的完整列表,您可以在此处找到。

作为关于此的额外建议,始终打开“将警告视为错误”。编译器还会发出警告,而不仅仅是错误,这些警告通常会被忽略,因为应用程序可以运行!!!但请记住,当您重构了某些东西,最终得到了一个不再使用的变量或字段时?编译器会抱怨但仍允许您编译,但下次您还会记得为什么需要那个字段或变量吗?也许不会,因为它目前不使用,所以没有带来价值。

异常

这些是在应用程序运行时抛出的错误,并且它们被设计成在明确的条件下抛出。仍然存在一些系统,它们不抛出异常,而是返回错误代码,讨厌!想象一下有一本关于应用程序中使用的错误代码的书籍或文档,以及哪个代码代表什么以及它是如何发生的。

异常解决了这个问题,通过向开发人员提供更有用的信息(大多数时候),例如错误消息,尝试解释其发生的原因,当然还有非常有用的堆栈跟踪,它向我们展示了为了达到那种场景而调用的函数(尽管对于有状态开发,即使这样可能也不够)。

如果发生异常,作为开发人员,我们的工作是找出原因,以及它是否不可避免(例如,尝试访问受限制的文件或数据库超时),以及如何处理它,可能重试操作,或者向用户显示一个用户友好的消息,告知他们操作失败的原因,可能由于输入错误或其他出现的问题。

NullReferenceException

这是访问尚未实例化的类的成员时最常见的异常之一,您可能会遇到此错误。如果您对这个异常有多么普遍感到好奇,请尝试在任何代码库中搜索 `null` 检查(与 `null` 的比较),看看您会找到多少。我敢打赌,大多数项目至少有 25% 的方法包含某种形式的 `null` 检查。

这个异常之所以变得如此糟糕,是因为即使是 .NET 框架也通过 `as` 运算符或 `TryParse` 和 `FirstOrDefault` 等方法提供了更多的检查机会,这些方法要么返回 `null` 本身,要么返回布尔值而不是处理异常。别误会我的意思,这些是必需的,但就像关键字一样,它们有时会被滥用,而不是真正解决问题的根本原因。

这里有一个在重载方法中非常常见的模式,我们将看看它们是如何遇到的以及我认为它们应该如何处理。

int ComputeTaxes(int stateId, Location location = null)
{
    if (location == null) { //do something...}
}

现在来看看处理它的方法,根据具体情况,我认为有几种处理方法,但这并非一个完整的列表。

public int ComputeTaxes(int stateId, Location location)
{
    if (location == null) { location = new NullLocation(); }

    // or use the syntax location = location ?? new NullLocation();

    return ComputeTaxesInternal(state, location)
}

private int ComputeTaxesInternal(int stateId, Location location)
{

}

在此场景中,我们使用了Null Object Pattern,它允许我们定义一个不执行任何操作但也不会抛出错误的对象的。这有助于我们避免在计算算法中引入 `null` 检查。

public int ComputeTaxes(int stateId, Location location)
{
    if (location == null) 
       { throw new ArgumentNullException("location should not be null", "location"); }

    int taxes = ComputeTaxes(state);

    // do calculations on the location

    return taxes;
}

public int ComputeTaxes(int stateId) { //do logic here}

在这种情况下,我们尽早抛出异常,并确保方法的先决条件工作正常,然后我们就能在进行任何计算之前就知道我们遇到了问题。

这个异常的结论是:使用它,让它显示设计的缺陷,不要通过 `null` 检查来丢弃或避免它。如果一个集合必须包含一个项,请使用 `First` 方法而不是 `FirstOrDefault`;如果一个对象必须可转换为某种类型,请直接进行类型转换而不是使用 `as` 运算符。问题是我们为什么会得到一个 Null Reference Exception,而不是我们如何在本地避免它。

ArgumentNullException

当接收方法的参数时,尤其对于库和框架开发人员来说,应该尽可能频繁地抛出此异常(当然,如果需要的话,不要过度使用),一是为了确保方法能够正常工作,二是为了在源头上发现问题。

异常总结

我知道这一部分示例少了很多,但 .NET Framework 中有大量的异常,再加上您或您的团队将开发的异常。我相信到目前为止,您已经遇到过不止一次这些异常。

结论

仅仅因为错误给我们一个“耳光”,并不意味着它们是为了让我们感到愚蠢或惩罚我们,它们存在都有坚实的原因,应该被理解并 accordingly 地处理。因此,在去 StackOverflow 寻找如何避免错误的解决方案之前,花点时间阅读其文档,了解其发生的原因,然后进行适当的更改。

谢谢,下次再见。

© . All rights reserved.