c# 学习笔记 —— 第五章 类

C# 学习笔记 —— 第 5 章 类

5.1 类的定义和实例化

对比java:定义类

  • 一般应该将每个类放到自己的文件中,用类名为文件进行命名,并非必须。
  • 不要在一个源代码文件中放置多个类,并非必须。
  • 要用所含有的公共类型名称来命名源代码文件。

实例化对象

new 操作符指示运行时为对象分配内存、实例化对象,并返回实例的引用。

对比cpp:delete操作符

  • 程序员应该将new的作用理解成为实例化对象而非分配内存,c#中在堆上和栈上分配的内存都支持new操作符。
  • c#的内存分配和回收由运行时自动进行,不需要delete操作符。
  • c#不支持c++的隐式确定性内存清理(在编译时确定的位置进行清理)。但通过using语句支持显式确定资源清理,通过终结器支持隐式非确定性资源清理。

5.2 实例字段

  • 成员变量在c#中称为字段,它是与包容类型关联的具名储存单元。
  • 实例字段是在类的级别上声明的变量,用于储存与对象关联的数据。
  • 关联(association)是字段类型与包容类型之间的联系。

5.2.1 实例字段的声明

5.2.2 实例字段的访问

  • 实例字段没有关键字static,意味着只能从类的实例访问,而不能从类访问。

5.3 实例方法

  • 静态方法不能直接访问类的实例字段,必须获取类的实例才能调用实例成员。

5.4 使用this关键字

在c#中,为了显式的指出字段、方法是包容类的实例成员,可以使用关键字this。调用任何实例成员时this都是隐式的,放回对象本生的实例。

  • 使用this可以避免歧义,然而依靠合适的编码风格和遵守编码规范避免歧义是更好的选择。
  • 向其它第三方传递对象实例本身需要使用this。

5.5 访问修饰符

  • c#的字段可以使用5种访问修饰符:public、protected、internal、protect internal、private
  • c#的类和cpp的class一样,默认访问修饰符为private

5.6 属性

希望实现某种并非非常彻底的封装,可以将字段标记为私有,提供取值和赋值方法来访问和修改数据。

5.6.1 属性的声明

考虑到经常会用到属性的编程模式,c#的设计者决定为之提供显式的语法支持,将这种语法称之为“属性”

  • 属性的关键特点在于提供了在编程角度看似字段的API,但事实上并不存在这样的字段。
  • 属性声明看似字段声明,属性的实现由两个可选的部分set和get组成。

5.6.2 自动实现的属性

c#3.0以后,编译器允许在声明属性时,不添加取值或者赋值方法,也不声明任何支持字段,一切都自动实现。

c#6.0中,可以像字段一样初始化属性

5.6.3 属性和字段的设计编码规范

  • 在面临属性和方法的选择时,一般的编码规则是:
    • 方法代表行动
    • 属性用于代表数据:属性倾向于对简单的计算提供简单的访问,调用属性的代价不应当比调用字段高出太多。
  • 对于为属性提供支持的私有字段,常见的命名规范有:_FirstName,_firstName,m_FirstName,firstName(但属性应当尽量避免这种局部变量和参数也经常使用的风格)
  • 为了符合封装的原则,字段不应当被声明为public和protected
  • 避免从属性的取值方法中引发异常,要在属性引发异常时保留原始属性值。
  • 如果没有额外实现的逻辑,要使用自动实现的属性,而非带有简单支持字段的属性。
  • 不要为属性和支持字段使用双下划线前缀,这是为编译器保留的。
  • 要使用名词,名词短语或者形容词来命名属性。
  • 考虑让属性和他的类型同名。
  • 如果有用的话,要为布尔值属性附加Is,Can,Has前缀

5.6.4 提供属性验证

  • 避免从属性外部(即使是在包容属性的类中)直接访问属性的支持字段。
  • ps:在c#6.0中,throw new ArgumentException("LastName cannot be blank","value");的语法被throw new ArgumentException("LastName cannot be blank",nameof(value));替代
    • 某一类异常带一个string类型的实参paraName。用于标识无效的参数名称。
    • nameof操作符带一个标识符,返回对标识符名称的字符串表示。

5.6.5 只读属性和只写属性

  • 通过移除某个属性的取值方法或者赋值方法部分,可以改变属性的可访问性。
  • 只提供取值方法会得到只读属性;在字段被标记为只读的时候会引发异常,因为只能在构造器中构造这个值;在c#6.0中,可以直接给一个只读属性赋值,而不用考虑对只读字段的要求。
    • 在c#6.0之前,通过构造函数而非属性给只读字段赋值。后者会引发编译错误
    • 在c#6.0忠厚,创建只读的自动实现的属性可以通过属性初始化。

5.6.6 属性作为虚字段使用

属性的行为与虚字段相似,有时候甚至不需要字段,而让属性的取值方法返回计算好的值,让赋值方法解析值,将他间接的存储到其它成员的字段中。

5.6.7 为取值方法和赋值方法指定访问修饰符

  • c#1.0中,set和get的访问级别必须一致,而2.0中可为两者部分指定(不能全部)以覆盖属性的访问修饰符。
  • 覆盖的修饰符必须比属性的修饰符更加严格

5.6.8 属性和方法调用不允许作为ref或者out参数值使用

原理:ref和out的内部实现是通过传递地址实现的,但是可能属性是没有支持字段的虚字段,也有可能是只读的或者只写的,因此不能作为ref或者out传递。

5.7 构造器

5.7.1 构造器的声明

为了纠正可能对实例化而未初始化的对象赋值的问题,提供一种在创建对象时指定必须的数据,可以用构造器实现。

  • 构造器和cpp,java的构造器形式一致。
  • 字段声明时的赋值发生在构造器构造之前,构造器赋值会覆盖字段声明时的赋值;和java一致。

5.7.2 默认构造器

没有显式定义的构造器,编译器会自动在编译时添加默认构造器,在存在其他构造器时则不生成默认构造器,和cpp,java一致。

5.7.3 对象初始化器

c#3.0中新增了对象初始化器,在调用构造器生成对象时,可以在后面的一对大括号中添加可访问字段和属性的初始化列表,这是一种对他们一次性简单赋值的语法糖,和cpp的对象初始化列表相似。

  • 构造器退出时,所有的属性都应该初始化成为合理的默认值。
  • 利用属性方法的校验逻辑,可以制止将无效的数据赋值给属性。
  • 当一个对象的某一个或多个属性的无效值导致其它本对象的属性产生无效值的时候,应该推迟为无效状态引发异常到无效的、不相关的属性变得有效、相关。

高级主题:集合初始化器

采用和对象初始化器相似的语法,c#3.0新增了集合初始化器

像这样为新的集合实例赋值,编译器生成的代码会按顺序实例化并通过Add()方法将他们添加到集合中。

高级主题:终结器

因为c#运行时只能自动的清理内存的垃圾,为了实现其他类型的资源释放,定义在对象销毁中发生的事情,c#提供了终结器。

  • 与cpp的析构器不同,终结器并不是在对一个对象的所有引用都消失之后马上运行。相反,终结器是在对象被判定为“不可达”之后的不确定性消失。
  • 垃圾回收器会在一次垃圾回收过程中判断出带有终结器的对象,但不是立即回收,而是将他们添加到一个终结队列中。一个独立的线程遍历终结队列的每一个对象,调用其终结器,然后将其从终结队列中删除。

5.7.4 构造器的重载

  • 应当优先使用可选参数而不是重载,一边在API中清楚地看出默认属性的“默认值”。
  • 使用构造器参数来设置属性,应当使用和属性相同的名称,仅仅在大小写风格上不同。

5.7.5 构造器链:使用this调用另一个构造器

  • 用法与cpp相似,语法略有不同
  • 通常采用相反的模式,参数最少的构造器调用参数最多的构造器,为未知的参数传递默认值。
  • 不能在上下文不存在相关变量的情况下,调用相关构造器

高级主题:匿名类型

c#3.0引入了对匿名主题的支持,它是由编译器动态生成的数据类型。

编译器遇到匿名类型的语法时,会自动生成一个CIL类,该类具有与匿名类型中已经命名的值和数据类型对应的属性,该类是静态类型。

  • 若向其中的一个名称赋值,从而即可显式的标识匿名类型的成员名称,如果所赋的值是字段或者属性,则在没有明确指明名称的情况下,则会自动使用字段或者属性的名称。
  • 除非:使用Lambda表达式、查询表达式关联来自不同类型的数据,或者对数据进行水平投射。否则一般情况下应当避免使用这样的声明,甚至减少var来声明隐式类型的变量。

5.8 静态成员

5.8.1 静态字段

为了定义能由多个实例共享的数据,需要使用static关键字。

  • 包含static修饰符的字段称之为静态字段,一个类的所有实例的静态变量共享一个静态变量的储存位置。
  • 实例字段和静态字段都可以在声明的同时进行初始化。
  • 静态字段未初始化的情况下将获得和类型对应的初始值,和cpp类似。非静态字段(类内变量,cpp称),不会自动初始化。
  • 设置和获取静态字段的初始值要使用类名,而不是对类型的实例的引用。
  • c#禁止静态字段和实例字段同名。

5.8.2 静态方法

  • 静态方法不通过实例引用,故this关键字在静态方法中无效。
  • 要在静态方法中获得实例字段,必须先获得对实例对象的引用。

5.8.3 静态构造器

c#支持静态构造器,它不显式调用,而是在“运行时”首次访问类时自动调用,“首次访问”发生在首次调用普通构造器时,也可能发生在访问类的静态方法或者字段时。不允许显式调用,所以不允许任何参数。

不要在静态构造器中引发异常,这会造成类型在应用程序剩余的生存期内无法使用。

高级主题:最好在声明的同时完成静态初始化

  • 有时对静态成员初始化代价比较高,而且在访问之前确实没有必要进行初始化,所以这个设计能带来一定程度上性能的改进,应该考虑以内联方式初始化静态字段,不使用静态构造器,也不一定要在声明时初始化。

5.8.4 静态属性

使用静态属性几乎肯定比使用公共静态字段好。

5.8.5 静态类

  • 不包含任何实例字段(或者方法)的类可以用static修饰,防止创建实例;编译器标记为abstract和sealed,将类指定为不可拓展,不能派生出其他类。
  • using static指令用于静态类,可以不用前缀调用静态类方法。

5.9 拓展方法

c# 3.0 引入了拓展方法的概念,能模拟为不同的类创建实例方法。只需要更改静态方法的签名,使第一个参数成为想要拓展的类型,并在类型名称后附加this关键字

这个改进使得我们能为任何类添加实例方法,包括那些不在同一个程序集中的类,拓展方法作为普通静态方法调用:

  • 第一个参数是要被拓展或者要操作的类型,称为“被拓展的类型”
  • 为了指定拓展方法,在要被拓展的类型前面加this修饰符
  • 要将方法作为拓展方法使用,要用using方法导入“拓展类型”的命名空间,使得调用方法和拓展类型位于一个命名空间之中。

如果拓展方法的签名和被拓展类型之中的签名匹配,则拓展方法永远不会得到调用。而且使用继承来特化类型,要优于使用拓展方法。

5.10 封装数据

5.10.1 封装数据

与java中的常量类似,绝大数时候时候是cpp中的单层const:

  • 常量字段通常只使用包含字面值的类型。
  • 常量字段会自动成为静态字段,但是将常量字段显式的生命为静态变量会引发编译错误。
  • 要为永远不变的值使用常量字段。
  • const变量只能在编译时确定。

高级主题:public常量应该是恒定值

public常量应该恒定不变,因为如果对他进行了修改,在使用它的程序集中不一定能反映出最新的改变:如果一个程序集引用了另一个程序集的常量,将直接将这个值编译到引用程序集中,如果被引用程序集中的这个值发生改变,而引用程序集没有重新编译,那么引用程序集将继续使用原始值。

5.10.2 readonly

与const不同,readonly修饰符只能用于字段,它指出该字段只能从构造器中、或是在声明时通过初始化器进行修改。

  • 每个实例的readonly变量都可以不同。
  • 既可以是实例字段也可以是静态字段。
  • 可以在执行时为readonly字段赋值。
  • 不限于包含字面值的类型。

因为不能从包容属性外部访问属性支持的字段,故在c#6.0中,应该使用自动实现的只读属性代替readonly修饰符。

无论是只读的自动实现或者是readonly修饰符,提供数组引用的不变性都是很有必要的编码技巧,因为它顶层的特性,将保证可修改元素,但不能修改数组的体制本身,因而不会将新数组赋给成员,丢弃已有的数组。

5.1 嵌套类

在类中不仅可以定义字段或者方法,还可以定义另外一个类——称之为嵌套类:假如一个类在它的包容类外部没有多大意义,就把它设计成为嵌套类。

  • 嵌套类的独特之处是可以把其自身声明为private,以表示只为嵌套类服务
  • 嵌套类的this成员代表嵌套类而不是包容类的实例,想要访问包容类的实例,需要显式的传递包容类的实例。
  • 嵌套类可以访问包容类的私有方法,包容类只能访问嵌套类的共有方法。
  • 要从包容类外部引用,就不能声明成为嵌套类。
  • 避免声明公共类型嵌套。

语言对比:java内部类

java不仅支持嵌套类,还支持内部类(inner class),对应的是同包容类实例关联的对象,而不仅仅是和包容类有一个语法上的包容关系。在c#中,可以在外层类中包含嵌套类型的一个实例字段,从而得到相同的结构。一个工厂方法或者构造器可以确保在内部类的实例中,也会设置对外部类的相应实例的引用。

5.12 分部类

c#新蹭了名为分部类的语言特性:对于以WPF(xaml)的语言生成工具作用巨大:可以在一个文件中进行分部,也可以在多个文件中进行分部。

5.12.1 定义分部类

在c#2.0以上版本中的class之前使用partial关键字声明分部类,类似于cpp中

的作用

  • 分部类常用于代码生成器生成的代码中。
  • 分部类常用于将每个嵌套类的定义都放到他们自己的文件中,从而符合编码规范。
  • 分部类不允许对编译好的类(或者其他程序集中的类)进行拓展,而是只能在同一个程序集中进行分拆。

5.12.2 分部方法

从c#3.0开始,设计者引入了分部方法的概念。

  • 分部方法只能存在于分部类中。
  • 分部方法不需要在partial前缀,而是作为一部分中的工具方法存在,被另一部分的方法调用。
  • 由于不知道是否提供实现可能导致无法成功的返回,分部方法只能声明为void返回类型。
  • out在分部方法中禁止使用,只能使用ref参数来间接的返回值。
  • 如果没有为分部方法提供实现,则CIL中不会出现任何分部方法的踪迹。
Tagged with:

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据