写点什么

如何将 C# 7 类库升级到 C# 8?使用可空引用类型

  • 2019-03-02
  • 本文字数:3838 字

    阅读完需:约 13 分钟

如何将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(string propertyName);
复制代码


使用这个类型修饰符注解变量、参数和返回类型,就可以解决项目中的很多编译器错误。

延迟加载属性

如果一个属性的求值成本非常高,可以使用延迟加载模式。在使用这个模式时,如果私有字段为空,表示尚未生成字段的值。


C# 8 可以很好地处理这种情况。在不改变代码的情况下,它能够正确地分析代码,以确定 getter 的结果将始终非空,尽管返回的变量可以为空。


string? m_CSharpFullName;public string CSharpFullName{    get    {        if (m_CSharpFullName == null)        {            var result = new StringBuilder(m_TypeInfo.ToString().Length);            BuildCSharpFullName(m_TypeInfo.AsType(), null, result);            m_CSharpFullName = result.ToString();        }        return m_CSharpFullName;    }}
复制代码


需要注意的是,这里存在潜在的竞态条件。理论上,另一个线程可以将 m_CSharpFullName 的值设置回 null,而编译器无法检测到。因此,在处理多线程代码时要特别小心。

一个变量的可空性由另一个变量决定

在下一个代码示例中,当且仅当 m_ItemPropertyChanged 不为空时,m_ListeningToItemEvents 才为 true。编译器无法知道这个规则。如果是这种情况,你可以将(!)附加到变量(在本例中为 m_ItemPropertyChanged)后面,表示它在这个时间点不会为空。


if (m_ListeningToItemEvents){    if (item is INotifyPropertyChangedWeak)        ((INotifyPropertyChangedWeak)item).AddHandler(m_ItemPropertyChanged!);    else if (item is INotifyPropertyChanged)        ((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 (var item in m_CheckpointValues){    if (item.Value is IEditableObject)        ((IEditableObject)item.Value).CancelEdit();}
复制代码


我们可以将 item.Value 保存在一个临时变量中。


foreach (var item in m_CheckpointValues){    object? value = item.Value;    if (value is IEditableObject)        ((IEditableObject)value).CancelEdit();}
复制代码


对于这种情况,我们可以通过使用条件转换(as 操作符)后面跟上一个条件方法调用(?.操作符)进一步简化它。


foreach (var item in m_CheckpointValues){    (item.Value as IEditableObject)?.CancelEdit();}
复制代码

泛型和可空类型

如果你经常使用泛型,可能会遇到一个有问题的可空类型。看一下这个 delegate:


public delegate void ValueChanged<in T>(T oldValue, T newValue);
复制代码


这个 delegate 的预期设计是 oldValue 和 newValue 都可以为空。所以,你会认为加几个问号就可以解决问题。但是,这样做会返回下面这样的错误消息:


Error CS8627 可空类型参数必须是值类型或非可空的引用类型。可以考虑添加“class”、“struct”或类型约束。


如果你需要同时支持值类型和引用类型,那么这个问题就没那么容易解决。由于你无法在类型约束中表达“or”,你需要一个用于类的 delegate 和一个用于结构体的 delegate。


public delegate void ValueChanged<in T>(T? oldValue, T? newValue) where T : class;public delegate void ValueChanged<T>(T? oldValue, T? newValue) where T : struct;
复制代码


但是,这样不起作用,因为两个 delegate 具有相同的名称。你可以给它们起不一样的名称,但你必须复制使用它们的代码。


所幸的是,C#有一个转义值。你可以使用 #nullable 指令恢复成 C #7 的语义,这样就可以达到预期的效果。


#nullable disablepublic delegate void ValueChanged<in T>(T oldValue, T newValue);#nullable enable
复制代码


这种方法并非没有缺陷。禁用可空引用可能是个好东西,但也可能什么都不是。你无法用它来让 oldValue 变成可空或让 newValue 变成不可空。

构造函数、反序列化器和初始化方法

对于下一个示例,你必须知道序列化器的一些技巧。有一个鲜为人知的函数用来绕过一个叫作 FormatterServices.GetUninitializedObject 的类构造函数。一些序列化器(如 DataContractSerializer)使用它来提高性能。


如果你总是要运行构造函数中的逻辑,应该怎么办?这个时候需要用到 OnDeserializing 属性。这个属性充当在 GetUninitializedObject 之后调用的代理构造函数。


为了减少冗余和出错的可能性,开发人员通常会使用常见的初始化方法,如下面的代码所示。


protected AbstractModelBase(){    Initialize();} [OnDeserializing]void _ModelBase_OnDeserializing(StreamingContext context){    Initialize();}void Initialize(){    m_PropertyChangedEventManager = new PropertyChangedEventManager(this);    m_Errors = new ErrorsDictionary();}
复制代码


这对 null 检查器来说是个问题。由于构造函数中没有显式地设置上述两个变量,因此它会把它们标记为未初始化。这意味着需要进行一些复制粘贴工作来移除这个错误。


还有一个风险,那就是忘记包含 OnDeserializing 方法。由于 null 检查器不理解 OnDeserializing 方法,因此如果出现意外空值就无法提醒你。


大多数开发人员发现这种行为令人困惑。因此,在.NET Core 中,DataContractSerializer 将调用构造函数。但这意味着如果你的目标是.NET Standard,则需要使用.NET Framework和.NET Core 测试反序列化代码,以理解不同的行为。

可空参数和 CallerMemberName

这个库大量使用了 CallerMemberName 模式。根据它使用的属性命名,基本思想是在方法的末尾添加一个可选参数。编译器将看到 CallerMemberName,并隐式地为该参数提供一个值。


public override bool IsDefined([CallerMemberName] string propertyName = null)
复制代码


从理论上讲,propertyNameparameter 可以显式设置为 null,但人们普遍认为不应该这样做,因为这样可能会发生意外的错误。


将这行代码转换为 C# 8 时,可能会想要将参数标记为可空。这样具有误导性,因为这个方法实际上并不是为处理空值而设计的。相反,你应该用空字符串替换 null。


public override bool IsDefined([CallerMemberName] string propertyName = "")
复制代码

还需要空参数检查吗?

如果要构建公共库(即 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


2019-03-02 11:128622
用户头像

发布了 948 篇内容, 共 266.6 次阅读, 收获喜欢 56 次。

关注

评论

发布
暂无评论
发现更多内容

揭秘攻击者规避XDR检测的惯用手法及应对建议

我再BUG界嘎嘎乱杀

黑客 网络安全 安全 网安 XDR检测

8月浙江省公安厅即将举办省网络与信息安全管理员项目职业技能竞赛

行云管家

网络安全 信息安全

利用 Python 和 IPIDEA:跨境电商与数据采集的完美解决方案

海拥(haiyong.site)

Python

向量检索服务的基本概念

DashVector

向量检索 #数据库 #人工智能 #大模型

向量检索服务应用场景

DashVector

数据库 向量检索 大模型

可观测性建设路线图

FunTester

ETL数据集成丨将DB2数据同步至Postgres数仓实践

RestCloud

数据库 postgresql 数据同步 ETL 数据集成工具

巴赫:阿里AI技术将巴黎奥运转播带到新高度

阿里云CloudImagine

云计算 音视频 视频云 奥运会 AI增强技术

走在市场前沿:用Lazada商品列表数据接口追踪竞争对手

tbapi

lazada商品API接口 lazada商品列表数据接口 lazada lazada商品数据采集接口

RAG+AI工作流+Agent:LLM框架该如何选择,全面对比MaxKB、Dify、FastGPT、RagFlow、Anything-LLM,以及更多推荐

汀丶人工智能

agent rag FastGPT dify AI 智能体

如何建立变更管控流水线

老张

软件测试 质量保障 交付质量 线上发布 变更管理

简析漏洞生命周期管理的价值与关键要求

我再BUG界嘎嘎乱杀

网络安全 安全 漏洞 网安

两个爆点,引爆在线教育平台数智化

字节跳动数据平台

在线教育 可视化 云服务 数智化

如何将文本转换为向量(DashScope)

DashVector

数据库 向量检索 大模型

GPT4o-mini是什么?有什么特点?

蓉蓉

GPT-4o mini

滴滴开源 LogicFlow:专注流程可视化的前端框架

源字节1号

开源

深入解析 Go 语言 GMP 模型:并发编程的核心机制

王中阳Go

Go 面试 GMP

支持英文语言的堡垒机是什么?叫做什么名字?

行云管家

软件 堡垒机

如何将C# 7类库升级到C# 8?使用可空引用类型_编程语言_Jonathan Allen_InfoQ精选文章