1.1 - C#语言习惯 - 使用属性而不是可访问的数据成员

 

  属性一直是C#语言中的一等公民。自1.0版本以来,C#对属性进行了一系列的增强,让其表达能力不管提高。你甚至可以为setter和getter指定不同的访问权限。

  隐式属性也极大降低了声明属性时的工作量,不会比声明数据成员麻烦多少。

 

  若你仍然在类型中声明公有成员,或是仍在手工编写set或get之类的方法,那么快停下来吧。

  属性允许将数据成员作为公共接口的一部分暴露出去,同时仍旧提供面向对象环境下所需要的封装。

  属性这个语言元素可以让你像访问数据成员一样使用,但其底层依旧使用方法实现。

 

  类型的某些成员确实非常适合作为数据,例如某个客户的名称,某个站点的x、y坐标或上一年度的收入等。

  而属性则让你可以创建出类似于数据访问,但实际上却是方法调用的借口,自然也可以享受到方法调用的所有好处。

  客户代码访问属性时,就像是在访问公有的字段,不过其底层使用方法实现,其中可以自由定义属性访问器的行为。

 

  .NET Framework假设你会对公有数据成员使用属性。

  实际上,.NET Framework中的数据绑定类仅支持属性,而不支持公有数据成员。

  对于类所有的数据绑定类库均是如此,包括WPF、WinForms和Silverlight。数据绑定会将某个对象的一个属性和某个用户界面控件相互关联起来。

  数据绑定机制将使用反射来找到类型中的特定属性:

1 textBoxCity.DataBindings.Add("Text", address, "City");

  这段代码将textBoxCity控件的Text属性绑定到了address对象的City属性上。

  公有的数据成员并不推荐使用,因此Framework Class Library设计其也不支持其实现绑定。这样的设计也保证了你必须选择合适的面向对象技术。

 

  确实,数据绑定只是用在用户界面逻辑中会使用到的类中。但这并不意味着属性仅应该用在UI逻辑中,其他类和结构中也应使用属性。

  在日后产生新的需求或行为时,属性更易于修改。

  例如,你会很快有这样的想法,客户对象不应该有空白的名称。若你使用了公有属性来封装Name,那么只要修改一处即可:

 1     public class Customer
 2     {
 3         private string name;
 4         public string Name
 5         {
 6             get { return name; }
 7             set
 8             {
 9                 if (string.IsNullOrEmpty(value))
10                 {
11                     throw new ArgumentException("Name cannot be blank!", "Name");
12                 }
13                 name = value;
14             }
15         }
16     }

 

  若是使用了公有的数据成员,那么就需要查找每一处设置客户名称的代码并逐一修复。这将花费大量的时间。

 

  因为属性是使用方法来实现的,所以添加多线程支持也非常简单。

  很容易即可在属性的get和set访问器中作如下的修改,从而支持对数据的同步访问:

 1     public class Customer
 2     {
 3         private object syncHandle = new object();
 4 
 5         private string name;
 6         public string Name
 7         {
 8             get
 9             {
10                 lock (syncHandle)
11                 {
12                     return name;
13                 }
14             }
15             set
16             {
17                 if (string.IsNullOrEmpty(value))
18                 {
19                     throw new ArgumentException("Name cannot be blank!", "Name");
20                 }
21                 lock (syncHandle)
22                 {
23                     name = value;
24                 }
25             }
26         }
27     }

 

  属性可以拥有方法的所有语言特性。例如,属性可以为虚的(virtual):

1     public class Customer
2     {
3         public virtual string Name
4         {
5             get;
6             set;
7         }
8     }

  注意,上述例子中使用了C# 3.0中的隐式属性语法。使用属性来封装私有字段是一个常用的模式。

  通常而言,我们并不需要验证属性的getter或setter逻辑。

  因为语言本身提供了简化的隐式属性语法,力求尽量降低开发人员的输入工作,即将一个简单的字段暴露或属性。

  编译器将为你创建一个私有的成员字段,并自动生成最简单的get和set访问器的逻辑。

 

  你还可以将属性声明为抽象的(abstract),以类似隐式属性语法的形式将其定义在接口中。

  下面的例子就将属性定义在了一个泛型接口中。

  需要注意的是,虽然其语法和隐式属性完全相同,但是编译器却不会自动生成任何实现。

  接口只是定义了一个契约,强制所有实现了该接口的类型都必须满足。

 1     public interface INameValuePair<T>
 2     {
 3         string Name
 4         {
 5             get;
 6         }
 7 
 8         T value
 9         {
10             get;
11             set;
12         }
13     }

 

  属性是一种全功能的、第一等的语言元素,能够一方法调用的形式访问或修改内部数据。成员函数中可以实现的功能均可在属性中实现。

 

  属性的访问器将作为两个独立的方法变异到你的类型中。在C#中,你可以为get和set访问器制定不同的访问权限。

  这样即可更精妙的控制作为属性暴露出来的数据成员的可见性。

 

1     public class Customer {
2         public virtual string Name
3         {
4             get;
5             protected set;
6         }
7     }

  上述属性语法的表达含义圆圆超出了简单数据字段的范畴。

  若类型需要包含并暴露出可索引的项目,那么可以使用索引器(即支持参数的属性);若想反悔序列中的项,创建一个属性会是个不错的做法。

 1     public class User
 2     {
 3         private string name;
 4 
 5         public string Name
 6         {
 7             get { return name; }
 8             set { name = value; }
 9         }
10 
11         // 获取Name中的指定字符
12         public char this[int index]
13         {
14             get
15             {
16                 if (index < 0)
17                 {
18                     throw new ArgumentException("Index must be more than 0!", "index");
19                 }
20                 return name[index];
21             }
22         }
23     }
24     class Program
25     {
26         static void Main(string[] args)
27         {
28             User mrZhang = new User() { Name = "张董" };
29 
30             Console.WriteLine(mrZhang[0]);
31         }
32     }
索引器(支持参数的属性)

  运行结果:

 

  索引器和单一条目属性有着同样的语言支持:他们都是作为方法实现的,因此可以在索引器内部实现任意的验证或计算逻辑。

  索引器也可以为虚的或抽象的,可声明在接口中,可以为只读或读写。

 

  一维且使用数字作为参数的索引器也可以参与数据绑定。使用非整数的索引器可用来定义Map和Dictionary:

1 public Address this[string name]
2 {
3 get { return addressValues[name]; } 4 set { addressValues[name] = value; } 5 }

 

 

  C#中支持多维数组,类似的,我们也可以创建多维索引器,每一个维度上可以使用同样或不同的类型:

1 public int this[int x, int y]
2 {
3     get { return ComputeValue(x, y); }
4 }
5 
6 public int this[int x, string name]
7 {
8     get { return ComputeValue(x, name); }
9 }

 

  需要注意的是,所有的索引器都是用this关键字声明,C#不支持为索引器命名。

  因此,类型中每个不同的索引器都必须有不同的参数列表,一面混淆。

  几乎属性上的所有特性都能应用到索引器上。索引器也可为虚的或抽象的,可以对setter和getter给出不同的访问限制,不过却不能像属性那样创建隐式索引器。

 

  属性的功能很强大,是个不错的改进。

  但你是不是还在想能不能先用数据成员来实现,而在稍后需要其他各种功能的时候再改成属性呢?

  这看似是个不错的策略,不过实际上却行不通。

  考虑如下这个类的定义:

1 // using public data members, bad practice
2 public class Customer
3 {
4     public string Name;
5     // remaining implementation omitted
6 }

 

  这个类描述一个客户(Customer),包含了一个名称(Name)。你可以使用熟悉的成员表示方法获取或设置该名称:

1 Customer zhangDong = new Customer();
2 zhangDong.Name = "张董";
3 string name = zhangDong.Name;

 

  看似简单直观,你也会认为若是日后将Name改成属性,那么代码也可以无需修改保持正常。

  但这个答案并不是完全正确的。

  属性仅仅是访问时类似于数据成员,这是语法所实现的目的。

  不过属性并不是数据,属性的访问和数据的访问将会生成不同的MSIL(Microsoft Intermediate Language,微软中间语言)指令。

 

  虽然属性和数据成员在源代码层次上是兼容的,不过在二进制层面上却大相径庭。

  这也就意味着,若将某个公有的数据成员改成了与之等同的共有属性,那么久必须重新编译所有用到该共有数据成员的代码。

  C#把二进制程序及作为一等公民看待。该语言本身的一个目标就是支持发布某个单一程序集时,不需要更新整个的应用程序。

  而这个将数据成员改为属性的简单操作却破坏掉了二进制兼容性,也就会让更新单一程序集变得非常困难。

 

  若是查看属性生成的IL,那么你或许会想比较一下属性和数据成员的性能。

  属性当然不会比数据成员访问快,不过也不会比其慢多少。

  JIT编译器将内联一些方法调用,包括属性访问器。当JIT编译器内联了属性访问器时,数据成员和属性的访问效率即可持平。

  即使某个属性访问器没有被内联,其性能差距也实在是微乎其微,仅仅一次函数调用之别而已。只有在某些极端情况下,二者的差距才会有所影响。

  

  在调用方法看,属性虽然是方法,但它和数据却有着类似的概念。这会使你的调用者对属性有着一些潜意识的认识。

  例如,调用者会把属性访问当成是数据的访问。

  不管怎样,二者看上去很像描述性访问器应该满足这些潜意识的预期。

  get访问器不应该有可被观察到的副作用。set访问器会修改状态,用户应该可以看到调用后带来的改变。

 

  调用者也会对属性访问器的性能有着一定的预期。

  属性的访问就像是访问一个数据字段,因此不会与访问数据由太过明显的性能差别。

  属性访问器不应该执行长时间的计算,或进行跨应用的调用(例如执行数据库查询等),或是其他任何与调用者期待不符的耗时操作。

 

  无论何时需要在类型的共有或保护接口中暴露数据,都应该使用属性。你也应该使用索引器来暴露序列或字典。

  所有的数据成员都应该是私有的,没有任何例外。

  这样你就立即得到了数据绑定的支持,也便于日后瑞方法实现的各种修改。

  对于将任何变量封装到一个属性所需的额外输入工作其实不会占用太多时间,而日后若是需要使用属性来更正设计,则会花去大量的时间。

  现在多投入一点点,换来的是今后维护时的更加游刃有余。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值