写点什么

如何将 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:128755
用户头像

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

关注

评论

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

DGIOT 工业物联网开源平台简介

dgiot

物联网 2月月更 2月日更 dgiot dgiot物联网

独家下载!阿里云云原生携 10+ 技术专家带来《云原生与云未来的新可能》

阿里巴巴云原生

阿里云 Kubernetes 云原生 电子书

Go 语言Web开发很简单:使用模板将视图与逻辑分离

宇宙之一粟

Go 语言 Web应用开发 2月月更

计算机网络面试知识点

yuexin_tech

面试 计算机网络

DevOps进阶(三)走近 DevOps 工程师

No Silver Bullet

DevOps 敏捷 jenkins 2月月更

《MySQL入门很轻松》第4章:数据表中能存放的数据类型

乌龟哥哥

数据库 2月月更

Linux系统编程-进程间通信(mmap内存映射)

DS小龙哥

2月月更 mmap内存映射

甜言蜜语生成器、定时问候邮件机…开源程序员为这个情人节付出太多

腾源会

开源

微信朋友圈高性能架构方案

Geek_36cc7c

花灯照 人笑颜|OceanBase祝大家工作生活都和元宵一样甜

OceanBase 数据库

数据库 分布式 开发者 OceanBase 开源 元宵

SSH远程连接命令执行没反应不报错问题解决(-bash: fork: retry: Resource temporarily unavailable.[资源暂时不可用])

山河已无恙

SSH Linxu 2月月更

国内唯一!阿里云容器服务进入 Forrester 领导者象限

阿里巴巴云原生

阿里云 云原生 容器平台

人人都是 Serverless 架构师 | 现代化 Web 应用开发实战

阿里巴巴云原生

阿里云 Serverless 云原生

Kotlin语法手册(三)

寻找生命中的美好

android kotlin 安卓

详细讲解mybatis的执行流程

编程江湖

mybatis

大数据培训:在 flink 中使用 hive udf的原因分析

@零度

flink 大数据开发

虎啸春来!丰树电子与中联重科签署战略合作协议

联营汇聚

如何快速构建服务发现的高可用能力

阿里巴巴云原生

阿里云 开源 微服务 云原生

企业为什么要做应用多活?

阿里巴巴云原生

阿里云 云原生 容灾

剑指Offer——企业级项目中分层的含义与依据及多态的优势

No Silver Bullet

封装 offer 继承 多态 2月月更

为什么start方法不能重复调用?而run方法却可以?

王磊

Java 面试

还没有表白神器?情人节来喽,快为心爱的她送上一份专属的礼物吧~

是Dream呀

Python 2月月更

美团动态线程池实践思路,开源了

yanhom

Java 线程池 动态调整线程池参数 动态线程池 美团线程池

阿里云容器服务差异化 SLO 混部技术实践

阿里巴巴云原生

阿里云 Kubernetes 云原生 混部技术

专访 OpenKruise 负责人:现在的云原生应用自动化发展到什么程度了?

阿里巴巴云原生

阿里云 开源 云原生

给面试加点硬菜:延迟任务场景,该如何提高吞吐量和时效性!

小傅哥

小傅哥 架构设计 Redis 6.0 任务系统

教程直播第8期|一文详解 OceanBase 社区版生态工具 ODP & OCP

OceanBase 数据库

数据库 分布式 直播 OceanBase 开源

Spring Boot Serverless 实战系列 | 性能调优

阿里巴巴云原生

阿里云 Serverless 架构 云原生

平安保险基于 SPI 机制的 RocketMQ 定制化应用

阿里巴巴云原生

阿里云 云原生 消息队列 开源云工具

流量录制与回放在vivo的落地实践

vivo互联网技术

测试工具 回归测试 流量回放

云原生落地大爆发,企业和开发者如何把握先机?

阿里巴巴云原生

阿里云 云原生 ACK ACK Anywhere

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