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

C# 8.0 的可空引用类型,不止是加个问号哦!你还有很多种不同的可空玩法

C# 8.0 引入了可空引用类型,你可以通过 ? 为字段、属性、方法参数、返回值等添加是否可为 null 的特性。

但是如果你真的在把你原有的旧项目迁移到可空类型的时候,你就会发现情况远比你想象当中复杂,因为你写的代码可能只在部分情况下可空,部分情况下不可空;或者传入空时才可为空,传入非空时则不可为空。


本文内容

    • C# 8.0 可空特性
    • 更灵活控制的可空特性
      • 输入:`AllowNull`
      • 输入:`DisallowNull`
      • 输出:`MaybeNull`
      • 输出:`NotNull`
      • `NotNullWhen`, `MaybeNullWhen`
      • `NotNullIfNotNull`
    • 在早期 .NET Framework 或者早期版本的 .NET Core 中使用
    • Walterlv.NullableAttributes

C# 8.0 可空特性

在开始迁移你的项目之前,你可能需要了解如何开启项目的可空类型支持:

  • C# 8.0 如何在项目中开启可空引用类型的支持 - walterlv

可空引用类型是 C# 8.0 带来的新特性。

你可能会好奇,C# 语言的可空特性为什么在编译成类库之后,依然可以被引用它的程序集识别。也许你可以理解为有什么特性 Attribute 标记了字段、属性、方法参数、返回值的可空特性,于是可空特性就被编译到程序集中了。

确实,可空特性是通过 NullableAttributeNullableContextAttribute 这两个特性标记的。

但你是否好奇,即使在古老的 .NET Framework 4.5 或者 .NET Standard 2.0 中开发的时候,你也可以编译出支持可空信息的程序集出来。这些古老的框架中没有这些新出来的类型,为什么也可以携带类型的可空特性呢?

实际上反编译一下编译出来的程序集就能立刻看到结果了。

看下图,在早期版本的 .NET 框架中,可空特性实际上是被编译到程序集里面,作为 internalAttribute 类型了。
反编译

所以,放心使用可空类型吧!旧版本的框架也是可以用的。

更灵活控制的可空特性

阻碍你将老项目迁移到可空类型的原因,可能还有你原来代码逻辑的问题。因为有些情况下你无法完完全全将类型迁移到可空。

例如:

  1. 有些时候你不得不为非空的类型赋值为 null 或者获取可空类型时你能确保此时一定不为 null(待会儿我会解释到底是什么情况);
  2. 一个方法,可能这种情况下返回的是 null 那种情况下返回的是非 null
  3. 可能调用者传入 null 的时候才返回 null,传入非 null 的时候返回非 null

为了解决这些情况,C# 8.0 还同时引入了下面这些 Attribute

  • AllowNull: 标记一个不可空的输入实际上是可以传入 null 的。
  • DisallowNull: 标记一个可空的输入实际上不应该传入 null。
  • MaybeNull: 标记一个非空的返回值实际上可能会返回 null,返回值包括输出参数。
  • NotNull: 标记一个可空的返回值实际上是不可能为 null 的。
  • MaybeNullWhen: 当返回指定的 true/false 时某个输出参数才可能为 null,而返回相反的值时那个输出参数则不可为 null。
  • NotNullWhen: 当返回指定的 true/false 时,某个输出参数不可为 null,而返回相反的值时那个输出参数则可能为 null。
  • NotNullIfNotNull: 指定的参数传入 null 时才可能返回 null,指定的参数传入非 null 时就不可能返回 null。
  • DoesNotReturn: 指定一个方法是不可能返回的。
  • DoesNotReturnIf: 在方法的输入参数上指定一个条件,当这个参数传入了指定的 true/false 时方法不可能返回。

想必有了这些描述后,你在具体遇到问题的时候应该能知道选用那个特性。但单单看到这些特性的时候你可能不一定知道什么情况下会用得着,于是我可以为你举一些典型的例子。

输入:AllowNull

设想一下你需要写一个属性:

public string Text
{
    get => GetValue() ?? "";
    set => SetValue(value ?? "");
}

当你获取这个属性的值的时候,你一定不会获取到 null,因为我们在 get 里面指定了非 null 的默认值。然而我是允许你设置 null 到这个属性的,因为我处理好了 null 的情况。

于是,请为这个属性加上 AllowNull。这样,获取此属性的时候会得到非 null 的值,而设置的时候却可以设置成 null

++  [AllowNull]
    public string Text
    {
        get => GetValue() ?? "";
        set => SetValue(value ?? "");
    }

输入:DisallowNull

与以上场景相反的一个场景:

private string? _text;

public string? Text
{
    get => _text;
    set => _text = value ?? throw new ArgumentNullException(nameof(value), "不允许将这个值设置为 null");
}

当你获取这个属性的时候,这个属性可能还没有初始化,于是我们获取到 null。然而我却并不允许你将这个属性赋值为 null,因为这是个不合理的值。

于是,请为这个属性加上 DisallowNull。这样,获取此属性的时候会得到可能为 null 的值,而设置的时候却不允许为 null

输出:MaybeNull

如果你有尝试过迁移代码到可空类型,基本上一定会遇到泛型方法的迁移问题:

public T Find<T>(int index)
{
}

比如以上这个方法,找到了就返回找到的值,找不到就返回 T 的默认值。那么问题来了,T 没有指定这是值类型还是引用类型。

如果 T 是引用类型,那么默认值 default(T) 就会引入 null。但是泛型 T 并没有写成 T?,因此它是不可为 null 的。然而值类型和引用类型的 T? 代表的是不同的含义。这种矛盾应该怎么办?

这个时候,请给返回值标记 MaybeNull

++  [return: MaybeNull]
    public T Find<T>(int index)
    {
    }

这表示此方法应该返回一个不可为 null 的类型,但在某些情况下可能会返回 null

实际上这样的写法并没有从本质上解决掉泛型 T 的问题,不过可以用来给旧项目迁移时用来兼容 API 使用。

如果你可以不用考虑 API 的兼容性,那么可以使用新的泛型契约 where T : notnull

public T Find<T>(int index) where T : notnull
{
}

输出:NotNull

设想你有一个方法,方法参数是可以传入 null 的:

public void EnsureInitialized(ref string? text)
{
}

然而这个方法的语义是确保此字段初始化。于是可以传入 null 但不会返回 null 的。这个时候请标记 NotNull

--  public void EnsureInitialized(ref string? text)
++  public void EnsureInitialized([NotNull] ref string? text)
    {
    }

NotNullWhen, MaybeNullWhen

string.IsNullOrEmpty 的实现就使用到了 NotNullWhen

bool IsNullOrEmpty([NotNullWhen(false)] string? value);

它表示当返回 false 的时候,value 参数是不可为 null 的。

这样,你在这个方法返回的 false 判断分支里面,是不需要对变量进行判空的。

当然,更典型的还有 TryDo 模式。比如下面是 Version 类的 TryParse

bool TryParse(string? input, [NotNullWhen(true)] out Version? result)

当返回 true 的时候,result 一定不为 null

NotNullIfNotNull

典型的情况比如指定默认值:

[return: NotNullIfNotNull("defaultValue")]
public string? GetValue(string key, string? defaultValue)
{
}

这段代码里面,如果指定的默认值(defaultValue)是 null 那么返回值也就是 null;而如果指定的默认值是非 null,那么返回值也就不可为 null 了。

在早期 .NET Framework 或者早期版本的 .NET Core 中使用

在本文第一小节里面,我们说 Nullable 是编译到目标程序集中的,所以不需要引用什么特别的程序集就能够使用到可空引用的特性。

那么上面这些特性呢?它们并没有编译到目标程序集中怎么办?

实际上,你只需要有一个命名空间、名字和实现都相同的类型就够了。你可以写一个放到你自己的程序集中,也可以把这些类型写到一个自己公共的库中,然后引用它。当然,你也可以用我已经写好的 NuGet 包 Walterlv.NullableAttributes。

Walterlv.NullableAttributes

微软 .NET 官方的可空特性在这里:

  • NullableAttributes.cs

我将其注释翻译成中文之后,也写了一份在这里:

  • Walterlv.Packages/CodeAnalysis.cs at master · walterlv/Walterlv.Packages

如果你想简单一点,可以直接引用我的 NuGet 包:

  • 作为 dll 引用:NuGet Gallery - Walterlv.NullableAttributes
  • 作为源代码包引用:NuGet Gallery - Walterlv.NullableAttributes.Source

源代码包可以在不用引入其他 dll 依赖的情况下完成引用。最终你输出的程序集是不带对此包的依赖的,详见:

  • .NET 将多个程序集合并成单一程序集的 4+3 种方法 - walterlv

参考资料

  • Upgrade APIs for nullable reference types with attributes that define expectations for null values - Microsoft Docs

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

相关文章:

  • 一个简单的方法:截取子类名称中不包含基类后缀的部分
  • 使用 MSBuild Target 复制文件的时候如何保持文件夹结构不变
  • 如何在 MSBuild 中正确使用 % 来引用每一个项(Item)中的元数据
  • 如何将一个 .NET 对象序列化为 HTTP GET 的请求字符串
  • 屏幕边缘上有趣的 1 个像素,看不见、摸不到
  • 在 MSBuild 编译过程中操作文件和文件夹(检查存在/创建文件夹/读写文件/移动文件/复制文件/删除文件夹)
  • 在 WPF 程序中应用 Windows 10 真•亚克力效果
  • 推荐 .NET/C# 开发者安装的几款代码分析插件或对应的代码分析 NuGet 包
  • 在 HTML 超链接上添加可交互的 ToolTip
  • 在移动端打开 Google 的网页快照
  • 为自己搭建的博客添加可切换的暗色和亮色主题
  • 如何编写基于 Microsoft.NET.Sdk 的跨平台的 MSBuild Target(附各种自带的 Task)
  • 让你编写的控件库在 XAML 中有一个统一的漂亮的命名空间(xmlns)和命名空间前缀
  • Sdk 风格的 csproj 对 WPF/UWP 支持不太好?有第三方 SDK 可以用!MSBuild.Sdk.Extras
  • 为博客或个人站点的 Markdown 添加 LaTeX 公式支持
  • Java 23种设计模式 之单例模式 7种实现方式
  • Javascript 原型链
  • Making An Indicator With Pure CSS
  • MYSQL如何对数据进行自动化升级--以如果某数据表存在并且某字段不存在时则执行更新操作为例...
  • npx命令介绍
  • scrapy学习之路4(itemloder的使用)
  • socket.io+express实现聊天室的思考(三)
  • SwizzleMethod 黑魔法
  • unity如何实现一个固定宽度的orthagraphic相机
  • vue-cli在webpack的配置文件探究
  • 码农张的Bug人生 - 见面之礼
  • 思考 CSS 架构
  • 小而合理的前端理论:rscss和rsjs
  • 验证码识别技术——15分钟带你突破各种复杂不定长验证码
  • 国内开源镜像站点
  • 如何用纯 CSS 创作一个菱形 loader 动画
  • #android不同版本废弃api,新api。
  • #图像处理
  • (iPhone/iPad开发)在UIWebView中自定义菜单栏
  • (Matalb回归预测)PSO-BP粒子群算法优化BP神经网络的多维回归预测
  • (MATLAB)第五章-矩阵运算
  • (webRTC、RecordRTC):navigator.mediaDevices undefined
  • (读书笔记)Javascript高级程序设计---ECMAScript基础
  • (附源码)springboot 智能停车场系统 毕业设计065415
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (实战篇)如何缓存数据
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • (转载)(官方)UE4--图像编程----着色器开发
  • (转载)虚函数剖析
  • .NET MVC之AOP
  • .NET Remoting Basic(10)-创建不同宿主的客户端与服务器端
  • .Net的C#语言取月份数值对应的MonthName值
  • .NET开源全面方便的第三方登录组件集合 - MrHuo.OAuth
  • .one4-V-XXXXXXXX勒索病毒数据怎么处理|数据解密恢复
  • .考试倒计时43天!来提分啦!
  • [23] GaussianAvatars: Photorealistic Head Avatars with Rigged 3D Gaussians
  • [AIGC] Kong:一个强大的 API 网关和服务平台
  • [ai笔记3] ai春晚观后感-谈谈ai与艺术
  • [bzoj1006]: [HNOI2008]神奇的国度(最大势算法)
  • [C#]winform部署yolov5-onnx模型