我最近看过以下代码:
public class Person
{
//line 1
public string FirstName { get; }
//line 2
public string LastName { get; } = null!;
//assign null is possible
public string? MiddleName {get; } = null;
public Person(string firstName, string lastName, string middleName)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName;
}
public Person(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
MiddleName = null;
}
}
基本上,我尝试挖掘c#8的新功能。其中之一是NullableReferenceTypes
。实际上,已经有很多文章和信息。例如。 this article很好。
但是我找不到有关此新语句null!
的任何信息
有人可以为我提供解释吗?为什么我需要使用这个?
line1
和line2
有什么区别?
答案 0 :(得分:54)
了解null!
含义的关键是了解!
运算符。您之前可能已将其用作“非”运算符。但是从C#8.0开始,该运算符还可以用于控制Nullabilty的类型
!
运算符是什么?将!
运算符用于一种类型时,称为Null Forgiving Operator [docs]。它是在C#8.0中引入的
假设此定义:
class Person
{
public string? MiddleName;
}
用法为:
void LogPerson(Person person)
{
Console.WriteLine(person.MiddleName.Length); // WARNING: may be null
Console.WriteLine(person.MiddleName!.Length); // No warning
}
此运算符基本上会关闭编译器的空检查。
使用此运算符告诉编译器可以安全地访问的内容可以为空。您表示打算在这种情况下“不关心”空安全性。
在谈论null安全性时,变量可以处于2种状态。
自C#8.0起,默认情况下所有引用类型都是不可为空的。
可以通过以下两个新的类型操作符来修改“可空性”:
!
=从Nullable
到Non-Nullable
?
=从Non-Nullable
到Nullable
这些运算符基本上是彼此对应的。 编译器使用您使用这些运算符定义的信息来确保null安全。
?
操作员用法。可为空的string? x;
x
是引用类型-因此默认情况下不可为空。?
运算符-使其可以为空。x = null
工作正常。不可空string y;
y
是引用类型-因此默认情况下不可为空。y = null
生成警告,因为您为不应该为null的对象分配了null值。!
操作员用法。string x;
string? y = null;
x = y
Warning: "y" may be null
x = y!
y!
开始工作,将!
运算符应用于y
,这使其不可为空。警告
!
运算符仅在类型系统级别关闭编译器检查-在运行时,该值仍可能为null。
您应该尝试尝试避免使用!
Null-Forgiving-Operator。
有一些有效的用例(下面将详细介绍),例如适合使用此运算符的单元测试。但是,在99%的情况下,使用替代解决方案会更好。请不要在您的代码中拍打数十个!
,只是为了使警告静音。考虑一下您的原因是否真的值得使用。
使用-但要小心。如果没有具体目的/用例,请不要使用它。
它抵消了编译器保证的空安全性的影响。
使用!
运算符将导致很难发现错误。如果您有一个标记为不可为空的属性,则将假定您可以安全地使用它。但是在运行时,您突然遇到NullReferenceException
并挠头。由于使用!
绕过编译器检查后,值实际上变为空。
null
通过时的代码行为。null!
是什么意思?它告诉编译器null
不是null
值。听起来很奇怪,不是吗?
与上面示例中的y!
相同。 这看起来很奇怪,因为您将运算符应用于null
文字。但是概念是相同的。
public string LastName { get; } = null!;
此行定义类型为LastName
的名称为string
的不可为空的类属性。
由于它不可为空,因此从技术上讲,您不能为它分配null。
但是您只是这样做-通过使用null
运算符将LastName
分配给!
。因为null!
不为null-就编译器所关心的null安全而言。
答案 1 :(得分:9)
打开“可空引用类型”功能时,编译器会跟踪它认为代码中哪些值可能为null或不是null。有时候编译器可能没有足够的知识。
例如,您可能正在使用延迟初始化模式,在该模式下,构造函数不会使用实际(非空)值初始化所有字段,但是您始终调用一种初始化方法来确保这些字段为非空值。在这种情况下,您将面临一个权衡:
null!
禁止它),那么该字段可以不进行空检查而使用。请注意,使用!
抑制运算符会带来一些风险。想象一下,您实际上并未像您想象的那样一致地初始化所有字段。然后使用null!
初始化字段掩盖了null
进入的事实。一些毫无疑问的代码可以收到null
并因此失败。
更一般而言,您可能具有一些领域知识:“如果我检查了某种方法,那么我知道某些值不为空”:
if (CheckEverythingIsReady())
{
// you know that `field` is non-null, but the compiler doesn't. The suppression can help
UseNonNullValueFromField(this.field!);
}
同样,您必须对代码的不变性充满信心(“我知道得更好”)。
答案 2 :(得分:1)
null!
用于为不可为空的变量赋值为空,这是一种保证变量在实际使用时不会为null
的方式。
我使用 null!
和 Entity Framework Core 来标记通过反射初始化的变量:
public class MyDbContext : DbContext
{
public DbSet<Entity> Entities { get; set; } = null!; // Assigned by EF Core
public DbSet<Person> People { get; set; } = null!; // Assigned by EF Core
}
我还在单元测试中使用它来标记由 setup 方法初始化的变量:
public class MyUnitTests
{
IDatabaseRepository _repo = null!;
[OneTimeSetUp]
public void PrepareTestDatabase()
{
...
_repo = ...
...
}
}
如果您在这种情况下不使用 null!
,则每次读取变量时都必须使用感叹号,这会很麻烦而且没有好处。
答案 3 :(得分:-1)
简而言之,我们在编译器建议存在空变量的任何地方使用它,但我们确定或应该假设它不是。
例如:
if (".js".Equals(Path.GetExtension(@"D:\file.js")))
{
var directory = Path.GetDirectoryName(includeFile);
var map = Path.Combine(
directory, // Compiler: Possible 'null' assignment to nun-nullable entity
"file.map");
}
纠正方法如下:
if (".js".Equals(Path.GetExtension(@"D:\file.js")))
{
var directory = Path.GetDirectoryName(includeFile);
var map = Path.Combine(
directory!, // The warning is ignored because we are sure the directory variable is not null
"file.map");
}
还有其他情况,例如在单元测试中我们故意向不接受空参数的函数发送空值
例如:
public class Person
{
public Person(string name) => Name = name ?? throw new ArgumentNullException(nameof(name));
public string Name { get; }
}
我们测试:
[TestMethod, ExpectedException(typeof(ArgumentNullException))]
public void NullNameShouldThrowTest()
{
var person = new Person(null!);
}