深入谈谈C#9新特性的实际运用

前言

你一定会好奇:“老周,你去哪开飞机了?这么久没写博客了。”

老周:“我买不起飞机,开了个铁矿,挖了一年半的石头。谁知铁矿垮了,压死了几条蜈蚣,什么也没挖着。”

所以,这么丢死人的事,还是不要提了,爷爷从小教导我做人要低调……

一转眼,.NET 5 要来了,同时也带来了 C# 9。遥想当年,老周刚接触 .NET 1.1 的时候,才刚上大学;如今已经过去13年了。岁月是把水果刀,从来不饶人啊。

老周很少去写诸如“XXX新特性”之类的文章,总觉得没啥用处。不过,针对 C# 9,老周想说一点什么。

好,在开始之前,老周再次强调一下:这些语言新特性的东西,你千万不要特意去学习,千万不要,不要,不要,重要的事情讲四遍!这些玩意儿你只要看看官方给的说明,刷一遍就能掌握了(刷这个比刷抖音有意义多了),不用去学的。如果你连这些东东也要学习成本的话,我只想说句好唱不好听的话——你的学习能力真的值得怀疑。

好了,下面开始表演。

第一出:record 类型

record ,我还是用原词吧,我知道有翻译为“记录类型”的说法。只是,只是,老周老觉得这不太好听,可是老周也找不出更好的词语,还是用回 record吧。

record 是引用类型,跟 class 很像(确实差不多)。那么,用人民群众都熟悉的 class 不香吗,为何要新增个 record 呢?答:为了数据比较的便捷。

不明白?没事,往下看。最近有一位热心邻居送了老周一只宠物:

 public class Cat
 {
  public string Nick { get; set; }
  public string Name { get; set; }
  public int Age { get; set; }
 }

这只新宠物可不简单,一顶一的高级吃货。鱼肉、猪肉、鸡腿、饼干、豆腐、面包、水果、面条、小麦、飞蛾……反正,只要它能塞进嘴里的,它都吃。

接下来,我们 new 两个宠物实例。

// 两个实例描述的是同一只猫
   Cat pet1 = new Cat
   {
    Nick = "松子",
    Name = "Jack",
    Age = 1
   };
   Cat pet2 = new Cat
   {
    Nick = "松子",
    Name = "Jack",
    Age = 1
   };

   // 居然不是同一只猫
   Console.WriteLine("同一只?{0}", pet1 == pet2);

其实,两个实例描述的都是我家的乖乖。可是,输出的是:

同一只?False

这是因为,在相等比较时,人家关心的类型引用——引用的是否为同一个实例。但是,在数据处理方案中,我们更关注对象中的字段/属性是否相等,即内容比较。

现在,把 Cat 的声明改为 record 类型。

 public record Cat
 {
  public string Nick { get; set; }
  public string Name { get; set; }
  public int Age { get; set; }
 }

然后同样用上面的 pet1 和 pet2 实例进行相等比较,得到预期的结果:

同一只?True

record 类型让你省去了重写相等比较(重写 Equals、GetHashCode 等方法或重载运算符)的逻辑。

实际上,代码在编译后 record 类型也是一个类,但自动实现了成员相等比较的逻辑。以前你要手动去折腾的事现在全交给编译器去干。

假如,有一个 User 类型,用于表示用户信息(包括用户名、密码),然后这个 User 类型在数据处理方案中可能会产生N多个实例。例如你根据条件从EF模型中筛选出一个 User 实例 A,根据用户输入的登录名和密码产生了 User 实例 B。为了验证用户输入的登录信息是否正确,如果 User 是 class,你可能要这样判断:

if(A.UserName == B.UserName && A.Password == B.Password)
{
 ..................
}

但要是你把 User 定义为 record 类型,那么,一句话的工夫:

A == B

第二出:模式匹配(Pattern Matching)

"模式匹配"这个翻译感觉怪怪滴,老周还没想出什么更好的词语。模式匹配并不是什么神奇的东西,它只是在对变量值进行检测时的扩展行为。以前,老感觉C++/C# 的 switch 语句不够强大,因为传统的用法里面,每个 case 子句只能比较单个常量值。比如

int 考试成绩 = 85;

   switch (考试成绩)
   {
    case 10:
     Console.WriteLine("才考这么点破分啊");
     break;
    case 50:
     Console.WriteLine("还差一点,就合格了");
     break;
    case 85:
     Console.WriteLine("真是秀");
     break;
    case 90:
     Console.WriteLine("奇迹发生");
     break;
   }

我幻想着,要是能像下面这样写就好了:

switch (考试成绩)
   {
    case 0:
     Console.WriteLine("缺考?");
     break;
    case > 0 && <= 30:
     Console.WriteLine("太烂了");
     break;
    case > 30 && < 60:
     Console.WriteLine("还是不行");
     break;
    case >= 60 && < 80:
     Console.WriteLine("还得努力");
     break;
    case >= 80 && < 90:
     Console.WriteLine("秀儿,真优秀");
     break;
    case >= 90 && <= 100:
     Console.WriteLine("不错,奇迹");
     break;
   }

等了很多年很多年(“千年等一回,等……”)以后,终于可以实现了。

switch (考试成绩)
   {
    case 0:
     Console.WriteLine("缺考?");
     break;
    case > 0 and <= 30:
     Console.WriteLine("太烂了");
     break;
    case > 30 and < 60:
     Console.WriteLine("还是不行");
     break;
    case >= 60 and < 80:
     Console.WriteLine("还得努力");
     break;
    case >= 80 and < 90:
     Console.WriteLine("秀儿,真优秀");
     break;
    case >= 90 and <= 100:
     Console.WriteLine("不错,奇迹");
     break;
   }

哟西,真香。

有时候,不仅要检测对象的值,还得深入到其成员。比如下面这个例子,Order类表示一条订单信息。

public class Order
 {
  public int ID { get; set; }
  public string Company { get; set; }
  public string ContactName { get; set; }
  public float Qty { get; set; }
  public decimal UP { get; set; }
  public DateTime Date { get; set; }
 }

前不久,公司接到一笔Order,做成了收益应该不错。

Order od = new Order
   {
    ID = 11,
    Company = "大嘴狗贸易有限公司",
    ContactName = "陈大爷",
    Qty = 425.12f,
    UP = 1000.55M,
    Date = new(2020, 10, 27)
   };

假如我要在变量 od 上做 switch,看看,就这样:

switch (od)
   {
    case { Qty: > 1000f }:
     Console.WriteLine("发财了,发财了");
     break;
    case { Qty: > 500f }:
     Console.WriteLine("好家伙,年度大订单");
     break;
    case { Qty: > 100f }:
     Console.WriteLine("订单量不错");
     break;
   }

咦?这,这是什么鬼?莫惊莫惊,这不是鬼。它的意思是判断 Qty 属性的值,如果订单货量大于 100 就输出“订单量不错”;要是订单货量大于 1000,那就输出“发财了,发财了”。

但你会说,这对大括号怎么来的呢?还记得这种 LINQ 的写法吗?

from x in ...
  where x.A ...
  select new {
   Prop1 = ...,
   Prop2 = ...,
   ................
  }

new { ... } 是匿名类型实例,那如果是非匿名类型呢,看看前面的 Cat 实例初始化。

  Cat {
   ..........
  }

这就对了,这对大括号就是构造某实例的成员值用的,所以,上面的 switch 语句其实是这样写的:

switch (od)
   {
    case Order{ Qty: > 1000f }:
     Console.WriteLine("发财了,发财了");
     break;
    case Order{ Qty: > 500f }:
     Console.WriteLine("好家伙,年度大订单");
     break;
    case Order{ Qty: > 100f }:
     Console.WriteLine("订单量不错");
     break;
   }

Order{ ... } 就是匹配一个 Order 对象实例,并且它的 Qty 属性要符合 ... 条件。由于变量 od 始终就是 Order 类型,所以,case 子句中的 Order 就省略了,变成

    case { Qty: > 1000f }:
     Console.WriteLine("发财了,发财了");
     break;

如果出现多个属性,则表示为多个属性设定匹配条件,它们之间是“且”的关系。比如

    case { Qty: > 100f, Company: not null }:
     Console.WriteLine("订单量不错");
     break;

猜猜啥意思?这个是可以“望文生义”的,Qty 属性的值要大于 100,并且 Company 属性的值不能为 null。不为 null 的写法是 not null,不要写成 !null,因为这样太难看了。

如果你的代码分支较少,你可以用 if 语句的,只是得配合 is 运算符。

   if (od is { UP: < 3000M })
   {
    Console.WriteLine("报价不理想");
   }

但是,这个写法目前有局限性,它只能用常量值来做判断,你要是这样写就会报错。

   if (od is { Date: < DateTime.Now })
   {
    ................
   }

DateTime.Now 不是常量值,上面代码无法通过编译。

is 运算符以前是用来匹配类型的,上述的用法是它的语法扩展。

   object n = 5000000L;
   if(n is long)
   {
    Console.WriteLine("它是个长整型");
   }

进化之后的 is 运算符也可以这样用:

   object n = 5000000L;
   if(n is long x)
   {
    Console.WriteLine("它是个长整型,存放的值是:{0}", x);
   }

如果你在 if 语句内要使用 n 的值,就可以顺便转为 long 类型并赋值给变量 x,这样就一步到位,不必再去写一句 long x = (long)n 。

如果 switch... 语句在判断之后需要返回一个值,还可以把它变成表达式来用。咱们把前面的 Order 例子改一下。

string message = od switch
   {
    { Qty: > 1000f } => "发财了",
    { Qty: > 500f }  => "年度大订单",
    { Qty: > 100f }  => "订单量不错",
    _     => "未知"
   };

   Console.WriteLine(message);

这时候你得注意:

1)switch 现在是表达式,不是语句块,所以最后大括号右边的分号不能少;

2)因为 switch 成了表达式,就不能用 case 子句了,所以直接用具体的内容来匹配;

3)最后返回“未知”的那个下划线(_),也就是所谓的“弃婴”,哦不,是“弃元”,就是虽然赋了值但不需要使用的变量,可以直接丢掉。这里就相当于 switch 语句块中的 default 子句,当前面所有条件都不能匹配时,就返回“未知”。

第三出:属性的 init 访问器

要首先得知道,这个 init 只用于只读属性的初始化阶段,对于可读可写的属性,和以前一样,直接 get; set; 即可。

有人说这个 init 不知干啥用,那好,咱们先不说它,先来看看 C# 前些版本中新增的属性初始化语句。

  public class Dog
  {
    public int No { get; } = 0;
    public string Name { get; } = "no name";
    public int Age { get; } = 1;
  }

你看,这样就可以给属性分配初始值了,那还要 init 干吗呢?

好,我给你制造一个问题——我要是这样初始化 Dog 类的属性,你试试看。

      Dog x = new Dog
      {
        No = 100,
        Name = "吉吉",
        Age = 4
      };

试一下,编译会出错吧。

有些情况,你可以在属性定义阶段分配初始值,但有些时候,你必须要在代码中初始化。在过去,我们会通过定义带参数的构造函数来解决。

public class Dog
  {
    public int No { get; } = 0;
    public string Name { get; } = "no name";
    public int Age { get; } = 1;

    public Dog(int no, string name, int age)
    {
      No = no;
      Name = name;
      Age = age;
    }
  }

然后,这样初始化。

  Dog x = new(1001, "吉吉", 4);

可是,这样做的装逼指数依然不够高,你总不能每个类都来这一招吧,虽然不怎么辛苦,但每个类都得去写一个构造函数,不利落。

于是,init 访问器用得上了,咱们把 Dog 类改改。

  public class Dog
  {
    public int No { get; init; }
    public string Name { get; init; }
    public int Age { get; init; }
  }

你不用再去写带参数的构造函数了,实例化时直接为属性赋值。

      Dog x = new Dog
      {
        No = 100,
        Name = "吉吉",
        Age = 4
      };

这样一来,这些只读属性都有默认的初始值了。

当然,这个赋值只在初始化过程中有效,初始化之后你再想改属性的值,没门!

      x.Name = "冬冬"; //错误
      x.Age = 10;    //错误

嗯,好了,以上就是老周对 C# 9 新特性用法的一些不成文的阐述。看完后你就别说难了。

总结

到此这篇关于C#9新特性实际运用的文章就介绍到这了,更多相关C#9新特性实际运用内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C#9.0 新特性简介

    CandidateFeaturesForCSharp9 看到标题,是不是认为我把标题写错了?是的,C# 8.0还未正式发布,在官网它的最新版本还是Preview 5,通往C#9的漫长道路却已经开始.前写天收到了活跃在C#一线的BASSAM ALUGILI给我分享C# 9.0新特性,我在他文章的基础上进行翻译,希望能对大家有所帮助. 这是世界上第一篇关于C#9候选功能的文章.阅读完本文后,你将会为未来可能遇到的C# 9.0新特性做好更充分的准备. 这篇文章基于, C# 9.0候选新特性 原生大小的

  • 浅谈C#9.0新特性之参数非空检查简化

    参数非空检查是缩写类库很常见的操作,在一个方法中要求参数不能为空,否则抛出相应的异常.比如: public static string HashPassword(string password) { if(password is null) { throw new ArgumentNullException(nameof(password)); } ... } 当异常发生时,调用者很容易知道是什么问题.如果不加这个检查,可能就会由系统抛出未将对象引用为实例之类的错误,这不利于调用者诊断错误. 由

  • 浅析C# 9.0 新特性之 Lambda 弃元参数

    大家好,这是 C# 9.0 新特性短系列的第 5 篇文章. 弃元(Discards) 是在 C# 7.0 的时候开始支持的,它是一种人为丢弃不使用的临时虚拟变量.语法上它是用来赋值的,但它却不被分配存储空间,即没有值,所以不能从中读取值.弃元用 _(下划线) 表示,下划线是一个关键字,只能赋值,不能读取,例如: 在 C# 7.0 中,弃元的使用场景主要有下面四种: 元组和对象的解构 使用 is 和 switch 的模式匹配 对具有 out 参数的方法的调用 作用域内独立使用场景 针对这几个场景,

  • C# 9.0 新特性之模式匹配简化的实现

    记得在 MS Build 2020 大会上,C# 语言开发项目经理 Mads Torgersen 宣称 C# 9.0 将会随着 .NET 5 在今年 11 月份正式发布.目前 .NET 5 已经到了 Preview 5 阶段了,C# 9.0 也已经初具规模.忍不住激动的心情,暂停更新<C#.NET 拾遗补漏>系列几天,先要和大家分享一下我了解到的 C# 9.0 的新特性.由于新特性比较多,所以会分成几篇来讲.这是第一篇,专讲模式匹配这个特性的简化. 模式匹配(Pattern Matching)

  • c# 9.0新特性nint和Pattern matching的使用方法

    一:背景 1. 讲故事 上一篇跟大家聊到了Target-typed new 和 Lambda discard parameters,看博客园和公号里的阅读量都达到了新高,甚是欣慰,不管大家对新特性是多头还是空头,起码还是对它抱有一种极为关注的态度,所以我的这个系列还得跟,那就继续开撸吧,今天继续带来两个新特性,更多新特性列表,请大家关注:新特性预览 二:新特性研究 1. Native ints 从字面上看貌似是什么原生类型ints,有点莫名其妙,还是看一看Issues上举得例子吧: Summar

  • 浅谈C# 9.0 新特性之只读属性和记录

    大家好,这是 C# 9.0 新特性系列的第 4 篇文章. 熟悉函数式编程的童鞋一定对"只读"这个词不陌生.为了保证代码块自身的"纯洁",函数式编程是不能随便"弄脏"外来事物(参数.变量等)的,所以"只读"对函数式编程非常重要. 为了丰富 C# 对函数式编程支持,较新的 C# 版本引入了一些很有用的新特性.比如 C# 8 中就对 struct 类型的方法增加了 readonly 修饰符支持,被 readonly 修饰的方法是不能

  • 深入谈谈C#9新特性的实际运用

    前言 你一定会好奇:"老周,你去哪开飞机了?这么久没写博客了." 老周:"我买不起飞机,开了个铁矿,挖了一年半的石头.谁知铁矿垮了,压死了几条蜈蚣,什么也没挖着." 所以,这么丢死人的事,还是不要提了,爷爷从小教导我做人要低调-- 一转眼,.NET 5 要来了,同时也带来了 C# 9.遥想当年,老周刚接触 .NET 1.1 的时候,才刚上大学:如今已经过去13年了.岁月是把水果刀,从来不饶人啊. 老周很少去写诸如"XXX新特性"之类的文章,总觉得

  • 聊聊 PHP 8 新特性 Attributes

    PHP8的Alpha版本,过几天就要发布了,其中包含了不少的新特性,当然我自己认为最重要的还是JIT,这个我从2013年开始参与,中间挫折无数,失败无数后,终于要发布的东东. 不过,今天呢,我不打算谈JIT,等PHP8发布了以后,我再单独写个类似<深入理解PHP8之JIT>系列来说吧. 嘿嘿,今天呢,我想谈谈Attributes,为啥呢, 是昨天我看到很多群在转发一个文章,叫做<理解PHP8中的Attributes>,说实在的,这篇文章应该是直接从英文翻译过来的,写的晦涩难懂,很多

  • Vue.js 2.5新特性介绍(推荐)

    TypeScript TypeScript是一种由微软开发的自由和开源的编程语言.它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程.2012年十月份,微软发布了首个公开版本的TypeScript,在2013年6月19日,微软发布了TypeScript 0.9的正式版本,到目前为止,TypeScript已发展到2.x版本 安装TypeScript 安装TypeScript主要有两种方式: 通过npm方式安装(Node.js包管理器) 安装TypeS

  • Java8新特性Stream流实例详解

    什么是Stream流? Stream流是数据渠道,用于操作数据源(集合.数组等)所生成的元素序列. Stream的优点:声明性,可复合,可并行.这三个特性使得stream操作更简洁,更灵活,更高效. Stream的操作有两个特点:可以多个操作链接起来运行,内部迭代. Stream可分为并行流与串行流,Stream API 可以声明性地通过 parallel() 与sequential() 在并行流与顺序流之间进行切换.串行流就不必再细说了,并行流主要是为了为了适应目前多核机器的时代,提高系统CP

  • JAVA8 十大新特性详解

    "Java is still not dead-and people are starting to figure that out." 本教程将用带注释的简单代码来描述新特性,你将看不到大片吓人的文字. 一.接口的默认方法 Java 8允许我们给接口添加一个非抽象的方法实现,只需要使用 default关键字即可,这个特征又叫做扩展方法,示例如下: 复制代码 代码如下: interface Formula {    double calculate(int a); default do

  • ES6新特性三: Generator(生成器)函数详解

    本文实例讲述了ES6新特性三: Generator(生成器)函数.分享给大家供大家参考,具体如下: 1. 简介 ① 理解:可以把它理解成一个函数的内部状态的遍历器,每调用一次,函数的内部状态发生一次改变. ② 写法: function* f() {} ③ 作用:就是可以完全控制函数的内部状态的变化,依次遍历这些状态. ④ 运行过程:当调用Generator函数的时候,该函数并不执行,而是返回一个遍历器(可以理解成暂停执行).通过调用next()开始执行,遇到yield停止执行,返回一个value

  • C# 8.0新特性介绍

    C# 语言是在2000发布的,至今已正式发布了7个版本,每个版本都包含了许多令人兴奋的新特性和功能更新.同时,C# 每个版本的发布都与同时期的 Visual Studio 以及 .NET 运行时版本高度耦合,这也有助于开发者更好的学习掌握 C#,并将其与 Visual Studio 以及 .NET 的使用结合起来. 加快 C# 版本的发布速度 在被称为"新微软"的推动下,微软创新的步伐也加快了.为了做到加快步伐,微软开发部门将一些过去集成在一起的技术现在都分离了出来. Visual S

  • 详解Android Studio 3.0的新特性与适配

    简介 Android Studio升级到3.0后,有不少的改动和新特性,先贴出官方的迁移说明. 本文会持续收集与总结本人在使用Android Studio 3.0进行开发的过程中所遇到的问题. 版本配置 Gradle版本 Android Studio 3.0需要的Gradle版本至少为4.1. 如果是使用gradle wrapper,则工程根目录/gradle/wrapper/gradle-wrapper.properties中的distributionUrl字段为https\://servic

  • W3C Group的JavaScript1.8 新特性介绍

    JavaScript 1.8 计划作为Gecko 1.9(将要合并在Firefox 3中)的一部分开始使用的.比起JavaScript 1.7来说,这只是很小的更新,不过它也确实包含了一些向ECMAScript 4/JavaScript 2进化的痕迹.JavaScript 1.8还将包含在JavaScript 1.6 和 JavaScript 1.7中的所有新特性. 使用 JavaScript 1.8 为了可以在HTML中使用 JavaScript 1.8 的新特性,需要如下这样写: <scri

  • AngularJS 2.0新特性有哪些

    AngularJS已然成为Web应用开发世界里最受欢迎的开源JavaScript框架.自成立以来,见证其成功的是惊人的经济增长以及团体的支持与采用--包括个人开发者.企业.社区. Angular已经变成一个构建复杂单页面应用的客户端MVW框架(Model-View-Whatever).它在应用测试和应用编写方面都扮演重要角色,同时简化了开发过程. Angular目前的版本为1.3,该版本稳定,并被谷歌(框架维护者)用于支持众多应用(据估计,在谷歌有超过1600个应用运行于Angular1.2或1

随机推荐