内容简介:这篇文章将介绍将 C# 7 类库升级到 C# 8(支持可空引用类型)的一个案例。本案例中使用的项目 Tortuga Anchor 由一组 MVVM 风格的基类、反射代码和各种实用程序函数组成。之所以选择这个项目,是因为它很小,并且同时包含了惯用和不常用的 C# 模式。这篇文章将介绍将 C# 7 类库升级到 C# 8(支持可空引用类型)的一个案例。本案例中使用的项目目前,
这篇文章将介绍将 C# 7 类库升级到 C# 8(支持可空引用类型)的一个案例。本案例中使用的项目 Tortuga Anchor 由一组 MVVM 风格的基类、反射代码和各种实用程序函数组成。之所以选择这个项目,是因为它很小,并且同时包含了惯用和不常用的 C# 模式。
关键要点
- 为每个项目启用可空引用类型。
- 使用泛型时,可能需要禁用可空引用类型。
- 可以通过在本地变量中缓存属性来修复警告。
- 公开方法仍然需要进行 Null 参数检查。
- .NET Framework 和.NET Core 的反序列化方式是不一样的。
这篇文章将介绍将 C# 7 类库升级到 C# 8(支持可空引用类型)的一个案例。本案例中使用的项目 Tortuga Anchor 由一组 MVVM 风格的基类、反射代码和各种实用程序函数组成。之所以选择这个项目,是因为它很小,并且同时包含了惯用和不常用的 C# 模式。
项目设置
目前, 可空引用类型仅适用于.NET Standard 和.NET Core 项目。在 Visual Studio 2019 发布时, 应该也支持.NET Framework。
在项目文件中,添加或修改以下配置:
复制代码
</PropertyGroup> <LangVersion>8.0</LangVersion> <NullableContextOptions>enable</NullableContextOptions> </PropertyGroup>
在保存文件后,应该会看到可空性错误。如果没有看到,请尝试构建项目。
指示一个类型可以为空
在接口方法 GetPreviousValue 中,返回类型可以为空。为了显式地说明这一点,可以在 object 后面跟上可空类型修饰符(?)。
复制代码
object?GetPreviousValue(stringpropertyName);
使用这个类型修饰符注解变量、参数和返回类型,就可以解决项目中的很多编译器错误。
延迟加载属性
如果一个属性的求值成本非常高,可以使用延迟加载模式。在使用这个模式时,如果私有字段为空,表示尚未生成字段的值。
C# 8 可以很好地处理这种情况。在不改变代码的情况下,它能够正确地分析代码,以确定 getter 的结果将始终非空,尽管返回的变量可以为空。
复制代码
string? m_CSharpFullName; publicstringCSharpFullName { get { if(m_CSharpFullName ==null) { varresult =newStringBuilder(m_TypeInfo.ToString().Length); BuildCSharpFullName(m_TypeInfo.AsType(),null, result); m_CSharpFullName = result.ToString(); } returnm_CSharpFullName; } }
需要注意的是,这里存在潜在的竞态条件。理论上,另一个线程可以将 m_CSharpFullName 的值设置回 null,而编译器无法检测到。因此,在处理多线程代码时要特别小心。
一个变量的可空性由另一个变量决定
在下一个代码示例中,当且仅当 m_ItemPropertyChanged 不为空时,m_ListeningToItemEvents 才为 true。编译器无法知道这个规则。如果是这种情况,你可以将(!)附加到变量(在本例中为 m_ItemPropertyChanged)后面,表示它在这个时间点不会为空。
复制代码
if(m_ListeningToItemEvents) { if(itemisINotifyPropertyChangedWeak) ((INotifyPropertyChangedWeak)item).AddHandler(m_ItemPropertyChanged!); elseif(itemisINotifyPropertyChanged) ((INotifyPropertyChanged)item).PropertyChanged += OnItemPropertyChanged; }
使用显式强制转换纠正误报
在下一个示例中,编译器错误地报告了 m_Base 的可空性。Values 与 IEnumerable
复制代码
readonly Dictionary<ValueTuple<TKey1, TKey2>, TValue> m_Base; IEnumerable<TValue>IReadOnlyDictionary<ValueTuple<TKey1, TKey2>, TValue>.Values { get{ return (IEnumerable<TValue>)m_Base.Values; } }
请注意编译器将该行标记为具有冗余强制转换。这是正常的编译器消息,而不是警告,但希望在发布时能够得到更正。
使用临时变量或条件强制转换纠正误报
在下一个示例中,编译器指出 CancelEdit 所在行存在一个错误。虽然前面的 if 语句证明 item.Value 不为空,但编译器不相信下次读取 item.Value 时它仍然是不为空。
复制代码
foreach (variteminm_CheckpointValues) { if(item.ValueisIEditableObject) ((IEditableObject)item.Value).CancelEdit(); }
我们可以将 item.Value 保存在一个临时变量中。
复制代码
foreach(variteminm_CheckpointValues) { object?value= item.Value; if(valueisIEditableObject) ((IEditableObject)value).CancelEdit(); }
对于这种情况,我们可以通过使用条件转换(as 操作符)后面跟上一个条件方法调用(?. 操作符)进一步简化它。
复制代码
foreach (variteminm_CheckpointValues) { (item.ValueasIEditableObject)?.CancelEdit(); }
泛型和可空类型
如果你经常使用泛型,可能会遇到一个有问题的可空类型。看一下这个 delegate:
复制代码
publicdelegatevoidValueChanged<inT>(T oldValue, T newValue);
这个 delegate 的预期设计是 oldValue 和 newValue 都可以为空。所以,你会认为加几个问号就可以解决问题。但是,这样做会返回下面这样的错误消息:
Error CS8627 可空类型参数必须是值类型或非可空的引用类型。可以考虑添加“class”、“struct”或类型约束。
如果你需要同时支持值类型和引用类型,那么这个问题就没那么容易解决。由于你无法在类型约束中表达“or”,你需要一个用于类的 delegate 和一个用于结构体的 delegate。
复制代码
publicdelegatevoidValueChanged<inT>(T? oldValue, T? newValue)whereT :class; publicdelegatevoidValueChanged<T>(T? oldValue, T? newValue)whereT :struct;
但是,这样不起作用,因为两个 delegate 具有相同的名称。你可以给它们起不一样的名称,但你必须复制使用它们的代码。
所幸的是,C# 有一个转义值。你可以使用 #nullable 指令恢复成 C #7 的语义,这样就可以达到预期的效果。
复制代码
#nullable disable publicdelegatevoidValueChanged<in T>(T oldValue, T newValue); #nullable enable
这种方法并非没有缺陷。禁用可空引用可能是个好东西,但也可能什么都不是。你无法用它来让 oldValue 变成可空或让 newValue 变成不可空。
构造函数、反序列化器和初始化方法
对于下一个示例,你必须知道序列化器的一些技巧。有一个鲜为人知的函数用来绕过一个叫作 FormatterServices.GetUninitializedObject 的类构造函数。一些序列化器(如 DataContractSerializer)使用它来提高性能。
如果你总是要运行构造函数中的逻辑,应该怎么办?这个时候需要用到 OnDeserializing 属性。这个属性充当在 GetUninitializedObject 之后调用的代理构造函数。
为了减少冗余和出错的可能性,开发人员通常会使用常见的初始化方法,如下面的代码所示。
复制代码
protectedAbstractModelBase() { Initialize(); } [OnDeserializing] void_ModelBase_OnDeserializing(StreamingContextcontext) { Initialize(); } voidInitialize() { m_PropertyChangedEventManager =newPropertyChangedEventManager(this); m_Errors =newErrorsDictionary(); }
这对 null 检查器来说是个问题。由于构造函数中没有显式地设置上述两个变量,因此它会把它们标记为未初始化。这意味着需要进行一些复制粘贴工作来移除这个错误。
还有一个风险,那就是忘记包含 OnDeserializing 方法。由于 null 检查器不理解 OnDeserializing 方法,因此如果出现意外空值就无法提醒你。
大多数开发人员发现这种行为令人困惑。因此, 在.NET Core 中,DataContractSerializer 将调用构造函数。 但这意味着如果你的目标是.NET Standard, 则需要使用.NET Framework 和.NET Core 测试反序列化代码,以理解不同的行为。
可空参数和 CallerMemberName
这个库大量使用了 CallerMemberName 模式。根据它使用的属性命名,基本思想是在方法的末尾添加一个可选参数。编译器将看到 CallerMemberName,并隐式地为该参数提供一个值。
复制代码
publicoverrideboolIsDefined([CallerMemberName]stringpropertyName =null)
从理论上讲,propertyNameparameter 可以显式设置为 null,但人们普遍认为不应该这样做,因为这样可能会发生意外的错误。
将这行代码转换为 C# 8 时,可能会想要将参数标记为可空。这样具有误导性,因为这个方法实际上并不是为处理空值而设计的。相反,你应该用空字符串替换 null。
复制代码
publicoverrideboolIsDefined([CallerMemberName]stringpropertyName ="")
还需要空参数检查吗?
如果要构建公共库(即 NuGet),那么是的,所有公开方法仍然需要检查空参数。使用库的应用程序可能不一定会使用可空引用类型。事实上,他们甚至可能根本不使用 C# 8。
如果你的所有应用程序代码都使用了可空引用类型,那么答案仍然是“可能是”。虽然从理论上讲,你不会看到任何意外的空值,但由于动态代码、反射或误用(!)操作符,它们仍然可能会出现。
结论
在一个只有不到 60 个类文件的项目中,其中 24 个类文件需要更改。但没有一个是特别重要的,整个过程花了不到一个小时。总之,这是一个无痛的过程,大多数事情都像预期的那样。我希望大多数项目都能从这个特性中获益,并且在 C# 8 发布后就应该使用这个特性。
关于作者
Jonathan Allen在 90 年代后期开始为一家医疗诊所做 MIS 项目,逐步将 Access 和 Excel 应用到企业解决方案中。在花了五年时间为金融行业编写自动化交易系统之后,他成为了多个项目的顾问,其中包括机器人仓库的 UI、癌症研究软件的中间层,以及一家大型房地产保险公司对大数据的需求。在他的空闲时间,他喜欢学习和写作与 16 世纪武术相关的东西。
英文原文: https://www.infoq.com/articles/csharp-nullable-reference-case-study
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- c# – 值类型是否包含引用类型?
- .NET 中的值类型与引用类型
- C#中的值类型和引用类型
- 智能合约基础语言(五):Solidity变量类型:引用类型
- 《JavaScript面向对象精要》之一:基本类型和引用类型
- golang的值类型,指针类型和引用类型&值传递&指针传递
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
About Face 3
Alan Cooper、Robert Reimann、David Cronin / John Wiley & Sons / 2007-5-15 / GBP 28.99
* The return of the authoritative bestseller includes all new content relevant to the popularization of how About Face maintains its relevance to new Web technologies such as AJAX and mobile platforms......一起来看看 《About Face 3》 这本书的介绍吧!