五、类
5.1 基本概念
5.1.1 访问修饰符
- public
- private
- protected
- internal
- protected internal
- 同一程序集内或不同程序集的派生类中可访问
- 类成员的访问修饰符默认为private
5.1.2 this关键字
- 在类的实例成员内部,返回类的实例
5.1.3 类与对象
- 类是模板,用来定义对象,主要是为了将数据进行封装,可以形成一种层次结构以应对复杂多变的问题
- 对象是类的实例,实例方法可以操作实例字段。
5.2 属性·
5.2.1 属性
- 为了使字段能够通过赋值操作符设置数据,引入属性的概念
- 如果不使用属性,则需要getter()、setter()方法才能对读写进行控制
- 写入只能通过调用方法实现,不能直接赋值
- get、set、value为关键字;
- 即使在类内部,也应通过属性对字段进行赋值。
- CIL中内部机制类似java的getter、setter方法
- 类似于字段的API
5.2.2 自动实现的属性
-
常规属性
- 需要支持字段、读写方法
-
自动实现的属性
- 不需要支持字段
- 编译器自动实现支持字段
- 在CIL中有隐藏的支持字段,与显式定义支持字段的属性基本一样
- 不需要支持字段
-
nameof
- 在需要使用标识符名称字符串时使用
- 传入标识符,返回标识符名称字符串
- 标识符名称改变
- 重构工具自动更改nameof的实参
- 无重构工具则强迫手动修改,否则编译无法通过
- nameof(value) vs “value”
5.2.3 只读属性和只写属性
- 设置只读属性
- 初始化
- 声明时初始化属性
- 构造方法直接对字段进行直接赋值
- 初始化
- 不能设置只写属性
5.2.4 属性作为虚字段
- 虚字段
- 属性没有实际对应的支持字段,编译后自动实现的支持字段也没有
- 读取时返回其他属性的合并,写入时对其他属性进行赋值
- 但并不是用virtual修饰。
5.2.5 为取值和赋值方法指定访问修饰符
- 不能同时指定取值和赋值方法的访问修饰符
- 指定的修饰符范围不能超过属性的访问修饰符的范围
- 必须比属性的访问修饰符更严格
5.2.6 属性和方法调用的返回值不能作为ref和out参数值使用
- ref和out需要传递变量的引用
- 属性
- 可能是虚字段
- 可能是只读或只写的
- 方法返回值
- 属性
- 需要先用变量存放属性或方法调用的值作为中间过渡
5.3 构造器
5.3.1 构造器
- 即构造方法
- 当存在构造方法和声明时初始化同时对属性赋值时
- 先调用声明时初始化
- 最后调用构造方法
- 默认构造方法
- 当未显式声明构造方法时,编译器会提供默认构造方法
- 当显式声明了构造方法后,将不会再提供默认构造方法
- 当存在构造方法和声明时初始化同时对属性赋值时
5.3.2 对象初始化器
- 对象初始化器
- 用于初始化所有可访问的字段和属性
- 紧跟在构造器的后面进行初始化
- 例如,new Person( A, B ) { property1=value1,property2=value2 };
- 底层也是通过调用setter进行赋值,语法糖
- 集合初始化器
- 在创建集合时初始化集合,类似对象构造器
new List<Person>(){
new Person(property1,property2),
new Person(property1,property2)
};
- 终结器 ~
- 定义对象销毁时的操作
- 带有终结器的对象在不可到达时会被垃圾回收器放进终结队列中通过独立线程调用终结器并将对象销毁。
5.3.3 构造器链:使用this调用另一个构造器
- 构造器初始化器
- :this (A, B)
- 从一个构造器中调用另一个构造器,减少重复代码——构造器链
- 构造器重载时调用另一个构造器
- this所调用的构造器最先执行
- 常用模式是从参数少的构造器调用参数多的构造器,多的参数采用默认值
public Employee(int id, string firstName, string lastName)
:this(firstName, lastName)
{
Id = id;
}
public Employee(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
- 集中初始化
- 单独出一个方法集中对所有字段初始化
- 重载的构造器都调用该初始化的方法
- 未在参数列表中出现的属性需要进行一系列操作获得
- 初始化方法中只是对属性等进行赋值操作
- 单独出一个方法集中对所有字段初始化
5.4 静态成员
5.4.1 静态字段、方法
- 无法通过实例引用静态成员
- 静态字段、方法的作用
- 类的实例之间能共享数据或操作
5.4.2 静态构造器
- 用于对类而不是类实例进行初始化
- 使用静态构造器而不使用内联的方式在声明时初始化的原因
- 需要通过计算才能获得初始值
- 使用静态构造器而不使用内联的方式在声明时初始化的原因
- static person(){}
- 不能显式调用,没有输入参数。
- 静态构造器会在首次访问类时执行
- 为了支持这一行为,编译器会添加代码来检查所有静态成员、实例构造器
- 有静态构造器的类存在一定性能上的损失
- 当声明和静态构造器中同时存在对同一静态成员赋值时,静态构造器的值最终会被保留下来
5.4.3 静态类
- 类不需要实例成员
- 例如,工具类
- 所有成员都是静态的
- 没有实例成员也就不需要被实例化
- 同样也不能被实例化
- 不能在类中声明实例字段或方法,否则编译不通过
- 类的内部成员必须手动加上static
- 不可拓展,在CIL中标记为abstract、sealed
- 不能派生
5.5 扩展方法
- 扩展方法
- 可以为任何类添加实例方法
- 在其他类中,一般为静态类,编写public static方法
- 定义方式
- 方法接收的第一个参数必须是被拓展的类
- 第一个参数必须用this修饰
- 特殊的方法签名
- 特点
- 可以拓展类和接口
- 不能被继承,只能当作实例方法,通过实例调用
- 不属于类及接口的成员,不能被派生类或接口继承,且在派生类中无法通过base访问
- 被拓展的类的实例及其派生类的实例可以调用拓展方法
- 派生类的实例可以当作基类的一个实例使用(多态、隐式向上转型),所以派生类的实例可以调用拓展方法
- 被拓展的接口的实现类及其派生类的实例可以调用拓展方法
- 使用原则
- 被拓展的类不能拥有具有相同签名的方法
- 尽量不要为没有所有权的类进行方法拓展
- 如果要拓展类
- 如果有基类代码,则直接修改基类会更好
- 扩展方法的作用就是为类的实例拓展功能
- 直接在类上添加,与使用扩展方法效果类似,且不会有被屏蔽的风险
- 如果没有基类代码,则应为类实现的接口添加扩展方法
- 一般来说,修改接口比修改类的可能性要小得多,所以被屏蔽的风险也更小
- 与通过继承的方式对类进行拓展相比,使用扩展方法,在链式编程时就不需要进行类型转换
- 基类的方法会返回该类的实例的引用
- 同时需要用到基类方法和自己的拓展方法
- 如果使用继承,当调用基类方法时,需要转型到派生类才能调用自己拓展的方法,但有利于版本控制与组织代码结构
- 使用扩展方法则比较简单方便,但不利于版本控制
- 如果有基类代码,则直接修改基类会更好
- 如果要拓展接口
- 一般不会直接修改当前接口,否则所有实现类都得修改一遍代码以实现该拓展方法
- 继承和扩展方法各有利弊,想要方便则用拓展方法
5.6 封装数据
5.6.1 const
- 不能用static修饰
- 自动变为静态类型
- 在实例对象之间共享
- 必须保证常量值永远不变,如果另一个程序集引用该程序集,则常量会取字面值编译到另一个程序集中。
5.6.2 readonly
- 只能修饰字段
- 应该优先使用只读属性,例如,public Person person { get; }
- 只能在声明时初始化或者通过构造器直接修改字段
5.7 嵌套类
- 在包容类外部没有意义时则定义为嵌套类
- 嵌套类可被private修饰
- 相当于java内部类
- 可以访问包含类的所有成员
5.8 分部类
5.8.1 分部类
- 将类的定义划分到多个文件中
- 用于代码生成器
- 符合一个文件一个类的规范,当有嵌套类时
- 在class前加partial关键字
- partial class后面的类名是同一个
5.8.2 分部方法
- 在分部类中可以使用分部方法
- 只声明,不实现
- 在另一个分部类中实现
- 避免分部类代码被覆盖后方法需要重新编写
- 在另一个分部类中实现
- 不能有返回值
- 避免分部方法只声明了而没有在别的分部类中实现
- 若要返回值,可以使用ref
- 不支持out关键字
- 用partial修饰方法