c#编码技巧(十四):新语法糖record深入分析
从 C# 9 开始新增了一个关键字record,用于封装数据。
record实质是微软提供的一个语法糖,因很多开源项目都用到了这个关键字,说明这个语法糖比较实用。
那么这个record类型和普通class类型有什么区别呢?我们可以通过工具探究一下源码
首先新建一个普通的类PersonCls,添加两个属性
///
/// 普通类
///
public class PersonCls
{
public string Name { get; set; }
public string Address { get; set; }
}
再建一个record类型PersonRecord,record类型的声明简洁:一行就搞定,其中属性名字放在括号里,编译器自动为我们生成两个属性
///
/// record类型
///
///
///
public record PersonRecord(string Name, int Age);
//也即public record class PersonRecord(string Name, int Age);
//其中class可省略
var person = new PersonRecord("Tom", 18);
利用反编译工具查看普通类PersonCls代码,果然非常普通,除了两属性什么都没有
using System;
using System.Runtime.CompilerServices;
namespace DotNet6
{
// Token: 0x02000005 RID: 5
[NullableContext(1)]
[Nullable(0)]
public class PersonCls
{
// Token: 0x17000001 RID: 1
// (get) Token: 0x06000005 RID: 5 RVA: 0x00002092 File Offset: 0x00000292
// (set) Token: 0x06000006 RID: 6 RVA: 0x0000209A File Offset: 0x0000029A
public string Name { get; set; }
// Token: 0x17000002 RID: 2
// (get) Token: 0x06000007 RID: 7 RVA: 0x000020A3 File Offset: 0x000002A3
// (set) Token: 0x06000008 RID: 8 RVA: 0x000020AB File Offset: 0x000002AB
public string Address { get; set; }
}
}
同样利用反编译工具查看PersonRecord,却生成了很多代码
其中
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
namespace DotNet6
{
// Token: 0x02000006 RID: 6
[NullableContext(1)]
[Nullable(0)]
public class PersonRecord : IEquatable<PersonRecord>
{
// Token: 0x0600000A RID: 10 RVA: 0x000020BD File Offset: 0x000002BD
public PersonRecord(string Name, string Address)
{
this.Name = Name;
this.Address = Address;
base..ctor();
}
// Token: 0x17000003 RID: 3
// (get) Token: 0x0600000B RID: 11 RVA: 0x000020D4 File Offset: 0x000002D4
[CompilerGenerated]
protected virtual Type EqualityContract
{
[CompilerGenerated]
get
{
return typeof(PersonRecord);
}
}
// Token: 0x17000004 RID: 4
// (get) Token: 0x0600000C RID: 12 RVA: 0x000020E0 File Offset: 0x000002E0
// (set) Token: 0x0600000D RID: 13 RVA: 0x000020E8 File Offset: 0x000002E8
public string Name { get; set; }
// Token: 0x17000005 RID: 5
// (get) Token: 0x0600000E RID: 14 RVA: 0x000020F1 File Offset: 0x000002F1
// (set) Token: 0x0600000F RID: 15 RVA: 0x000020F9 File Offset: 0x000002F9
public string Address { get; set; }
// Token: 0x06000010 RID: 16 RVA: 0x00002104 File Offset: 0x00000304
[CompilerGenerated]
public override string ToString()
{
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append("PersonRecord");
stringBuilder.Append(" { ");
if (this.PrintMembers(stringBuilder))
{
stringBuilder.Append(' ');
}
stringBuilder.Append('}');
return stringBuilder.ToString();
}
// Token: 0x06000011 RID: 17 RVA: 0x00002150 File Offset: 0x00000350
[CompilerGenerated]
protected virtual bool PrintMembers(StringBuilder builder)
{
RuntimeHelpers.EnsureSufficientExecutionStack();
builder.Append("Name = ");
builder.Append(this.Name);
builder.Append(", Address = ");
builder.Append(this.Address);
return true;
}
// Token: 0x06000012 RID: 18 RVA: 0x0000218A File Offset: 0x0000038A
[NullableContext(2)]
[CompilerGenerated]
public static bool operator !=(PersonRecord left, PersonRecord right)
{
return !(left == right);
}
// Token: 0x06000013 RID: 19 RVA: 0x00002196 File Offset: 0x00000396
[NullableContext(2)]
[CompilerGenerated]
public static bool operator ==(PersonRecord left, PersonRecord right)
{
return left == right || (left != null && left.Equals(right));
}
// Token: 0x06000014 RID: 20 RVA: 0x000021AC File Offset: 0x000003AC
[CompilerGenerated]
public override int GetHashCode()
{
return (EqualityComparer<Type>.Default.GetHashCode(this.EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.<Name>k__BackingField)) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(this.<Address>k__BackingField);
}
// Token: 0x06000015 RID: 21 RVA: 0x000021EC File Offset: 0x000003EC
[NullableContext(2)]
[CompilerGenerated]
public override bool Equals(object obj)
{
return this.Equals(obj as PersonRecord);
}
// Token: 0x06000016 RID: 22 RVA: 0x000021FC File Offset: 0x000003FC
[NullableContext(2)]
[CompilerGenerated]
public virtual bool Equals(PersonRecord other)
{
return this == other || (other != null && this.EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(this.<Name>k__BackingField, other.<Name>k__BackingField) && EqualityComparer<string>.Default.Equals(this.<Address>k__BackingField, other.<Address>k__BackingField));
}
// Token: 0x06000018 RID: 24 RVA: 0x0000225F File Offset: 0x0000045F
[CompilerGenerated]
protected PersonRecord(PersonRecord original)
{
this.Name = original.<Name>k__BackingField;
this.Address = original.<Address>k__BackingField;
}
// Token: 0x06000019 RID: 25 RVA: 0x00002280 File Offset: 0x00000480
[CompilerGenerated]
public void Deconstruct(out string Name, out string Address)
{
Name = this.Name;
Address = this.Address;
}
}
}
使用这些方法,可以看到与普通class类不同的是:
static async Task Main(string[] args)
{
//record类型ToString()可以输出名称+值,而类只会输出命名空间+类名
var clsStr = (new PersonCls() { Name = "LiLei", Address="GD" }.ToString());
var str = (new PersonRecord("LiLei", "GD")).ToString();//输出:PersonRecord { Name = LiLei, Address = GD }
//属性值相同的普通类,使用==或Equals比较,判断为不相等
var cls1 = new PersonCls() { Name = "Tom", Address = "CN" };
var cls2 = new PersonCls() { Name = "Tom", Address = "CN" };
bool isClassOperatorEquals = cls1 == cls2;//false
bool isClassEquals = cls1.Equals(cls2);//false
bool isClassReferenceEquls = ReferenceEquals(cls1, cls2);//false
//属性值相同的两个record类型,使用==或Equals比较,判断为相等;使用ReferenceEquals比较,判断为不相等,因为引用是确实是不同
var rcd1 = new PersonRecord("Ben", "HK");
var rcd2 = new PersonRecord("Ben", "HK");
bool isRecordOperatorEquals = rcd1 == rcd2;// true:因为值相同
bool isRecordEquals = rcd1.Equals(rcd2);//true:因为值相同
bool isRecordReferenceEquls = ReferenceEquals(rcd1, rcd2);//false:引用不同
//rcd1.Address = "shenzhen";//定义在括号内的record的属性值,在new之后不能修改;
//想要能修改的属性,就像Level那样声明
/*
public record PersonRecordCanChange(string Name, string Address)
{
public string Level { get; set; }
}
*/
}
record还有其他用法,比如使用with { }可以复制这个record
var rcdCopy = rcd1 with { };//复制
Console.WriteLine($"name={rcdCopy.Name}, address={rcdCopy.Address}");//name=Ben, address=HK
只想部分复制,可以在{}内更改部分属性的值
var rcdCopy = rcd1 with { };//复制
Console.WriteLine($"name={rcdCopy.Name}, address={rcdCopy.Address}");//name=Ben, address=Beijing
可以继承
public record Animal(string Head, string Body);
public record Human(string Head, string Body, string Hand): Animal(Head, Body);
如果确实需要自定义属性,可以这样写
public record Points
{
//声明属性X,Y,并在构造函数中注入赋值
public Points(double time, double distance) => (X, Y) = (time, distance);
public double X { get; set; }
public double Y { get; set; }
}
var point = new Points(time: 10, distance: 990);
var x = point.X;
var y = point.Y;
但不建议这样做,这样违背了record的设计初衷
综上所述: