C#9特性record 类型、模式匹配、init 属性详情
c#的特性record 类型、模式匹配、init 属性
一、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);
这时候你得注意:
- switch 现在是表达式,不是语句块,所以最后大括号右边的分号不能少;
- 因为 switch 成了表达式,就不能用 case 子句了,所以直接用具体的内容来匹配;
- 最后返回“未知”的那个下划线(_),也就是所谓的“弃婴”,哦不,是“弃元”,就是虽然赋了值但不需要使用的变量,可以直接丢掉。这里就相当于 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#的record 类型、模式匹配、init 属性详情的详细内容,更多关于c#的record 类型、模式匹配、init 属性的资料请关注其它相关文章!