1 类的基本概念
类是引用类型,类的类成员类型有9种,分别是:
- 数据成员:
- 字段
- 常量
- 函数成员:
- 方法
- 属性
- 构造函数
- 析构函数
- 索引
- 事件
- 运算符
字段和方法是最重要的类成员类型。
C#在类型的外部不能声明全局变量(也就是变量或字段),所有字段都属于类型,而且必须在类型声明内部声明。
C#在类型的外部不能声明全局函数(也就是函数或方法),所有方法都属于类型,而且必须在类型声明内部声明。而且所有方法声明必须包含返回类型或
void
2 成员修饰符
修饰符是 成员 声明的可选部分,可以分为 访问修饰符,静态修饰符,常量修饰符等,如果有多个修饰符,可以是任何顺序排列。
2.1 访问修饰符
访问修饰符是成员声明的可选部分,指明程序的其他部分如何访问成员,五种成员访问控制如下:
private
:私有的,默认的访问类型public
:公有的protected
:派生继承的类可以访问internal
:同一程序集中的类可以访问protected internal
:同一程序集中的派生继承类可以访问
2.2 静态修饰符 static
类成员可以关联到类的一个实例,也可以关联到类的整体,即所有类的实例。
默认情况下,每个实例都拥有自己的各个类成员副本,这些成员称为 实例成员(如实例方法或实例字段),而关联到类的整体,即所有类的实例,称为 静态成员,使用 static
修饰符表示。
静态字段与实例字段的区别:
- 静态字段被类的所有实例共享,即所有实例都访问同一内存位置
- 生命周期不同,即使类没有实例,也存在静态成员,并可以访问(同理静态函数也一样),外部访问时,必须使用类名,而不是实例名
- 静态函数成员不能访问实例成员,可以访问其他静态成员
- 成员常量 不能声明为
static
,注意 成员常量并不同于本地常量(后者在方法内部初始化)
常量成员 和 索引器成员 类型不能声明
static
2.3 成员常量 const
成员变量类似于本地变量,只是它们被声明的位置不同,同时,成员常量表现得像 静态值,即没有类的实例也可以使用,与真正的静态量不同的是,常量没有自己的存储位置,而是在编译时被编译器替换,并且一旦值被设定就不能改变,以及在必须在声明的同时赋值
2.4 readonly
修饰符
与 const
相比较,readonly
修饰符有以下不同:
const
字段只能在字段的声明语句中初始化,而readonly
字段可以在以下状况位置中设置数值
- 字段声明语句
- 类的任意构造函数(静态成员需在静态构造函数中初始化)
const
的行为总是静态的,而readonly
则可以是实例字段,也可以是静态字段,并且它在内存中有存储位置
3 方法
3.1 关键字
3.1.1 var
关键字
var
关键字是句法的速记,表示如何可以从初始化语句的右边推断出的类型,其中有几个注意事项:
- 只能用于本地变量,不能用于字段
- 只能在变量声明中包含初始化时使用
- 一旦编译器推断出变量的类型后,它就是固定且不能更改
3.1.2 const
关键字
本地常量(注意不是本地变量),一旦被初始化就不能更改,且声明时必须初始化,在声明类型之前添加关键字 const
3.2 参数
3.2.1 形参
形参是本地参数,它声明在方法的参数列表中,而不是方法体中并在方法开始之前初始化(除输出参数外)
3.2.2 实参
当代码调用一个方法时,形参的值必须在方法的代码开始执行之前被初始化,用于初始化形参的表达式或变量称作实参。
实参位于方法调用的参数列表中
根据各自以略微不同的方式从方法传入或传出数据(类似类型中的值类型和引用类型),参数分为几种
3.2.3 值参数
值参数,通过将实参的值复制到形参的方式把实参的值,方法被调用时,系统做如下操作:
- 在栈中为形参分配空间
- 将实参的值复制给形参
3.2.4 引用参数
在使用引用参数时,注意事项:
- 必须在方法的声明和调用中都使用
ref
修饰符 - 传入的实参必须是变量,并且在调用方法前需明显被赋值
相对于值参数,系统在栈上为形参分配内存,相反,引用参数具有以下特征:
- 不会为形参在栈上分配内存
- 实际上形参的参数名将作为实参变量的别名
3.2.5 输出参数
输出参数的作用与引用参数的作用类似,区别在于引用参数必须传入已初始化的实参,而输出参数则是可以在内部初始化实参,而且是一定要初始化,并要在使用前初始化。
- 必须在方法的声明和调用中都使用
out
修饰符 - 传入实参必须是变量,在方法体内部初始化且一定要在读取前初始化,这意味着该参数的初始值无关的
3.2.6 参数数组
- 参数数组表示的所有参数都是必须具有相同的类型
- 参数数组必须是参数列表中的最后一个
- 参数数组中的所有参数都必须具有相同的类型
- 在数据类型前使用
params
修饰符,在数据类型后放置一组空的方括号[]
,如:
void ListInts(params int[] invals)
{
...
}
3.3 方法重载
一个类中可以有一个以上的方法拥有相同的名称,这叫做方法重载,使用相同名称的每个方法必须有一个和其他方法不相同的签名,签名有下列信息组成:
- 方法的名称
- 参数的数目
- 参数的数据类型和顺序
- 参数的修饰符
- 不包括返回类型
3.4 命名参数
语法:形参的名字后面跟着 冒号 和实际的参数值或表达式
3.5 可选参数
语法:使用形参名字后跟 等号默认值
4 属性
属性 是代表类的实例或类中的一个数据项的成员,写入与读取都像 字段,就类似Python中的 setter
和 getter
,属性本身没有任何存储,访问器决定任何处理发进来的数据,以及什么数据应被发送出去
因此属性与字段的不同,属性是一个函数成员,属性是指定的一组两个匹配的、称为 访问器的 方法
set
访问器是为属性赋值的方法get
访问器是从属性取值的方法
int MyValue
{
set
{
SetAccessorCode
}
get
{
GetAccessorCode
}
}
4.1 set
访问器
set
访问器可以想象成一个方法,拥有一个单独的、隐式的值参,名为value
,与属性类型相同,拥有一个返回类型 void
。
正如Python中的 setter
和 getter
,属性主要起一个过滤的作用,所以属性常与字段关联,常见的方式是:在类中将字段说明为 private
以起到 封装该字段,并声明一个 public
属性来控制从类的外部对该字段的访问。该字段常称为 后备字段 或 后备存储。
属性和后备字段有两种命名约定:
- 一般属性命名使用 Pascal 命名,而字段使用 Camel 命名
- 一般属性命名使用 Pascal 命名,而字段使用 下划线 + Camel 命名
eg:
class C1
{
private int _theRealValue;
public int TheRealValue
{
set
{
_theRealValue = value; //隐式值参
}
get
{
return _theRealValue; //get访问器没有参数,拥有一个与属性类型相同的返回类型
}
}
}
4.2 只读和只写属性
要想不定义属性的某个访问器,可以忽略该访问器的声明
- 只有
get
访问器的属性为 只读属性 - 只有
set
访问器的属性为 只写属性
4.3 自动实现属性
属性常被关联到 后备字段, C#提供了 自动实现属性,允许只声明属性而不声明后备字段,编译器会为你创建隐藏的后备字段,创建自动实现属性 不能提供访问器的方法体,它们必须被简单地声明为 ;
分号。
class C1
{
public int MyValue
{
set;get;
}
}
4.4 静态属性
与其它静态成员一样,静态属性的访问器具有以下特点:
- 不能访问类的实例成员,但能被实例成员访问
- 不管类是否有实例,它们都是存在的
- 当从类的外部访问时,必须使用类名,而不是实例名
4.5 访问器的访问修饰符
可以私自为访问器设置独自的访问修饰符,不过访问器修饰符有以下限制:
- 仅当成员(索引器或属性)同时有
get
访问器和set
访问器是,才能设置访问修饰符 - 两个访问器只能有一个设置修饰符
- 访问器的访问修饰符必须比成员的访问修饰符低级
5 索引器
索引器可以使访问字段如同数组一样。
索引器如同属性一样,是一组 get
和 set
访问器,索引器与属性都是相似的,如:
- 和属性一样,索引器不用分配内存来存储
- 索引器和属性都主要被用来访问其他数据成员
- 可以只有一个访问器,也可以两个都有
- 不必一定关联某个字段或属性,只需
get
访问器返回某个指定类型的值就可以了
但是,索引器总是实例成员,故不能声明 static
5.1 声明索引器
语法:
- 索引器没有名称,转而使用关键词
this
- 参数列表在 方括号 中
- 参数列表中至少声明一个参数
ReturnType this [Type param1,...]
{
get
{
SetAccessorCode
}
set
{
GetAccessorCode
}
}
5.2 索引器重载
只要索引器的参数列表不同,类就可以有多个索引器,这就叫做索引器重载
6 实例构造函数
与属性类似,实例构造函数(构造器)是一个 方法,用于创建类的每个实例是执行,如果在类的声明中没有显式的提供构造函数,那么编译器会提供一个隐式的默认构造函数,要点:
- 如果希望能从类的外部创建类的实例,需要将构造函数声明添加
public
修饰符 - 构造函数的 名称和类名相同
- 构造函数不能有返回值
6.1 带参数的构造函数
构造函数可以带参数,也可以被重载
6.2 静态构造函数
构造函数可以声明为 static
,实例构造函数初始化类的每个新实例,而 static
构造函数初始化 类级别的项(即如静态字段),要点:
- 静态构造函数声明中使用
static
修饰符 - 类只能有一个静态构造函数,且不能带参数
- 不能有 访问修饰符
- 静态构造函数不能访问所在类的实例成员,因此不能使用
this
访问器
6.3 对象初始化语句
对象初始化语句 扩展了创建语法,在表达式的尾部放置了一组成员初始化语句,这允许你在创建新的对象实例时,设置 字段 和 属性 的值,语法:
new TypeName(ArgList) {FieldOrProp = InitExpr, FieldOrProp = InitExpr...}; //使用花括号
要点:
- 创建对象的代码必须能够访问初始化时重设置的字段和属性
- 初始化发生在构造方法执行之后
7 this
关键字
this
关键字是对当前实例的引用,它只能被用在 类成员的代码块 中,如:
- 实例构造函数
- 实例方法
- 属性和索引器的实例访问器
静态成员不是实例的一部分,所以与之相关的都不能使用
this
8 分布类和分布类型
类的声明可以分割成几个分部类的声明:
- 每个分部类的声明都含有一些类成员的声明
- 类的分部类声明可以在同一个文件中也可以在不同的文件中
- 声明时,添加
partical
修饰符
eg:
partical class MyPartClass
{
...
}
9 析构函数
析构函数(destructor)执行在类实例被销毁之前需要的清理或释放非托管资源的行为
10 继承
继承是使用一个已经存在的类作为新类的基础进行扩展,这样的已存在的类称为 基类,新类称为 派生类。
声明一个派生类,要在声明的类名后添加 基类规格说明,基类规格说明由 冒号 和 基类名称 组成,eg:
class SonClass : ParentClass
{
...
}
要点:
- 派生类扩展它的基类,派生类不能删除它所继承的任何成员
- 所有类都派生自
object
类,所有的基类规格说明
中只能有一个单独的类
10.1 屏蔽基类的成员
虽然派生类不能删除它的继承的任何成员,但可以用重构一个成员从而 屏蔽 覆盖掉基类成员。创建屏蔽成员的要点:
- 声明时 需要使用
new
修饰符 - 数据成员:声明一个新的相同类型、相同名称的数据成员
- 函数成员:声明 相同签名 的函数成员(签名由名称和参数列表组成,不包括返回类型)
- 也可以 屏蔽静态成员
10.2 基类的访问
10.2.1 在派生类中基类的访问
如果派生类想要完全访问被隐藏的继承成员,可以使用 基类访问表达式(注:是派生类使用,而不是实例使用)。
基类访问表达式 由关键字 base
后跟 .
和成员类名组成,eg:
Console.WriteLine("{0}", base.Field1);
10.2.1 在实例层面中基类的访问
使用 base
只能在派生类中使用,如果想要在实例的层面上完全访问被隐藏的继承成员,就需要使用到 类型转换运算符
即如果有一个派生类的实例,就可以获取 该实例基类部分 的引用(使用 类型转换运算符 把该实例转换为 基类类型)
语法:类型转换运算符 放置在派生类的实例前面(类型转换运算符由 括号 括起要被转换成的 类名),eg:
MyDerivedClass derived = new MyDerivedClass();
MyBaseClass mybc = (MyBaseClass) derived;
10.3 虚方法、覆写方法以及抽象成员
基类的方法使用 virtual
修饰符,称为 虚方法,派生类的方法使用 override
修饰符,称为覆写方法。
虚方法 可以使基类的引用“升至”派生类内,即尽管使用了基类访问,返回的还是派生类的方法、数据。使用虚方法,需要满足下面条件:
- 派生类的方法和基类的方法有 相同的签名 和 返回类型
- 覆写和被覆写的方法必须有相同的可访问性
- 不能覆写
static
方法或 非虚方法 - 只有方法、属性、索引器和事件可以被声明为
virtual
和override
10.4 抽象类、密封类与静态类
10.4.1 抽象成员
抽象成员与虚方法中的虚成员类似,都是指设计为被覆写的函数成员,抽象成员有以下特征:
- 必须是一个函数成员
- 必须使用
ahstract
修饰符,并必须在派生类中指定override
修饰符 - 不能有实现代码块,使用
;
分号代替
eg:
// 函数方法
abstact public void PrintStuff(string s);
// 属性
abstract public int MyProperty
{
get;
set;
}
10.4.2 抽象类
抽象类就是指设计为被继承的类,抽象类只能被用作其他类的基类,因此不能创建抽象类的实例,同样的抽象类使用 abstract
修饰符声明,要点:
- 抽象类可以 包含 抽象成员或普通非抽象成员
- 抽象类可以派生自另一个抽象类
- 任何派生自抽象类的类 必须使用
override
关键字 实现该类所有的抽象成员
10.4.3 密封类
相对于抽象类必须作为基类,密封类则不能被继承,密封类使用 sealed
修饰符标注
10.4.4 静态类
静态类中所有的成员都是静态的,静态类用于存放不收实例数据影响的数据和函数,主要用途是创建一个包含一组数学方法和值的数据库,要点:
- 类需添加
static
修饰符 - 所有成员都是静态的
- 可以有静态构造函数,但不能有实例构造函数,不能创建实例
- 静态类不能继承
10.5 派生类的构造函数初始化语句
有两种形式的构造函数初始化语句,使用关键字 base
或 this
10.5.1 关键字 base
默认情况下,在构造实例时,将调用基类的无参数构造函数,如果派生类希望使用一个指定的基类构造函数,就 必须在 构造函数初始化语句 中指定它。
构造函数初始化语句模板后跟 :
冒号加关键字 base
和要调用的基类构造函数的参数列表组成。
在基类参数列表中的参数必须在 类型和顺序方面 与调用的基类构造函数的列表参数相匹配,eg:
public MyDerivedClass(int x, string s):base(s,x)
10.5.2 关键字 this
与 base
调用基类的构造函数不同, this
关键字用于调用本类内其他的构造函数,起组合的作用。语法与 base
的一样。
构造函数初始化语句模板后跟 :
冒号加关键字 this
和要调用的构造函数的参数列表组成。eg:
class MyClass{
readonly int firstVar;
public string UserName;
private MyClass()
{
firstVar = 20;
}
public MyClass(string fristName):this()
{
UserName = fristName;
}
}
10.6 扩展方法
想要在一个类中扩展方法,可以采取以下方式:
- 如果有源代码并可以直接修改这个类
- 没有源代码,但该类不是 密封 的,可以把其用作成一个基类,在派生类中实现这个方法
- 如果该类是 密封的,这样就不得不在另一个类中使用该类的 公有可用成员 编写一个方法
第三种方式例子:
class MyData
{
private double D1;
private double D2;
private double D3;
public MyData(double d1, double d2, double d3)
{
D1 = d1;
D2 = d2;
D3 = d3;
}
public double Sum()
{
return D1 + D2 + D3;
}
}
static class ExtendMyData
{
public static double Average(MyData md)
{
return md.sum()/3;
}
}
class Program
{
static void Main()
{
MyData md = new MyData(3, 4, 5);
Console.WriteLine("Average:{0}", ExtendMyData.Average(md));// ExtendMyData 是静态类
}
}
但如果想要直接通过调用基类的实例自身该添加功能的形式,就需要用到 扩展方法 了,而这只需在第三种方式中做小小的改动,就是在创建新功能的 参数说声明中的类型名前添加关键字 this
,这 其中的要求:
- 声明扩展方法的类必须是静态类
static
- 扩展方法本身必须声明为
static
- 扩展方法必须包含关键字
this
作为他的第一个参数类型,并在后面跟着它所扩展的类的名称
10.7 命名约定
- Pascal 命名:用于类型名称和类中对外可见成员的名称,如:命名空间、类、方法、属性、公共字段等。
- Camel 命名:用于 局部变量的名称 和方法声明的 形参名称
- 下划线加 Camel 命名:用于私有和受保护的 字段