C#类(八)
随着面向对象的出现,焦点从优化指令转移到组织程序的数据和功能上。程序的数据和功能被组织为逻辑上相关的数据项和函数的封装集合,并被称为类。
类是一个能存储数据并执行代码的数据结构。包含数据成员和函数成员。
| 数据成员存储数据 | 函数成员执行代码 |
|---|---|
| 字段 常量 |
方法 属性 构造函数 析构函数 运算符 索引器 事件 |
声明类
类的声明定义新类的特征和成员。它并不创建类的实例,但创建用于创建实例的模板,类的声明提供以下内容:
- 类的名称
- 类的成员
- 类的特征
1 | class MyExcellentClass |
类的成员
字段和方法是最重要的类的成员类型。字段是数据成员,方法是函数成员。
字段
与c和c++不同,c#在类型的外部不能声明全局变量。所有的字段都属于类型,而且必须在类型声明内部声明。
1 | class MyClass |
方法
1 | class SimpleClass |
参考后续的文章:C#方法(九)
[[csharp-9-func]]
创建变量和类的实例
声明类类型的变量分配的内存是用来保存引用的,而不是用来保存类对象实际数据的Dealer theDealer1;。
要为实际数据分配内存,需要使用new运算符。
1 | class Dealer { ... } |
访问修饰符
- 私有的(private)
- 公有的(public)
- 受保护的(protected)
- 内部的(internal)
- 受保护内部的(protected internal)
如果有多个修饰符,可以任意顺序排列
1 | public static int MaxVal; |
从类的内部访问成员
1 | class DaysTemp |
从类的外部访问成员
使用.运算符来访问。
1 | class DaysTemp |
静态字段
使用static修饰符将字段声明为静态字段。静态字段被类的所有实例共享,所有实例都访问同一内存位置。
静态字段示例:
1 | class D { |
1 | d1: Mem1 = 2, Mem2 = 4; |
类的外部访问静态成员
使用类名与.运算符来从外部访问。D.Mem2 = 5。
另一个访问静态成员的方法是使用using static声明。
1 | using static System.Console; //在其他成员中包含Writable() |
静态成员生存周期
静态成员的生命周期与实例不同,只有在实例创建之后才产生实例成员,实例销毁后实例成员也不存在了。
即使不存在类实例,静态成员也存在。如果静态字段有初始化语句,那么会在使用该类的任何静态成员之前初始化该字段,但不一定在程序执行的开始就初始化。
静态函数成员
- 如同静态字段,静态函数成员独立于任何类实例。即使没有类的实例,仍然可以调用静态方法。
- 静态函数成员不能访问实例成员,但能访问其他静态成员。
1 | class X |
1 | Value of A: 10 |
可以声明为static的类成员类型
| 数据成员(存储数据) | 函数成员(执行代码) |
|---|---|
| ✓ 字段 | ✓ 方法 |
| ✓ 类型 | ✓ 属性 |
| 常量 | ✓ 构造函数 |
| ✓ 运算符 | |
| 索引器 | |
| ✓ 事件 |
成员常量
成员常量与之前的局部常量类似,只是它们被声明在类声明中而不是方法内。
与局部常量类似,用于初始化成员常量的值必须是可计算的,而且通常是一个预定义简单类型或由它们组成的表达式。
同样,不能在成员常量声明以后给它赋值。
1 | class MyClass |
局部常量
- 在声明式必须初始化。
- 在声明后不能改变。
1 | void Display() |
常量与静态量
成员常量对类的每个实例都是”可见的”,并且没有类的实例也可以使用。
与静态量不同的是,常量没有自己的存储位置,而是在编译时被编译器替换,类似于c/c++中的#define值。
1 | class X |
虽然常量成员表现得很像静态值,但不能将常量声明为static。
1 | static const double PI = 3.14 //错误,不能将常量声明为static |
属性
属性是代表类实例或类中的数据项的成员。
和字段不同的是,属性是一个函数成员。
- 属性不一定为数据存储分配内存
- 属性执行代码
属性是一组匹配的,命名的,称为访问器的方法。set访问器为属性赋值。get访问器从属性获取值。
1 | int MyValue |
属性声明和访问器
set和get访问器有预定义的语法和语义。可以把set访问器想象成一个方法,带有单一的参数,它’设置’属性的值,get访问器没有参数并从属性返回一个值。
set访问器:
- 拥有一个单独的,隐式的值参,名称为
value,与属性的类型相同; - 拥有一个返回类型
void。
get访问器:
- 没有参数;
- 拥有一个与属性类型相同的返回类型。
属性示例
1 | class C1 |
使用属性与关联字段
写入和读取属性的方法与访问字段一样,访问器被隐式调用。
属性常和字段关联,一种常见的方式是在类中将字段声明为private以封装该字段,并声明一个public属性来控制从类的外部对该字段的访问。和属性关联的字段常被称为后备字段或后备存储。
1 | class C1 |
属性会根据是写入还是读取来隐式地调用适当的访问器。不能显式地调用访问器,因为这样做会产生编译错误。
1 | y = MyValue.get(); // 错误!不能显式调用 get 访问器 |
执行其他计算
属性访问器并不局限于对关联的后备字段进传出数据。访问器get和set能执行任何计算,也可以不执行任何计算。唯一必须的行为是get访问器要返回一个属性类型的值。
1 | public int Useless |
1 | int theRealValue = 10; // 字段 |
c#7.0为属性的getter/setter引入了表达函数体(Lambda表达式)语法。这种语法只有在访问函数体由一个表达式组成才能使用。
1 | int MyValue |
只读和只写属性
- 只有
get访问器的属性称为只读属性。只读属性能够安全地将一个数据项从类或类的实例中传出,而不必让调用者修改属性值。 - 只有
set访问器的属性称为只写属性。只写属性很少见,因为几乎没啥用途。如果想在赋值时触发一个副作用,应该使用方法而不是属性。 - 两个访问器中至少有一个必须定义,否则编译器会产生一条错误消息。
属性与公有字段
按照推荐的编码实践,属性比公有字段更好。
- 属性是函数成员而不是数据成员,允许你处理输出和输出,而公有字段不行。
- 属性可以只读或只写,而字段不行。
- 编译后的变量和编译后的属性语义不同。(某些情况下,修改字段导致访问这个字段的其他程序集都要重新编译,而属性,只用修改属性的实现。)
自动实现属性
因为属性经常被关联到后备字段,所以c#提供了自动实现属性(automatically implemented property或auto-implemented property,常简称为“自动属性”,auto-property),允许只声明属性而不声明后备字段。编译器会为你创建隐藏的后备字段,并且自动挂接到get和set访问器上。
- 不声明后备字段——编译器根据属性的类型分配存储。
- 不能提供访问器的方法体——它们必须被简单地声明为分号。get 担当简单的内存读,set 担当简单的写。但是,因为无法访问自动属性的方法体,所以在使用自动属性时调试代码通常会更加困难。
1 | class C1 |
1 | MyValue: 0 |
静态属性
属性也可以声明为static。静态属性的访问器和所有静态成员一样,具有如下特点:
- 不能访问类的实例成员,但能被实例成员访问。
- 不管类是否有实例,它们都是存在的。
- 在类的内部,可以仅使用名称来引用静态属性。
- 在类的外部,正如本章前面描述的,可以通过类名或者使用 using static 结构来引用静态属性。
1 | using System; |
1 | Init Value: 0 |
实例构造函数
实例构造函数是一个特殊的方法,它在创建类的每个新实例时执行。
- 构造函数用于初始化类实例的状态。
- 如果希望能从类的外部创建类的实例,需要将构造函数声明为public。
- 构造函数的名称和类名相同。
- 构造函数不能有返回值。
带参数的构造函数
- 构造函数可以带参数。
- 构造函数可以被重载。
使用new运算符来创建类的新实例。
1 | class Class1 |
1 | Name Nemo, Id 28 |
默认构造函数
在类的声明中没有显式地提供实例构造函数,那么编译器会提供一个隐式的默认构造函数。
- 没有参数。
- 方法体为空。
如果你为类声明了任何构造函数,那么编译器将不会为该类定义默认构造函数。
1 | class Class2 |
静态构造函数
构造函数也可以声明为static。实例构造函数初始化类的每个新实例,而static构造函数初始化类级别的项。通常,静态构造函数初始化类的静态字段。
- 初始化类级别的项。
- 在引用任何静态成员之前。
- 在创建类的任何实例之前。
- 静态构造函数在以下方面与实例构造函数类似。
- 静态构造函数的名称必须和类名相同。
- 构造函数不能返回值。
- 静态构造函数在以下方面和实例构造函数不同。
- 静态构造函数声明中使用
static关键字。 - 类只能有一个静态构造函数,而且不能带参数。
- 静态构造函数不能有访问修饰符。
- 静态构造函数声明中使用
- 类既可以有静态构造函数也可以有实例构造函数。
- 如同静态方法,静态构造函数不能访问所在类的实例成员,因此也不能使用
this访问器。 - 不能从程序中显式调用静态构造函数,系统会自动调用它们:
- 在类的任何实例被创建之前;
- 在类的任何静态成员被引用之前。
1 | class RandomNumberClass |
对象初始化语句
对象初始化语句扩展了创建语法,在表达式的尾部放置了一组成员初始化语句。利用对象初始化语句,可以在创建新的对象实例时,设置字段和属性的值。
- 创建对象的代码必须能够访问要初始化的字段和属性。
- 初始化发生在构造方法执行之后,因此在构造方法中设置的值可能会在之后对象初始化中重置为相同或不同的值。
1 | public class Point |
1 | pt1: 1, 2 |
析构函数
析构函数(destructor)执行在类的实例被销毁之前需要的清理或释放非托管资源的行为。非托管资源是指通过Win32 API获得的文件句柄,或非托管内存块。使用.NET 资源是无法得到它们的,因此如果坚持使用.NET类,就不需要为类编写析构函数。
readonly修饰符
字段可以用readonly修饰符声明。其作用类似于将字段声明为const,一旦值被设定就不能改变。
const字段只能在字段的声明语句中初始化,而readonly字段可以在下列任意位置设置它的值。- 字段声明语句,类似于
const。 - 类的任何构造函数。如果是
static字段,初始化必须在静态构造函数中完成。
- 字段声明语句,类似于
const字段的值必须在编译时决定,而readonly字段的值可以在运行时决定。这种自由性允许你在不同的环境或不同的构造函数中设置不同的值!const的行为总是静态的,而对于readonly字段以下两点是正确的。- 它可以是实例字段,也可以是静态字段。
- 它在内存中有存储位置。
1 | class Shape |
this关键字
this关键字在类中使用,是对当前实例的引用。它只能被用在下列类成员的代码块中。
- 实例构造函数。
- 实例方法。
- 属性和索引器的实例访问器。
很明显,因为静态成员不是实例的一部分,所以不能在任何静态函数成员的代码中使用this关键字。更适当地说,this用于下列目的: - 用于区分类的成员和局部变量或参数;
- 作为调用方法的实参。
1 | class MyClass |
1 | Max: 30 |
索引器
索引器是一组get和set访问器,与属性类似。
1 | string this [ int index ] |
索引器和属性
索引器和属性在很多方面是相似的。
- 和属性一样,索引器不用分配内存来存储。
- 索引器和属性都主要被用来访问其他数据成员,它们与这些成员关联,并为它们提供获取和设置访问。
- 属性通常表示单个数据成员。
- 索引器通常表示多个数据成员。
可以认为索引器是为类的多个数据成员提供 get 和 set 访问的属性。通过提供索引器,可以在许多可能的数据成员中进行选择。索引本身可以是任何类型,而不仅仅是数值类型。
- 和属性一样,索引器可以只有一个访问器,也可以两个都有。
- 索引器总是实例成员,因此不能被声明为
static。 - 和属性一样,实现
get和set访问器的代码不一定要关联到某个字段或属性。这段代码可以做任何事情也可以什么都不做,只要 get 访问器返回某个指定类型的值即可。
声明索引器
- 索引器没有名称。在名称的位置是关键字 this。
- 参数列表在方括号中间。
- 参数列表中必须至少声明一个参数。
1 | ReturnType this [ Type param1, ... ] |
索引器的set访问器与get访问器
和属性一样,不能显式调用get和set访问器。取而代之,当索引器用在表达式中取值时,将自动调用get访问器。当使用赋值语句对索引器赋值时,将自动调用set访问器。
1 | emp[0] = "Doe"; |
索引器示例
1 | class Employee { |
1 | class Class1 |
1 | Values = T0: 0, T1: 0 |
索引器重载
只要索引器的参数列表不同,类就可以有任意多个索引器。单纯索引器类型不同是不够的。这叫作索引器重载,因为所有的索引器都有相同的名称:this 访问引用。请记住,类中重载的索引器必须有不同的参数列表。
1 | class MyClass |
访问器的访问修饰符
属性和索引器,在默认情况下,成员的两个访问器的访问级别和成员自身相同。也就是说,如果一个属性的访问级别是public,那么它的两个访问器的访问级别也是如此。索引器也一样。
不过,可以为两个访问器分配不同的访问级别。例如,如下代码演示了一个常见而且重要的范式,那就是将set访问器声明为private,将get访问器声明为public。get之所以是public的,是因为属性的访问级别是public。
在这段代码中,尽管可以从类的外部读取属性,但只能在类的内部设置它(本例中是在构造函数内设置)。这是一个非常重要的封装工具。
1 | class Person |
1 | Person's name is Capt. Ernest Evans |
访问器的访问修饰符有几个限制。最重要的限制如下。
- 仅当成员(属性或索引器)既有
get访问器也有set访问器时,其访问器才能有访问修饰符。 - 虽然两个访问器都必须出现,但它们中只能有一个有访问修饰符。
- 访问器的访问修饰符的限制必须比成员的访问级别更严格。
如果属性的访问级别是protected,则唯一能对访问器使用的访问修饰符是private。
分部类和分别类型
类的声明可以分割成几个分部类的声明。
- 每个分部类的声明都含有一些类成员的声明。
- 类的分部类声明可以在同一个文件中也可以在不同文件中。
每个分部类声明必须被标注为partial class,而不是单独的关键字class。分部类声明看起来和普通类声明相同,只是增加了类型修饰符partial。
1 | partial class MyPartClass // 类名称与下面的相同 |
说明:类型修饰符 partial 不是关键字,所以在其他上下文中,可以在程序中把它用作标识符。但直接用在关键字 class、struct 或 interface 之前时,它表示分部类型。
分部方法
分部方法是声明在分部类中不同部分的方法。分部方法的不同部分可以声明在分部类的不同部分中,也可以声明在同一个部分中。分部方法的两个部分如下:
- 定义分部方法声明:给出签名和返回类型,声明的实现部分只是一个分号。
- 实现分部方法声明:给出签名和返回类型,以普通的语句块形式实现。
定义声明和实现声明的签名和返回类型必须匹配。签名和返回类型有如下特征:
- 返回类型必须是
void。 - 签名不能包括访问修饰符,这使部分方法是隐式私有的。
- 参数列表不能包含
out参数。 - 在定义声明和实现声明中都必须包含上下文关键字
partial,并且直接放在关键字void之前。
可以有定义部分而没有实现部分。在这种情况下,编译器把方法的声明以及方法内部任何对方法的调用都移除。不能只有部分方法的实现部分而没有定义部分。
1 | partial class MyClass |
1 | Sum is 11 |
