C#8.0 可空引用类型

C#8.0 可空引用类型

介绍

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

目录

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

1
2
3
4
5
6
7
8
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>

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

将警告提升为异常

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

1
2
3
4
5
6
7
8
9
10
11
<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 分析器所给的警告代码,都可按照自己的要求将其变为异常来约束自己。

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

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

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

泛型

1
2
3
4
5
6
7
8
9
public TKey GetKey<TKey>()
{
//必须返回不可空类型
}

public TValue? GetValue<TValue>()
{
//可返回可空类型
}

使用示例

image-20211114144233735.png

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

image-20211114144541808.png

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

image-20211114145041104.png

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

image-20211114145405887.png

image-20211114145525241.png

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

image-20211114150135945.png

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

image-20211114150426034.png

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

进阶

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

[AllowNull]

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

image-20211114152033299.png

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

[DisallowNull]

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

image-20211114152750169.png

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

[NotNull]

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

image-20211114153459266.png

image-20211114153655036.png

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

image-20211114154005789.png

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

[NotNullIfNotNull]

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

1
2
3
4
5
[return: NotNullIfNotNull("student")]
public string? GetStudentAdress(Student? student)
{
return student?.Adress;
}

image-20211114154858318.png

adressadress2有着不同的待遇。

缺陷

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

Struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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,编辑器此时未出现任何警告。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的引用类型中的已知缺陷

1
2
3
4
5
6
7
8
9
10
11
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 判断。感兴趣的同学赶紧给自己的项目加入这个功能吧。

作者

MASA

发布于

2021-11-30

更新于

2022-08-04

许可协议