写在前面
在书写C#代码的时候你是否有过这样的经历:经常混用属性以及公有的数据成员。毕竟他们的用法基本一致,对于使用来说好像没什么区别啊。其实我也经常使用类的公有的数据成员来定义一些常量,为了简单,在一些仅仅需要对外暴露一些常量的类中(如定义一些全局使用的常量),也都是通过定义公有数据成员实现的。直到看到世界世界知名专家Bill Wagner
的那本《More Effective C#》之后才意识到应该尽量“使用属性而不是可直接访问的数据成员”。因为属性具有修改的便捷性,多线程的支持等等。
作者:依乐祝
原文地址:
为什么应该尽量使用属性
属性一直是C#语言的特色,目前的属性机制比C#刚引人它的时候更为完备,这使得开发者能够通过属性实现很多功能,例如,可以给getter
与setter
设定不同的访问权限。与直接通过数据成员来编程的方式相比,自动属性可以省去大量的编程工作,而且开发者可以通过该机制轻松地定义出只读的属性。此外还可以结合以表达式为主体的 ( expression-bodied) 写法将代码变得更紧凑。 有了这些机制就不应该继续在类型中创建公有 ( publish) 字段, 也不应该继续手工编写get
与set
方法。 属性既可以令调用者通过公有接口访问相关的数据成员 , 又可以确保这些成员得到面向对象式的封装。
注:在C#语言中, 属性这种元素可以像数据成员一样被访问, 但它们其实是通过方法来实现的。
方便修改
在所有的类与结构中,应该多使用属性,这样可以让你在发现新的需求时,更为方便的修改代码。比如说,如果你现在决定Customer
类型的name(名字)
数据不应该出现空白值,那么只需要修改Name
属性的代码即可:
public class Customer{ private string name; public string name { get=>name; set { if(string.IsNullOrWhiteSpace(value)) { throw new ArgumentException( "Name connot be blank", nameof(Name) ); } name=value; } }}
假如当初没有通过公有属性来实现Name
,而是采用了公有数据成员,那么现在我们就必须在代码库里找到设置过该成员的每行代码,并逐个修改,这会浪费很多时间。
多线程支持
由于属性是通过方法实现的,因此,开发者很容易就能给它添加多线程的支持。例如可以像下面这样实现get
与·set
访问器,使外界对Name
数据的访问得以同步:
public class Customer{ private object syncHandle = new object(); private string name; public string name { get { lock (syncHandle) { return name; } } set { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException( "Name connot be blank", nameof(Name) ); } lock (syncHandle) { name = value; } } }}
方法具备的好处,属性全有
C# 方法所具备的一些特性同样可以体现在属性身上,其中很明显的一条就是属性也可以声明为virtual
:
public class Customer{ public virtual string Name { get; set; }}
Note:刚才几个例子涉及属性的地方用的都是隐式写法。采用隐式写法时,开发者不用自己在属性的
getter
与setter
中编写过多逻辑。也就是说,我们在用属性来表示比较简单的字段时,无需通过大量的模板代码来构建这个属性,编译器会为我们自动创建私有字段(该字段通常称为后援字段,并实现get
,set
这两个访问器所需的简单逻辑)。
可以是抽象的,并成为接口的一部分
属性也可以是抽象的,从而成为接口定义的一部分,这种属性写起来与隐士属性相似。下面这段代码,就演示了怎样在泛型接口中定义属性。虽然与隐士属性的写法相似,但这种属性没有对应的实现物,定义该属性的接口只是要求实现本接口的类型都必须满足接口所订立的契约,也就是必须正确的提供Name
及Value
这两个属性:
public interface INameValuePair{ string Name { get; } T Value { get; set; }}
很方便的控制获取及设置权限
对于类型中的属性来说,它的访问器分成getter(获取器)
与setter(设置器)
这两个单独的方法,这使得我们能够对二者施加不同的修饰符,以便分别控制外界对该属性的获取权限以及设置权限。由于这两种权限可以分开调整,因此我们能够通过属性更为灵活的封装数据元素:
public class Customer{ public virtual string Name { get; protected set; }}
带参数的属性
属性不只适用于简单的数字字段。如果某个类型要在其接口中发布能够用索引来访问的内容,那么就可以创建索引器。这相当于带有参数的属性,或者说参数化的属性。下面这种写法很有用,用它创建出的属性能够返回序列中的某个元素:
public class Customer{ public virtual string Name { get; protected set; } public int this[int index] { get => theValues[index]; set => theValues[index] = value; } private int[] theValues = new int[100];}//Accessing an indexer;int val=someCustomer[i];
此外,若参数是整数的一维索引器,则可以参与数据绑定,若参数不是整数的一维索引器,则可以用来定义映射关系:
private DictionaryaddressValues; public Address this[string name] { get => addressValues[name]; set => addressValues[name] = value; }
注意:索引器一律要用this关键字来声明。由于C#不允许给索引器起名字,因此同一个类型的索引器必须在参数列表上有所区别,否则就会产生歧义。
另外,索引器必须明确的实现出来,而不能像简单属性那样由系统默认生成。
其他说明
后期再把数据成员改成属性
尽管属性是个相当好的机制,可是还有人想先创建普通的数据成员,然后在确实有必要的情况下再将其替换成属性,以便使用属性所具备的优势。这种想法听上去很有道理,但实际并不合适。例如,如下定义一个普通数据成员的代码:
public class Customer{ public string Name;}string name = customerOne.Name;customerOne.Name = "yilezhu";
其实我也经常这样用,不过都是定义一些静态的全局常量。
虽然在使用上属性可以像数据成员那样来访问,但是从MSIL的角度来看,却不是这样,因为访问属性时所使用的指令与访问数据成员所使用的指令是有区别的。因此如果把数据成员改成属性,则会破坏二进制层面的兼容机制,使得很难单独更新某一个程序集,需要全部更新。属性的性能损耗
你可能要问了,是以属性的形式访问数据比较快,还是以数据成员的形式访问比较快?其实前者的效率虽然不会超过后者,但也未必落后于它。因为JIT编译器会对某些方法调用进行内联处理,其中也包括属性。如果编译器对属性进行内联处理的话,那么它的效率就会与数据成员相同。即便没有内联,两者的差别也可以忽略不计。
总结
今天给大家介绍了使用属性来访问数据成员的诸多优势,因此建议如果要在类型的公有或受保护的接口中发布数据,那么应该以属性的形式来发布,对于序列或字典来说,应该以索引器的形式发布。在日常的开发中虽然用属性的形式来封装变量会占用你一到两分钟的时间,但是如果你一开始没有使用属性,后来想用属性来设计,那么可能就得用好几个小时去修正了。现在多花点时间,将来会省很多功夫。
文章大多内容来自观看《More Effective C#》第一小节的内容所做的笔记,当然后续我还会对剩下的提升C#代码的50个方法进行总结记录,敬请期待吧。如果你有兴趣可以加DotNetCore实战项目交流群637326624跟大伙进行交流。