# 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
**By D.S.Qiu**
尊重他人的劳动, **支持原创,转载请注明出处:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
在 C# 语言里,属性已经是“第一类公民(first-class citizens)”。自从C#1.0的发布以来,多个强化,使得属性有了更多的表现力。你可以使用 getter 和 setter 函数指定不同的访问限制。使用隐式(implicit)属性实现代替数据成员可以极大减少打字的功夫。如果你还是使用 public 变量(variables)在你的类中,请停下来!属性可以暴露你的数据成员像 public 接口(interface)一样,更提供你想要面向对象的封装。属性是语言基本元素,可以和访问数据成员的方式一样访问,但却是用方法实现的。
一个类的有些成员最好表示为数据:客户的名字,一个的坐标(x,y),或者去年的收入。属性是可以让你创建一个表现和数据访问一样又有所有方法的优势的接口。客服端代码可以像访问 public 域(fiedls)访问属性。但是实际是使用方法去定义属性访问的行为。
.NET 框架假定你会使用属性来访问你的 public 数据成员。实际上,在 .NET 框架数据绑定(data binding)类支持属性而不是 public 数据成员。这可以从所有数据绑定类库: WPF , Windows Forms , Web Forms ,和 Silverlight 。数据绑定就是绑定对象(object)到用户界面(user interface)控制上。数据绑定的使用反射(refelction)去查找一个类的已知属性:
```
textBoxCitye.DataBindings.Add("Text",address,"City");
```
上面的代码实现绑定 textBoxCity 的属性 Text 到对象 address 的属性 City 。如果使用 public 数据成员 City 是不能工作的,这些框架类库设计不支持这样的实践。 public 数据成员是一个不好的实践,所以没有被支持。这样的决定只是给出另一个理由——按照正确的面向对象计算取实践。
你也许会说,数据绑定只是应用于那些含有要在用户界面显示的元素的类。但那不意味着属性专门使用在用户界面的逻辑中。你应该使用属性在其他类和结构体(structures)。属性能最快捷地适应因新的需求或行为的改变。你能很快觉得你自定义的类的 name 不能为空。如果你使用 public 属性 Name,你可以很快修复这个问题在一个地方:
```
public class Customer
{
private string name;
public string Name
{
get { return name; }
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException(
"Name cannot be blank",
"Name");
name = value;
}
// More Elided.
}
}
```
如果你使用 public 数据成员,你会很困难地去找遍你的程序,找到每一处并且修复。这花费了太多时间,太多时间了。
因为属性是用方法实现的,增加多线程的支持是很容易的。你可以加强 get 和 set 访问器(accessors)的实现来提供数据访问的同步。
```
public class Customer
{
private object syncHandle = new object();
private string name;
public string Name
{
get
{
lock (syncHandle)
return name;
}
set
{
if (string.IsNullOrEmpty(value))
throw new ArgumentException(
"Name cannot be blank",
"Name");
lock (syncHandle)
name = value;
}
}
// More Elided.
}
```
属性有所有方法的语言特性。属性可以是 virtual :
```
public class Customer
{
public virtual string Name
{
get;
set;
}
}
```
你也许注意到上一个例子使用 C# 3.0 的隐式属性语法。创建属性包裹一个备份存储(backing store)是一个常见的模式。一般,你不需要验证逻辑在属性的 getters 或 setters 。 C# 语言支持最简化的隐式属性语法来极大减少需要作为属性来暴露简单域的打字功夫。编译器会创建一个 private 成员域(通常被称作备份存储)并且实现 get 和 set 放弃的逻辑。
你可以使用简化的隐式属性拓展属性为 abstract 和定义属性作为接口的一部分。下面的例子演示了在泛型接口(generic interface)定义了属性。可以注意到下面定义的接口没有包含任何实现并且语法和隐式属性是一致的。每个实现这个接口的类都要实现这个规范。
```
public interface INameValuePair<T>
{
string Name
{
get;
}
T Value
{
get;
set;
}
}
```
属性是全面的(full-fledeged),语言第一类元素:扩展方法以访问或修改内部数据。任何成员函数能做的,你都可以使用属性。
属性的两个访问器会被编译成类的两个单独的方法。在你的 C# 程序中可以指定 get 和 set 访问器不同访问修饰符。这让你可以很好控制通过属性暴露数据成员的可访问性。
```
public class Customer
{
public virtual string Name
{
get;
protected set;
}
// remaining implementation omitted
}
```
属性的语法扩展远超过简单的数据域。如果你的类包含可索引项(indexed items)作为它的接口,你可以使用索引器(indexers)(参数化属性(parameterized properties))。创建属性实现返回序列(sequence)中的项是非常有用的:
```
public int this[int index]
{
get { return theValues[index]; }
set { theValues[index] = value; }
}
// Accessing an indexer:
int val = someObject[i];
```
索引器能和单一项(single-item)属性一样被语言全面支持:索引器可以像你自己写的方法一样实现,里面可以应用你的校验和计算。索引器可以是 virtual 或 abstract ,可以在接口内声明,并且可以是只读或读和写。参数是数字的一维索引器可以参与数据绑定。其他使用非整数参数的索引器用来定义 maps 和 dictionaries :
```
public Address this[string name]
{
get { return adressValues[name]; }
set { adressValues[name] = value; }
}
```
为了和多维数组保持一致,你可以创建多维索引器,在不同轴(axis)使用相同或不同类型:
```
public int this[int x, int y]
{
get { return ComputeValue(x, y); }
}
public int this[int x, string name]
{
get { return ComputeValue(x, name); }
}
```
值得注意的是所有索引器必须使用 this 关键字声明。在 C# 中你不能自己命名索引器。所以一个类型的索引器必须有不同的参数列表来避免歧义。几乎所有的属性的功能都适用索引器。索引器可以是 virtual 或 abstract ;索引器的 setters 和 getters 可以不同的访问限制。不过,你不能像创建隐式属性一样创建隐式索引器。
属性的功能都非常好,是一个非常不错的改进。如果你还蠢蠢欲动使用数据成员的初始实现,然后等到你需要使用到属性的优势的时候,再用属性替换数据成员。这听起来像是一个合理的策略——但这是错误的。考虑下面的类的定义:
```
// using public data members, bad practice:
public class Customer
{
public string Name;
// remaining implementation omitted
}
```
类 Customer 有一个数据成员 Name 。你可以使用熟悉的成员记号(member notation) get 或 set 这个 Name :
```
string name = customerOne.Name;
customerOne.Name = "This Company, Inc.";
```
这个简单有直观。你会觉得你以后使用属性替换数据成员 Name ,而且代码不做改动能照常工作。好吧,确实是那样的。属性就是访问起来跟数据成员一样。这个语法的目的就在于此。但是属性不是数据。属性访问和数据访问会产生不同的微软中间语言(Microsoft Intermediate Language)指令。
尽管属性和数据成员在代码上兼容的。但在二进制上是不兼容的。一个很明显的案例,当你把一个 public 数据成员改为等同的 public 属性,意味着你要重新编译所有使用这个 public 数据成员的代码。 C# 把二进制程序集(assemblies)看做“第一类公民”。语言的一个目的就是你可以发布单一更新的程序集,而不要更新整个应用。把数据成员改为属性这么简单的行为却破坏二进制的兼容性。这个行为使得部署更新单一程序集变得更困难。
如果你看了 IL 的属性实现,你可能很想知道属性和数据成员的相对性能。属性并不比数据成员快,但也不会慢多少。 JIT 编译器对属性的访问器做了 inline 的优化。如果 JIT 编译器做了 inline 属性访问器优化, 数据成员和属性的性能是一样的。即使没有属性访问器没有置为 inline ,性能差别也只是可以忽略不计的一个函数调用话费。这差别只有在少数情况下才可以被测量出来。
属性是像数据一样被调用的方法。调用者就会有一些访问权限期望。他们把访问属性当做数据访问。毕竟,那就是属性。你的属性访问器要能满足这些期望。 Get 访问器不能有其他作用。 Set 访问器改变了状态,调用者要能看到改变。
属性访问器同时满足使用者的性能期望。属性访问跟数据域访问很类似。这不能导致属性和简单数据访问有着显著不同的性能特点。属性访问不能在长计算,或跨平台调用(比如执行数据库查询),或其他长操作和使用者期望保持一致。
无论什么时候,你要在类中暴露数据作为 public或 protected 接口,请使用属性。对于序列或 dictionaries 是使用索引器。所有数据成员应该无一例外地使用 private 修饰符。你会立即在数据绑定得到支持,而且在以后会很容易方法中的实现。把变量封装在属性里所有的打字功夫加起来就一到两分钟。你会发现在使用属性替换之前的设计会花费数小时。现在花一点时间,以后节约你大量时间。
小结:
万事开头难,终于写下了翻译 Effective C# : 50 Specific Ways to Improve Your C# 2nd Edition 的第一篇,花的时间有点心疼,感觉颈椎都熬出问题了(太专注了)。之前工作都没觉得什么,看来还是工作不要那么专注,以后把时间挤出来干这个,加油!发现最后一句话说的蛮好的:Spend a little time now, and save yourself lots of time later. 分享给大家.....
因为是第一次翻译,欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!
如果您对D.S.Qiu有任何建议或意见可以在文章后面评论,或者发邮件(gd.s.qiu@gmail.com)交流,您的鼓励和支持是我前进的动力,希望能有更多更好的分享。
转载请在 **文首** 注明出处:[http://dsqiu.iteye.com/blog/1976256](/blog/1976256)
更多精彩请关注D.S.Qiu的博客和微博(ID:静水逐风)
- 第一章 C# 语言习惯
- 原则1:使用 属性(Poperty)代替可直接访问的数据成员(Data Member)
- 原则2:偏爱 readonly 而不是 const
- 原则3:选择 is 或 as 而不是强制类型转换
- 原则4:使用条件特性(conditional attribute)代替 #if
- 原则5:总是提供 ToString()
- 原则6:理解几个不同相等概念的关系
- 原则7:明白 GetHashCode() 的陷阱
- 原则8:优先考虑查询语法(query syntax)而不是循环结构
- 原则9:在你的 API 中避免转换操作
- 原则10:使用默认参数减少函数的重载
- 原则11:理解小函数的魅力
- 第二章 .NET 资源管理
- 原则12:选择变量初始化语法(initializer)而不是赋值语句
- 原则13:使用恰当的方式对静态成员进行初始化
- 原则14:减少重复的初始化逻辑
- 原则15:使用 using 和 try/finally 清理资源
- 原则16:避免创建不需要的对象
- 原则17:实现标准的 Dispose 模式
- 原则17:实现标准的 Dispose 模式
- 原则18:值类型和引用类型的区别
- 原则19:确保0是值类型的一个有效状态
- 原则20:更倾向于使用不可变原子值类型
- 第三章 用 C# 表达设计
- 原则21:限制你的类型的可见性
- 原则22:选择定义并实现接口,而不是基类
- 原则23:理解接口方法和虚函数的区别
- 原则24:使用委托来表达回调
- 原则25:实现通知的事件模式
- 原则26:避免返回类的内部对象的引用
- 原则27:总是使你的类型可序列化
- 原则28:创建大粒度的网络服务 APIs
- 原则29:让接口支持协变和逆变
- 第四章 和框架一起工作
- 原则30:选择重载而不是事件处理器
- 原则31:用 IComparable&lt;T&gt; 和 IComparer&lt;T&gt; 实现排序关系
- 原则32:避免 ICloneable
- 原则33:只有基类更新处理才使用 new 修饰符
- 原则34:避免定义在基类的方法的重写
- 原则35:理解 PLINQ 并行算法的实现
- 原则36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原则37:构造并行算法的异常考量
- 第五章 杂项讨论
- 原则38:理解动态(Dynamic)的利与弊
- 原则39:使用动态对泛型类型参数的运行时类型的利用
- 原则40:使用动态接收匿名类型参数