如何将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。

在项目文件中,添加或修改以下配置:

\u0026lt;/PropertyGroup\u0026gt;    \u0026lt;LangVersion\u0026gt;8.0\u0026lt;/LangVersion\u0026gt;    \u0026lt;NullableContextOptions\u0026gt;enable\u0026lt;/NullableContextOptions\u0026gt;\u0026lt;/PropertyGroup\u0026gt;

在保存文件后,应该会看到可空性错误。如果没有看到,请尝试构建项目。

指示一个类型可以为空

在接口方法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\u0026lt;ValueTuple\u0026lt;TKey1, TKey2\u0026gt;, TValue\u0026gt; m_Base;IEnumerable\u0026lt;TValue\u0026gt; IReadOnlyDictionary\u0026lt;ValueTuple\u0026lt;TKey1, TKey2\u0026gt;, TValue\u0026gt;.Values{    get { return (IEnumerable\u0026lt;TValue\u0026gt;)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\u0026lt;in T\u0026gt;(T oldValue, T newValue);

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

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

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

public delegate void ValueChanged\u0026lt;in T\u0026gt;(T? oldValue, T? newValue) where T : class;public delegate void ValueChanged\u0026lt;T\u0026gt;(T? oldValue, T? newValue) where T : struct;

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

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

#nullable disablepublic delegate void ValueChanged\u0026lt;in T\u0026gt;(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 = \u0026quot;\u0026quot;)

还需要空参数检查吗?

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/280018.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

android 设备名称_如何更改您的Android TV的设备名称

android 设备名称Android TV is Google’s attempt at taking over the living room, and with some units being available for under $99, it’s not unheard of for users to have more than one box. The problem is, when multiple devices identify themselves identical…

AD-查找符合指定条件的用户Get-User

以下服务器为Exchange 2010一、使用 Get-User 命令查找部门为IT的用户Get-User -ResultSize Unlimited | ? { $_.Department -Eq "IT" } | ft Name,Department二、查找注释为多行内容的指定用户如下图:注释Notes信息为多行要使用 match 和 (?*) 来做匹配…

目标检测算法之Fast R-CNN算法详解

在介绍Fast R-CNN之前我们先介绍一下SPP Net 一、SPP Net SPP:Spatial Pyramid Pooling(空间金字塔池化) 众所周知,CNN一般都含有卷积部分和全连接部分,其中,卷积层不需要固定尺寸的图像,而全连…

RGB-D(深度图像) 图像深度

RGB-D(深度图像) 深度图像 普通的RGB三通道彩色图像 Depth Map 在3D计算机图形中,Depth Map(深度图)是包含与视点的场景对象的表面的距离有关的信息的图像或图像通道。其中,Depth Map 类似于灰度图像&…

WPF-21 基于MVVM员工管理-01

接下来我们通过两节课程使用MVVM来开发一个简单的Demo,首先我们创建一个项目名称WPF-22-MVVM-Demo,目录结构如下:我们在Models文件下创建Employee类并让该类实现INotifyPropertyChanged接口,该类中定义编号、姓名和角色三个基本属…

qt 苹果应用程序_什么是苹果的电视应用程序,您应该使用它吗?

qt 苹果应用程序Apple’s TV app, which recently appeared on iOS devices and Apple TV, is meant to help users discover and watch shows across an increasingly expanding lineup of television channels, as well as iTunes movies and shows, in one central app. App…

细说flush、ob_flush的区别

ob_flush/flush在手册中的描述, 都是刷新输出缓冲区, 并且还需要配套使用, 所以会导致很多人迷惑… 其实, 他们俩的操作对象不同, 有些情况下, flush根本不做什么事情.. ob_*系列函数, 是操作PHP本身的输出缓冲区. 所以, ob_flush是刷新PHP自身的缓冲区. 而flush, 严格来讲, 这…

关于jHipster框架在构建中的出现的error修复

jhipster The JDL object and the database type are both mandatory.这个错误应该是在构建基于jHipster的spring-cloud项目中经常遇到的,因为这个在这个过程中会读取.yo-rc文件,之后生成相关的.json文件,再之后生成相关的.java文件&#xff…

protobuf编码

proto2Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据序列化,适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。 字段规则 required: 字段必须存在opti…

定制.NET 6.0的Middleware中间件

大家好,我是张飞洪,感谢您的阅读,我会不定期和你分享学习心得,希望我的文章能成为你成长路上的垫脚石,让我们一起精进。在本文中,我们将学习中间件,以及如何使用它进一步定制应用程序。我们将快…

Python-循环控制--个人课堂笔记

Python中的两种循环方式(目前学到):for循环和while循环 for循环和while循环的区别: for循环一般用于控制循环的次数,while循环则是条件循环。 操作实例-猜数字小游戏(3次猜错提示游戏结束)&…

删除microsoft_如何从您的Microsoft帐户中删除设备

删除microsoftWhen you sign into Windows 8 or 10 using your Microsoft account (and other Microsoft devices, like an Xbox), those devices become associated with your account. If you want to remove an old device you’ve gotten rid of, you’ll have to pay a vi…

线程的语法 (event,重要)

Python threading模块 2种调用方式 直接调用 12345678910111213141516171819import threadingimport timedef sayhi(num): #定义每个线程要运行的函数print("running on number:%s" %num)time.sleep(3)if __name__ __main__:t1 threading.Thread(targetsayhi,args(…

求最大值和下标值

本题要求编写程序&#xff0c;找出给定的n个数中的最大值及其对应的最小下标&#xff08;下标从0开始&#xff09;。 输入格式: 输入在第一行中给出一个正整数n&#xff08;1<n≤10&#xff09;。第二行输入n个整数&#xff0c;用空格分开。 输出格式: 在一行中输出最大值及…

windows应用商店修复_如何修复Windows应用商店中的卡死下载

windows应用商店修复Though it’s had its share of flaky behavior since being introduced in Windows 8, the Windows Store has gotten more reliable over time. It still has the occasional problems, though. One of the more irritating issues is when an app update…

OpenWrt:Linux下生成banner

Linux下有三个小工具可以生成banner&#xff1a;1、banner使用#生成banner&#xff1b;2、figlet使用一些普通字符生成banner&#xff1b;3、toilet使用一些复杂的彩色特殊字符生成banner。使用apt-get安装的时候需要输入以下命令&#xff1a; $ sudo apt-get install sysvbann…

新冠病毒中招 | 第二天

今天跟大家分享我个人感染奥密克戎毒株第二天的经历和感受。早上7点多自然醒来&#xff0c;已经没有四肢乏力的感觉&#xff0c;但是身体的本能还是告诉我不愿意动弹。由于第一天躺着睡了一天&#xff0c;确实是躺得腰酸背疼的。起床量了一下体温36.4正常&#xff0c;决定今天不…

输出到Excel

HSSFWorkbook oBook new HSSFWorkbook(); NPOI.SS.UserModel.ISheet oSheet oBook.CreateSheet(); #region 输出到Excel MemoryStream ms new MemoryStream(); oBook.Write(ms);string sExportPath ""; using (SaveFileDialog saveFileDialog1 new SaveFileDial…

JavaScript 精粹 基础 进阶(5)数组

转载请注明出处 原文连接 blog.huanghanlian.com/article/5b6… 数组是值的有序集合。每个值叫做元素&#xff0c;每个元素在数组中都有数字位置编号&#xff0c;也就是索引。JS中的数组是弱类型的&#xff0c;数组中可以含有不同类型的元素。数组元素甚至可以是对象或其它数组…

icloud 购买存储空间_如何释放iCloud存储空间

icloud 购买存储空间Apple offers 5 GB of free iCloud space to everyone, but you’ll run up against that storage limit sooner than you’d think. Device backups, photos, documents, iCloud email, and other bits of data all share that space. Apple为每个人提供5 …