首页
TVP 最新优惠活动
学习
活动
专区
工具
返回腾讯云官网
.NET 5.0已经发布,C# 9.0也为我们带来了许多新特性,其中最让我印象深刻的就是init和record type,很多文章已经把这两个新特性讨论的差不多了,本文不再详细讨论,而是通过使用角度来思考这两个特性。
init是C# 9.0中引入的新的访问器,它允许被修饰的属性在对象初始化的时候被赋值,其他场景作为只读属性的存在。直接使用的话,可能感受不到init的意义,所以我们先看看之前是如何设置属性为只读的。
设置只读属性有很多种方式,本文基于private set来讨论。 首先声明一个产品类,如下代码所示,我们把Id设置成了只读,这个时候也就只能通过构造函数来赋值了。在通常情况下,实体的唯一标识是不可更改的,同时也要防止Id被意外更改。
public class Product
{
public Product(int id)
{
this.Id = id;
}
public int Id { get; private set; }
//public int Id { get; }
public string ProductName { get; set; }
public string Description { get; set; }
}
class Program
{
static void Main(string[] args)
{
Product product = new Product(1)
{
ProductName = "test001",
Description = "Just a description"
};
Console.WriteLine($"Current Product Id: {product.Id},\n\rProduct Name: {product.ProductName}, \n\rProduct Description: {product.Description}");
//运行结果
//Current Product Id: 1,
//Product Name: test001,
//Product Description: Just a description
Console.ReadKey();
}
}
使用init方式,是非常简单的,只需要把private set改成init就行了:
public int Id { get; init; }
为了方便比较,我们可以将ProductName设置成了private set,然后通过ILSpy来查看一下编译后的代码,看看编译后的Id和ProductName有何不同
咋一看,貌似没啥区别,都使用到了initonly来修饰。但是如果仅仅只是替换声明方式,那么这个新特性似乎就没有什么意义了。 接下来我们看第二张图:
如图标记的那样,区别还是很明显的,通过init修饰的属性并没有完全替换掉set,由此看来微软在设计init的时候,还是挺用心思的,也为后面的赋值留下了入口。
instance void modreq([System.Runtime]System.Runtime.CompilerServices.IsExternalInit) set_Id (
int32 'value'
)
另外在赋值的时候,使用private set修饰的属性,需要定义构造函数,通过构造函数赋值。而使用了init修饰的属性,则不需要定义构造函数,直接在对象初始化器中赋值即可。
Product product = new Product
{
Id = 1,
ProductName = "test001",
Description = "Just a description"
};
product.Id = 2;//Error CS8852 Init-only property or indexer 'Product.Id' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor.
如上代码所示,只读属性Id的赋值并没有在构造函数中赋值,毕竟当一个类的只读字段十分多的时候,构造函数也变得复杂。而且在赋值好之后,无法修改,这和我们对只读属性在通常情况下的理解是一致的。另外通过init修饰的好处便是省却了一部分只读属性在操作上的复杂性,使得对象的声明与赋值更加直观。 在合适的场景下选择最好的编程方式,是程序员的一贯追求,千万不要为了炫技而把init当成了茴字的第N种写法到处去问。
record是一个非常有用的特性,它是不可变类型,其相等性是通过内部的几个属性来确定的,同时它支持我们以更加方便的方式、像定义值类型那样来定义不可变引用类型。 我们把之前的Product类改成record类型,如下所示:
public record Product
{
public Product(int id, string productName, string description) => (Id, ProductName, Description) = (id, productName, description);
public int Id { get; }
public string ProductName { get; }
public string Description { get; }
}
然后查看一下IL,可以看到record会被编译成类,同时继承了System.Object,并实现了IEquatable泛型接口。 编译器为我们提供的几个重要方法如下:
通过图片中的代码,我们知道比较两个record对象,首先需要比较类型是否相同,然后再依次比较内部属性。
record类型通过基类型以及所有的属性及字段的方式来计算HashCode,这在整个继承层次结构中增强了基于值的相等性,也就意味着两个同名同姓的人不会被认为是同一个人
这个方法貌似非常简单,实在看不出有什么特别的地方,那么我们通过后面的内容再来解释这个方法。
了解DDD值对象的小伙伴应该想到了,record类型的特性非常像DDD中关于值对象的描述,比如不可变性、其相等于是基于其内部的属性的等等,我们先来看下值类型的定义方式。
public abstract class ValueObject
{
public static bool operator ==(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, null) || left.Equals(right);
}
public static bool operator !=(ValueObject left, ValueObject right)
{
return !(left == right);
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
// Other utility methods
}
public class Address : ValueObject
{
public string Street { get; private set; }
public string City { get; private set; }
public string State { get; private set; }
public string Country { get; private set; }
public string ZipCode { get; private set; }
public Address(string street, string city, string state, string country, string zipcode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipcode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
// Using a yield return statement to return each element one at a time
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
public override string ToString()
{
return $"Street: {Street}, City: {City}, State: {State}, Country: {Country}, ZipCode: {ZipCode}";
}
}
main方法如下:
static void Main(string[] args)
{
Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
Console.WriteLine($"address1: {address1}");
Address address2 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
Console.WriteLine($"address2: {address2}");
Console.WriteLine($"address1 == address2: {address1 == address2}");
string jsonAddress1 = address1.ToJson();
Address jsonAddress1Deserialize = jsonAddress1.FromJson<Address>();
Console.WriteLine($"jsonAddress1Deserialize == address1: {jsonAddress1Deserialize == address1}");
Console.ReadKey();
}
运行结果如下:
基于class:
address1: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
address2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
address1 == address2: True
jsonAddress1Deserialize == address1: True
如果有大量的值对象需要我们编写,这无疑是加重我们的开发量的,这个时候record就派上用场了,最简洁的record风格的代码如下所示,只有一行:
public record Address(string Street, string City, string State, string Country, string ZipCode);
IL代码如下图所示,从图中我们也可以看到record类型的对象,默认情况下用到了init来限制属性的只读特性。
main方法代码不变,运行结果也没有因为Address从class变成record而发生改变
基于record:
address1: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
address2: Street: aaa, City: bbb, State: ccc, Country: ddd, ZipCode: fff
address1 == address2: True
jsonAddress1Deserialize == address1: True
如此看来我们的代码节省的不止一点点,而是太多太多了,是不是很爽啊。
使用方式如下:
class Program
{
static void Main(string[] args)
{
Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
Console.WriteLine($"1. address1: {address1}");
Address addressWith = address1 with { Street = "############" };
Console.ReadKey();
}
}
public record Address(string Street, string City, string State, string Country, string ZipCode);
通过ILSpy查看如下所示:
private static void Main(string[] args)
{
Address address1 = new Address("aaa", "bbb", "ccc", "ddd", "fff");
Console.WriteLine($"1. address1: {address1}");
Address address2 = address1.<Clone>$();
address2.Street = "############";
Address addressWith = address2;
Console.ReadKey();
}
由此可以看到record在更改的时候,实际上是通过调用Clone而产生了浅拷贝的对象,这也非常符合DDD ValueObject的设计理念。
参考:
本文分享自 DotNet技术平台 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!
两个鬼故事长春搜房网新房咏月杜怎么起名加盟快餐店起名用笔画姓聂女宝宝取名起名大全罪人和圣人孙姓女宝起名湖南卫视节目表今天电信积分兑换商城官网调剂行给砖厂起个名称搞笑空间留言代码名表表起雾木命起名用木百货店名起名大全伪神复苏菏泽商标注册宠物猫起名字农村合作社怎么起名字猎美高手我喜爱的职业我的四季企业名字起名猪年男孩取名起名大全宜用字起重设备厂家名录梦见坐飞机轩辕剑外传穹之扉周易易经取名起名大全为什么白敬亭不拍吻戏少年生前被连续抽血16次?多部门介入两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”淀粉肠小王子日销售额涨超10倍高中生被打伤下体休学 邯郸通报单亲妈妈陷入热恋 14岁儿子报警何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言张家界的山上“长”满了韩国人?男孩8年未见母亲被告知被遗忘中国拥有亿元资产的家庭达13.3万户19岁小伙救下5人后溺亡 多方发声315晚会后胖东来又人满为患了张立群任西安交通大学校长“重生之我在北大当嫡校长”男子被猫抓伤后确诊“猫抓病”测试车高速逃费 小米:已补缴周杰伦一审败诉网易网友洛杉矶偶遇贾玲今日春分倪萍分享减重40斤方法七年后宇文玥被薅头发捞上岸许家印被限制高消费萧美琴窜访捷克 外交部回应联合利华开始重组专访95后高颜值猪保姆胖东来员工每周单休无小长假男子被流浪猫绊倒 投喂者赔24万小米汽车超级工厂正式揭幕黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发当地回应沈阳致3死车祸车主疑毒驾恒大被罚41.75亿到底怎么缴妈妈回应孩子在校撞护栏坠楼外国人感慨凌晨的中国很安全杨倩无缘巴黎奥运校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变王树国卸任西安交大校长 师生送别手机成瘾是影响睡眠质量重要因素国产伟哥去年销售近13亿阿根廷将发行1万与2万面值的纸币兔狲“狲大娘”因病死亡遭遇山火的松茸之乡“开封王婆”爆火:促成四五十对奥巴马现身唐宁街 黑色着装引猜测考生莫言也上北大硕士复试名单了德国打算提及普京时仅用姓名天水麻辣烫把捣辣椒大爷累坏了