当前位置: 首页 > news >正文

NullReferenceException,就不应该存在!

如果要你说出 .NET 中的三个异常,NullReferenceException 一定会成为其中一个;如果说出 .NET 中的一个异常,NullReferenceException 也会被大多数人说出来。它让这么多人印象深刻,是因为它在项目中实在是太常见了,常见到每一个 C#/.NET 入门者必然会遇到。

然而,这个异常本不应该存在!


NullReferenceException 的可恨之处

你说 NullReferenceException 可以告诉你程序中某个字段为 null,告诉你程序发生了 BUG。

可是这是真的吗?说真的一定是因为用 Visual Studio 调试了,Visual Studio 告诉了我们异常发生在哪一句,哪个字段为 null。然而从真实用户或其他日志那里收集回来的数据是没有也不可能有这些信息的。这是因为 NullReferenceException 异常除了调用栈(StackTrace)之外不能提供其他额外的异常信息,连变量或字段名都不能提供。于是,当从异常日志准备分析异常原因的时候,只能猜,猜到底为 null 的是谁!

另外,NullReferenceException 异常发生的地方一定不是真正出错的地方!因为我们尝试去调用某个属性或方法时假设了它不为 null,这意味着它为 null 就是个错误。但是,从异常的调用栈中我们却找不到任何痕迹能够告诉我们是哪里给它设置成了 null(或者是从未赋值过)。现在,又只能猜,猜到底是什么时候通过什么方式将字段设为了 null

举个例子:

public class Walterlv
{
    private string _value;

    public void SetValue(string value)
    {
        _value = value;
    }

    public void DoSomething()
    {
        Console.WriteLine(_value.Length);
    }
}

SetValue 可以在任何时候被任何方法调用,指不定某个时候 _value 就被设为 null 了。那么 DoSomething 被调用的时候,直接就会抛出 NullReferenceException。这个方法比较简单,我们猜 _valuenull 基本不会有问题了,方法复杂一点儿就难猜了。然而真正让 _valuenull 的罪魁祸首就找不到了,因为它发生在 SetValue 中。

总结起来,可恨之处有亮点:

  1. 不能知道为 null 的是哪个变量、字段或属性;
  2. 不能知道为什么为 null

而这两点直接与异常机制相悖。异常就是要提供足够我们诊断错误的信息,让我们在开发中避免发生这样的错误。

NullReferenceException 的替代方案

既然 NullReferenceException 没能给我们提供足够的信息,那么我们就自己来提供这些信息。

ArgumentNullException 就是一个不错的替代异常,说它好因为有两点:

  1. 在错误发生的最开始就报告了错误,避免错误的蔓延。
    因为 SetValue 中发生了异常后,获取到的调用栈是导致 _valuenull 的调用栈。
  2. 告知了为 null 的参数名称。

靠以上两点,当发生异常时,我们能唯一确定 _valuenull 的原因,而这才是本质错误。

可是,如果并不是参数问题导致了 null,那我们还能用什么异常呢?InvalidOperationException 是个不错的方案,它的默认异常提示语是“对象当前的状态使得该操作无效”。当程序此时此刻的状态让我们获取不到某个数据致使数据为 null 时,可以写一个新的提示语告知此时到底是什么样的状态错误才使得获取到的数据为 null。当然,这比 ArgumentNullException 的信息准确性还是差了点儿。

当然,还有一个替代方案,就是在 Console.WriteLine(_value.Length); 之前先对 _value 进行 null 判断。可是,你能说出 _valuenull 代表什么意义吗?为什么为 null 时不应该输出?如果这个问题回答不上来,那么你的这个 null 判断为你的程序埋藏了一个更深的 BUG——当用户反馈软件行为不正常时,你甚至连异常信息都没收集到!硕大一个程序,你甚至都无法定位到底是哪个模块发生了错误!!!

对待 null,建议的约定

当了解了 NullReferenceException 的缺陷,再了解了其替代方案后,其实我们会发现一个问题:

  • 其实多数时候根本就不应该存在 null

null 带来了两个困惑:

  1. 意义不明确。相比于异常,null 并不能告知我们到底发生了什么。
  2. 使用方不知道究竟应不应该判空,也难以理清楚判空究竟意味着什么。

所以,为了解决这些困惑,我建议在开发中以如下方式对待我们的 null

  1. 对任何可被外部模块调用的方法的参数进行 null 判断,并在参数为 null 时抛出 ArgumentNullException
  2. 不要在方法中返回 null。如果你无法根据现有状态完成方法承诺的任务,请抛出具体的异常并给出真实的原因。
  3. 如果确实要用 null 在程序中代表某种状态,请确定这能够代表某种唯一确定的状态,并强制要求使用方判空。

其中,对于第 2 点,不用担心异常导致雪崩,因为 try-catch-finally 就是用来恢复错误防止雪崩的,在需要防止雪崩的地方恢复错误即可。但要注意异常依然需要报告,可由程序统一处理这些未经处理的异常。

对于第 3 点,JetBrains 为我们提供了 JetBrains.Annotations,这是一组 100+ 个的 Attribute,以 NuGet 包的形式提供。强烈建议在 null 代表了某种特殊意义的地方标记 [CanBeNull];这样,ReSharper 插件将提醒我们这些地方必须要进行判空。C# 8.0 极有可能为我们带来“可空引用类型”或者“非空引用类型”;如果真的带来了,这将比 JetBrains.Annotations 拥有更大的强制性,帮助我们避免出现意外的 null 引用,帮助我们在可能为 null 的地方强制判空。再次重申:我们使用 null 一定是因为它代表了某种确定的特殊含义,而不是代表了一堆不明所以的错误!

相关文章:

  • 当我们使用 MVVM 模式时,我们究竟在每一层里做些什么?
  • 分享一个算法,计算能在任何背景色上清晰显示的前景色
  • WPF 绘制对齐像素的清晰显示的线条
  • 让 ScrollViewer 的滚动带上动画
  • UI 设计中的视觉无障碍设计
  • 为什么委托的减法(- 或 -=)可能出现非预期的结果?(Delegate Subtraction Has Unpredictable Result)
  • 将美化进行到底,使用 Oh My Posh 把 PowerShell 做成 oh-my-zsh 的样子
  • 实现一个 WPF 版本的 ConnectedAnimation
  • C#/.NET 中的契约
  • WPF 自定义键盘焦点样式(FocusVisualStyle)
  • 异步任务中的重新进入(Reentrancy)
  • 迫不及待地体验了一把 C#8.0 中的可空引用类型(Nullable Reference)
  • C#/.NET 匿名函数会捕获变量,并延长对象的生命周期
  • 卡诺模型(KANO Model)
  • .NET 中的轻量级线程安全
  • 【许晓笛】 EOS 智能合约案例解析(3)
  • 5分钟即可掌握的前端高效利器:JavaScript 策略模式
  • Django 博客开发教程 16 - 统计文章阅读量
  • HashMap ConcurrentHashMap
  • HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP
  • iOS高仿微信项目、阴影圆角渐变色效果、卡片动画、波浪动画、路由框架等源码...
  • JavaScript对象详解
  • Laravel 中的一个后期静态绑定
  • Python十分钟制作属于你自己的个性logo
  • spring cloud gateway 源码解析(4)跨域问题处理
  • thinkphp5.1 easywechat4 微信第三方开放平台
  • vue--为什么data属性必须是一个函数
  • Vue学习第二天
  • 案例分享〡三拾众筹持续交付开发流程支撑创新业务
  • 基于axios的vue插件,让http请求更简单
  • 探索 JS 中的模块化
  • 微服务核心架构梳理
  • 我是如何设计 Upload 上传组件的
  • 【干货分享】dos命令大全
  • (MonoGame从入门到放弃-1) MonoGame环境搭建
  • (Redis使用系列) Springboot 使用redis的List数据结构实现简单的排队功能场景 九
  • (solr系列:一)使用tomcat部署solr服务
  • (排序详解之 堆排序)
  • (十二)python网络爬虫(理论+实战)——实战:使用BeautfulSoup解析baidu热搜新闻数据
  • (算法)Travel Information Center
  • (一)Dubbo快速入门、介绍、使用
  • (轉貼) 2008 Altera 亞洲創新大賽 台灣學生成果傲視全球 [照片花絮] (SOC) (News)
  • .net core 微服务_.NET Core 3.0中用 Code-First 方式创建 gRPC 服务与客户端
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .NET/C# 编译期间能确定的相同字符串,在运行期间是相同的实例
  • @select 怎么写存储过程_你知道select语句和update语句分别是怎么执行的吗?
  • [Android] Implementation vs API dependency
  • [C/C++]关于C++11中的std::move和std::forward
  • [ERROR ImagePull]: failed to pull image k8s.gcr.io/kube-controller-manager失败
  • [HackMyVM]靶场 Wild
  • [IE9] GPU硬件加速到底是实用创新还是噱头
  • [JavaScript]_[初级]_[关于forin或for...in循环语句的用法]
  • [jobdu]不用加减乘除做加法
  • [Labtools 27-1429] XML parser encountered a problem in file
  • [LeetCode] 178. 分数排名