C#8.0 可空引用类型

  • A+
所属分类:.NET技术
摘要

我们的项目代码运行时最频繁的错误之一就是 System.NullReferenceException 异常,c#8.0增加的可为空引用类型就是用来帮助开发者降低甚至消除NULL异常。我们需要注意的是可空引用类型是语法级别的功能,也就是代码编写的时候就会受到编程约束,这个与可为空值类型是不一样的。项目支持c#8.0请参见C# 语言版本控制。


介绍

我们的项目代码运行时最频繁的错误之一就是 System.NullReferenceException 异常,c#8.0增加的可为空引用类型就是用来帮助开发者降低甚至消除NULL异常。我们需要注意的是可空引用类型是语法级别的功能,也就是代码编写的时候就会受到编程约束,这个与可为空值类型是不一样的。项目支持c#8.0请参见C# 语言版本控制

目录

在项目中启用可空引用类型支持

<Project Sdk="Microsoft.NET.Sdk.Web">    <PropertyGroup>     <TargetFramework>net6.0</TargetFramework>     <Nullable>enable</Nullable>   </PropertyGroup>      </Project> 

在项目文件中增加<Nullable>enable</Nullable>后,项目代码中的引用类型将被解析拆分为不可空引用类型可空引用类型

将警告提升为异常

可空引用类型功能是以警告的形式出现,并不会干扰项目生成编译,约束力较弱。如果想严格要求自身,那我们可将特定的警告变为异常来提升约束力。

<Project Sdk="Microsoft.NET.Sdk.Web">    <PropertyGroup>     <TargetFramework>net6.0</TargetFramework>     <Nullable>enable</Nullable>   				                <WarningsAsErrors>     	$(WarningsAsErrors);CS8600;CS8601;CS8602;CS8603;CS8604;CS8609;CS8610;CS8614;CS8616;CS8618;CS8619;CS8622;CS8625     </WarningsAsErrors>   </PropertyGroup>      </Project> 

相关技术文档C# 编译器选项 - 错误和警告 | Microsoft DocsNon-nullable references with C# 8 and .NET Core 3.0 · Cezary Piątek Blog (cezarypiatek.github.io),大家在编写代码时遇到Microsoft.CodeAnalysis.CSharp分析器所给的警告代码,都可按照自己的要求将其变为异常来约束自己。

将变量标注为可空引用类型

我们平时使用的引用类型属于不可空引用类型,在其后附加?便为可空引用类型

string name; //不可空字符串 string? adress; //可空字符串 

泛型

public TKey GetKey<TKey>() {     //必须返回不可空类型 }  public TValue? GetValue<TValue>() {     //可返回可空类型 } 

使用示例

C#8.0 可空引用类型

如上示例,由于Student拥有默认的空构造函数new Student(),此构造函数会使NameAdress属性为null,所以分析器发出了CS8618的警告。

C#8.0 可空引用类型

我们将空构造函数写上,此时警告智能的转移到构造函数上了。

C#8.0 可空引用类型

我们在构造函数中将可能为null的string类型属性附上值,警告消除。而string?类型无需处理,因为它是允许为null的。

C#8.0 可空引用类型

C#8.0 可空引用类型

以上两种方式也可以消除警告。

C#8.0 可空引用类型

GetStudentNames方法中,我们使用StudentEnglishName属性时,分析器发出了CS8604警告,因为EnglishName属性是可空引用类型,无法放入List<string>中,只能放入在List<string?>中。

C#8.0 可空引用类型

我们使用??判断当EnglishName为null时,使用不可空引用类型属性Name,此时CS8604警告消除。

进阶

可空引用类型模式中,属性是可以被拆分为两种模式的,其一是属性是否可被赋值null,其二是属性的值是否可能为null。大家可能对这句话理解起来有点懵,请接着看下面的讲解。

[AllowNull]

不可为null的引用类型属性允许被赋值null

C#8.0 可空引用类型

上面代码中,Adress属性即使被赋值null,也不会使其值为null,不会在代码中引发潜在的Null异常。所以此场景是合理且被允许的。

[DisallowNull]

可为null的引用类型属性不允许赋值为null

C#8.0 可空引用类型

Adress属性虽然默认值是null,但对其赋值null是不合理的。虽然不能赋值null,但获取Adress属性的值时仍可能为null,大家可在合适的场景使用[DisallowNull]

[NotNull]

可为null的引用类型属性的值永远不会是null,可放心使用

C#8.0 可空引用类型

C#8.0 可空引用类型

我们使用GetStudentAdress方法返回StudentAdress属性,分析器并没有发出警告,因为分析器通过[NotNull]特性也知道了Adress属性的值永远不会为null。

C#8.0 可空引用类型

我们尝试将Adress属性改为可能返回null值,分析器立马发出了CS8603警告,很给力。

[NotNullIfNotNull]

这个特性作用于方法中,用于告诉其他程序员只要你不给我的方法传null参,我就不会返回null给你,你看着办。

[return: NotNullIfNotNull("student")] public string? GetStudentAdress(Student? student) {     return student?.Adress; } 

C#8.0 可空引用类型

adressadress2有着不同的待遇。

缺陷

有些场景分析器无法分析出潜在的null异常

Struct

public struct Student {     public string FirstName;     public string? MiddleName;     public string LastName; }  public static class Program {     public static void PrintStudent(Student student)     {         Console.WriteLine($"First name: {student.FirstName.ToUpper()}");         Console.WriteLine($"Middle name: {student.MiddleName?.ToUpper()}");         Console.WriteLine($"Last name: {student.LastName.ToUpper()}");     }      public static void Main() => PrintStudent(default(FirstName));     public static void Main2() => PrintStudent(new Student()); } 

default(FirstName)new Student()中的FirstNameLastName 运行时为 null,编辑器此时未出现任何警告。

public struct Foo<T> {     public T Bar { get; set; } }  public static class Program {     public static void Main()     {         string s = default(Foo<string>).Bar;         string s2 = new Foo<string>().Bar;     } } 

属性 Bar 在运行时为 null,而ss2是不可为null字符串类型,编辑器此时未出现任何警告。

数组

数组也是可为 null 的引用类型中的已知缺陷

using System;  public static class Program {     public static void Main()     {         string[] values = new string[10];         string s = values[0];         Console.WriteLine(s.ToUpper());     } } 

代码中的数组声明其元素为不可为null的string,而其元素在初始化时都为null,编辑器此时未出现任何警告。

总结

将引用类型拆分为可空引用类型和不可空引用类型可以为我们的项目代码带来质的提升,团队之间协作或者使用第三方的类库都可以通过?标识来知道方法的某个参数传null不会引发异常、属性赋值null不会引发异常,反之我们使用某些属性或者方法的返参也可以知道其是否可能为null,对于不可能为null的变量我们就无需再麻烦的检测null值了,而在以前,我们可能需要对每个变量都需要做null判断。感兴趣的同学赶紧给自己的项目加入这个功能吧。

我们正在行动,新的框架、新的生态

我们的目标是自由的易用的可塑性强的功能丰富的健壮的

所以我们借鉴Building blocks的设计理念,正在做一个新的框架MASA Framework,它有哪些特点呢?

  • 原生支持Dapr,且允许将Dapr替换成传统通信方式
  • 架构不限,单体应用、SOA、微服务都支持
  • 支持.Net原生框架,降低学习负担,除特定领域必须引入的概念,坚持不造新轮子
  • 丰富的生态支持,除了框架以外还有组件库、权限中心、配置中心、故障排查中心、报警中心等一系列产品
  • 核心代码库的单元测试覆盖率90%+
  • 开源、免费、社区驱动
  • 还有什么?我们在等你,一起来讨论

经过几个月的生产项目实践,已完成POC,目前正在把之前的积累重构到新的开源项目中

目前源码已开始同步到Github(文档站点在规划中,会慢慢完善起来):

MASA.BuildingBlocks

MASA.Contrib

MASA.Utils

MASA.EShop

BlazorComponent

MASA.Blazor

QQ群:7424099

微信群:加技术运营微信(MasaStackTechOps),备注来意,邀请进群

C#8.0 可空引用类型

​ ------ END ------

作者简介

吴炜来:MASA技术团队成员。