OOP 以类型为基础进行软件构建。类型是由一组数据和子程序构成的集合,这些数据和子程序共同拥有一组内聚的、明确定义的职责。类型可以只是由一组子程序构成的集合,这些子程序提供了一组内聚的服务,哪怕其中并未涉及共用的数据。成为高效程序员的关键是——当你开发的程序时,能否安全地忽视程序中尽可能多的其余部分。而类型就是实现这一目标的工具。
C#作为一种面向对象开发语言,它为我们提供了强大的抽象能力。其中 class、struct 为我们提供了对现实世界的抽象;interface 提供了对 class、struct 的抽象;delegate 提供了对方法的抽象;attribute 提供了对类型元数据的抽象;generics 给上述因素进一步、进两步、直至进 n 步的抽象机会。
结构与类为我们提供了对现实世界抽象的能力,是我们最常使用的类型。它们实际上都是创建对象的模板,在很多地方它们都存在着共性,本文中对“类”的认识往往也适用于“结构”。所以我只说明了类与结构的选择问题,因为类比结构更常用,所以本文剩余部分都是以类来说明的,但其观点同样适用于结构。
一 何时使用类,何时使用结构
在类和结构之间的选择时我们要知道一些知识:
何时定义结构:
在其它情况下,应该将类型定义为类。
二 创建类的原因与应该避免的类
如果你认为创建的唯一理由是对现实世界中的物体建模,那么你的想法就是片面的。创建类的理由远远不止一个。
应该避免的类:
三 类型的各种成员
一个类型可以有两种特性:积累的和非积累的。积累特性是指一个类型中的每个部分都可以添加的东西,例如接口派生、属性、索引器、方法和成员变量。非积累特性是指类型中的每个部分必须一致的东西,如类型是一个类或一个结果,类型的可访问性,以及基类。在一个类型中,可以包容如下成员:
四 类型的各种修饰符
C# 支持的类型修饰符如下:
类型修饰符 | 修饰类 | 修饰结构 | 修饰成员 |
public | 公有,没有访问限制 | 公有,没有访问限制 | 公有,没有访问限制 |
private | 私有,只能由定义它们的类型进行访问 | 不支持 | 私有,只能由定义它们的类型进行访问 |
protected | 受保护,可以被定义它们的类型或者派生类来访问 | 不支持 | 受保护,可以被定义它们的类型或者派生类来访问 |
internal | 在当前程序集中访问 | 在当前程序集中访问 | 在当前程序集中访问 |
abstract | 定义抽象类 | 不支持 | 定义抽象方法 |
partial | 拆分类 | 拆分结构 | 拆分方法 |
sealed | 定义密封类 | 不支持 | 方法和属性 |
static | 定义静态类 | 不支持 | 成员不在类的具体实例上执行 |
new | 不支持 | 不支持 | 隐藏继承的成员 |
virtual | 不支持 | 不支持 | 成员可以由派生类重写 |
override | 不支持 | 不支持 | 重写继承的虚拟或抽象成员 |
readonly | 不支持 | 不支持 | 定义为只读 |
const | 不支持 | 不支持 | 定义为常量 |
extern | 不支持 | 不支持 | 仅静态[DllImport]方法 |
volatile | 不支持 | 不支持 | 修饰类或结构的字段,表示该字段可以由多个同时执行的线程修改 |
下面分组介绍各种修饰符的用法。
(一) 访问修饰符
C# 访问修饰符如下:
CLR 术语 | C# 访问修饰符 | 应用场景 | 描述 |
Public | public | 类型、类型成员、结构 | 没有限制 |
Private | private | 类型成员、嵌套类型 | 只能由定义它们的类型进行访问 |
Family | protected | 类型成员、嵌套类型 | 对象变量不能直接访问,可以被定义它们的类型或者派生类来访问 |
Assembly | internal | 类型、类型成员、结构 | 在当前程序集中访问 |
Family or Assembly | protected internal | 类型成员、嵌套类型 | 在定义它们的程序集、类型以及派生类型中可用 |
1 类型的可见性
默认情况下,类型是 internal 的。C# 支持有元程序集。可以使用 System.Runtime.CompilerServices 命名空间中定义的一个名为 InternalsVisibleTo 的特性来标明它认为是“友元”的其它程序集。当确认了自己的友元程序集之后,那些友元程序集就能访问该程序集中的所有 internal 类型,以及类型的 internal 成员。
声明一个友元程序集可能轻易被滥用,妨碍程序集内部的本质封装,增加耦合度。
注意:“友元程序集”功能只适合发布时间相同的程序集,最好是打包到一起发布。这是因为友元程序集的相互依赖程度高,所以间隔时间过长易引起兼容性问题。
2 成员的可访问性
默认情况下,成员是 private 的。一个派生类型重写在它的基类型中定义的一个成员时,C# 编译器要求原始成员与重写成员具有相同的可访问性。但是,CLR 并没有强制要求。CLR 允许放宽成员的可访问性,但不允许紧收。这样做是因为,CLR 承诺派生类总是可以转型为基类,并获取对基类方法的访问权。
(二)abstract 修饰符
abstract 修饰符可以和类、方法、属性、索引器和事件一起使用。
抽象类和抽象成员具有以下特征:
(三)partial 修饰符
partial 是一个纯设计时结构,只有方法、类、结构或接口才能为分部形式。partial 表明我们可以在同一项目中使用相同的类签名在其它位置对该类进行扩展。分部方法保留了方法的签名,并让我们自己决定是否实现,即我们可以根据需要决定是否向该类添加行为。所有类都可以通过分部类机制进行扩展。
分部形式具有以下特点:
分部形式适用于以下情况:
(四)sealed 修饰符
密封类和密封的方法或属性不能被扩展,主要为了防止他人进行扩展。
密封具有以下特点:
(五)static 修饰符
static 用于定义静态类和静态成员,可用于类、字段、方法、属性、运算符、事件和构造函数,但不能用于索引器、析构函数或类以外的类型。静态类和静态成员用于创建无需创建类的实例就能够访问的数据和方法。静态类用于定义独立于特定对象的方法。静态成员可用于描述独立于对象实例或者说所有对象的交集部分——这些数据和方法,不随新对象的创建而改变。
静态类与静态成员的特点:
(六)new 修饰符、virtual 修饰符、override 修饰符
virtual 用于修饰方法、属性、索引器或事件,虚成员可以在派生类中被重写。override 用于重写继承的虚拟或抽象成员。new 隐藏继承的成员。
new 与 override 的区别在于——new 会隐藏继承的成员,并新建一个独立于基类的成员,而 override 则是重写基类的虚方法。
(七)readonly 修饰符与 const 修饰符
const 用于修饰字段和局部变量,将其定义为常量。readonly 比 const 更为灵活,用于定义只读字段。只读字段可以是实例字段,这样类的每个实例可以有不同的值,也可以定义为静态字段。
const 与 readonly 的区别在于:
(八)extern 修饰符与 volatile 修饰符
extern 修饰在外部实现的方法,常与[DllImport] 特性一起使用,此时,其修饰的方法必须为 static。
volatile 修饰符指示一个字段可以由多个同时执行的线程修改,看到访问这种字段的代码,编译器、CLR 或硬件就不会执行一些“线程不安全”的优化措施。
五 类型设计
(一) 命名规范
1 类型、结构的命名
我们命名类和结构时应该劲量达到两个目的:
一般的命名规范如下:
基类 | 派生类 |
System.Attribute | 要给自定义的特性添加“Attribute”后缀 |
System.Delegate | 要给用于事件处理的委托添加“EventHandler”后缀 要给用于事件处理之外的那些委托添加“Callback”后缀 不要给委托添加“Delegate”后缀 |
System.EventArgs | 要添加“EventArgs”后缀 |
System.Enum | 不要派生自该类,而是使用 enum 定义枚举,枚举类型也不要添加“Enum”或“Flag”后缀 |
System.Exception | 要添加“Exception”后缀 |
IDictionary IDictionary |
要添加“Dictionary”后缀 |
IEnumerable ICollection IList IEnumerable ICollection IList |
添加“Collection”后缀 |
System.IO.Stream | 添加“Stream”后缀 |
CodeAccessPermission IPermission |
添加“Permission”后缀 |
2 成员的命名
成员命名的一般规范如下:
成员 | 大小写 | 规范 |
方法 | Pascal(公开)、Camel(私有) | 用动词或动词短语命名 |
属性 | Pascal | 用名词、名词短语或形容词来命名 集合属性应该使用复数形式,而不是添加后缀 用“Is”、“Can”、“Has”等表示布尔属性 可以用属性的类型名来命名属性 |
事件 | Pascal | 使用动词或动词短语来命名事件 用现在时和过去时来区分前置和后置事件 |
字段 | Pascal(公开)、Camel(私有) | 要用名词、名词短语或形容词来命名 不要加任何前缀 |
(二) 设计规范
1 抽象类的设计
2 静态类的设计
C# 编译器对静态类进行了如下限制:
使用静态类时应该注意:
3 结构的设计
4 嵌套类型的设计
5 如何防止类型被实例化。
我们有3种方法可以防止类型实例化: