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

简介:   属性一直是C#语言中的一等公民。自1.0版本以来,C#对属性进行了一系列的增强,让其表达能力不管提高。你甚至可以为setter和getter指定不同的访问权限。   隐式属性也极大降低了声明属性时的工作量,不会比声明数据成员麻烦多少。

 

  属性一直是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访问器会修改状态,用户应该可以看到调用后带来的改变。

 

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

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

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

 

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

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

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

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

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

 

【来自:张董'Blogs:http://www.cnblogs.com/LonelyShadow,转载请注明出处。】

亲们。码字不容易,觉得不错的话记得点赞哦。。

目录
相关文章
|
22天前
|
开发框架 .NET Java
C#集合数据去重的5种方式及其性能对比测试分析
C#集合数据去重的5种方式及其性能对比测试分析
34 11
|
24天前
|
开发框架 .NET Java
C#集合数据去重的5种方式及其性能对比测试分析
C#集合数据去重的5种方式及其性能对比测试分析
49 10
|
30天前
|
存储 安全 编译器
学懂C#编程:属性(Property)的概念定义及使用详解
通过深入理解和使用C#的属性,可以编写更清晰、简洁和高效的代码,为开发高质量的应用程序奠定基础。
93 12
|
3月前
|
JSON C# 开发者
C#语言新特性深度剖析:提升你的.NET开发效率
【10月更文挑战第15天】C#语言凭借其强大的功能和易用性深受开发者喜爱。随着.NET平台的演进,C#不断引入新特性,如C# 7.0的模式匹配和C# 8.0的异步流,显著提升了开发效率和代码可维护性。本文将深入探讨这些新特性,助力开发者在.NET开发中更高效地利用它们。
51 1
|
3月前
|
存储 开发框架 .NET
C#语言如何搭建分布式文件存储系统
C#语言如何搭建分布式文件存储系统
97 2
|
3月前
|
SQL 缓存 分布式计算
C#如何处理上亿级数据的查询效率
C#如何处理上亿级数据的查询效率
64 1
|
3月前
|
中间件 数据库连接 API
C#数据分表核心代码
C#数据分表核心代码
54 0
|
2月前
|
C# 开发者
C# 一分钟浅谈:Code Contracts 与契约编程
【10月更文挑战第26天】本文介绍了 C# 中的 Code Contracts,这是一个强大的工具,用于通过契约编程增强代码的健壮性和可维护性。文章从基本概念入手,详细讲解了前置条件、后置条件和对象不变量的使用方法,并通过具体代码示例进行了说明。同时,文章还探讨了常见的问题和易错点,如忘记启用静态检查、过度依赖契约和性能影响,并提供了相应的解决建议。希望读者能通过本文更好地理解和应用 Code Contracts。
47 3
|
2月前
|
设计模式 C# 图形学
Unity 游戏引擎 C# 编程:一分钟浅谈
本文介绍了在 Unity 游戏开发中使用 C# 的基础知识和常见问题。从 `MonoBehavior` 类的基础用法,到变量和属性的管理,再到空引用异常、资源管理和性能优化等常见问题的解决方法。文章还探讨了单例模式、事件系统和数据持久化等高级话题,旨在帮助开发者避免常见错误,提升游戏开发效率。
86 4
|
4月前
|
API C#
C# 一分钟浅谈:文件系统编程
在软件开发中,文件系统操作至关重要。本文将带你快速掌握C#中文件系统编程的基础知识,涵盖基本概念、常见问题及解决方法。文章详细介绍了`System.IO`命名空间下的关键类库,并通过示例代码展示了路径处理、异常处理、并发访问等技巧,还提供了异步API和流压缩等高级技巧,帮助你写出更健壮的代码。
61 2