<二> 类
正如前面所述,对象是面向对象语言的核心,数据抽象和对象封装是面向对象技术的基本要求,而实现这一切的主要手段和工具就是类。类的成员包含数据成员(常量、域、事件)和函数成员(方法、属性、索引器、操作符、构造函数和析构函数等)
1、类的定义
用关键字class来创建类。类定义的基本形式为:
[修饰符] class 类名 [:基类和实现的接口列表]
{
类成员
}
类允许的修饰符用法
修饰符 | 用法 |
public | 表示不限制对类的访问。类的访问权限默认为public |
protected | 表示该类只能被这个类的成员或派生类成员访问 |
private | 表示该类只能被这个类的成员访问 |
internal | 表示该类能够由程序集中的所有文件使用,而不能由程序员集之外的对象使用 |
new | 只允许用在嵌套类中,它表示所修饰的类会隐藏继承下来的同名成员 |
abstract | 表示这是一个抽象类,该类含有抽象成员,因此不能被实例化,只能用作基类 |
sealed | 表示这是一个密封类,不能从这个类再派生出其他类。显然密封类不能同时为抽象类 |
2、类的成员
类的定义包括类头和类体两部分,其中体用一对大花括号{}括起来,类体用于定义该类成员。
类成员由两部分组成,一是类体中以类成员声明形式引入的类成员,另一个则是直接从它的基类继承而来的成员。类的成员声明主要包括:常数声明、字段声明、方法声明、属性声明、事件声明、索引器声明、运算符声明、构造函数声明、析构函数声明、静态构造函数、类型声明等。当字段、方法、属性、事件、运算符和构造函数等声明中含有static修饰符时,则表明它们是表态成员,否则就是实例成员。类成员声明中可以使用以下5种修饰符中的一种:public、private、protected、internal、protected、internal。当类成员声明不包含访问修饰符时,默认约定访问修饰符为private。
2.1 常数声明
常数声明一般语法形式:
[常数修饰符] const 类型 标识符=常数表达式
◆ 常数修饰符可以是:new、public、private、protected、internal
◆ 类型必须是:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string、枚举类型或引用类型。
例如:public const int PI=3.1415926;
注意:常数声明不允许使用static修饰符,但它和静态成员一样只能通过类访问。
2.2 字段声明
字段声明的一般语法形式:
[字段修饰符] 类型 变量声明列表;
◆ 变量声明列表:标识符或者用逗号“,”分隔多个标识符,并且变量标识符还可以用赋值号“=”设定初始值。
◆ 字段修饰符可以是:new、public、private、protected、static、readonly、volatile。其中new、public、priavte、protected、internal前面已作介绍,加static修饰的字段是静态字段,不加static修饰的字段是实例字段。静态字段不属于某个实例对象,实例字段则属性实例对象。也就是说,一个类可以创建若干个实例对象,每个实例对象都有自己的实例字段映像,而若干个实例对象只能共有一份静态字段。所以对静态字段的访问只与类关联,对实例字段的访问要与实例对象关联。
◆ 加readonly修饰符的字段是只读字段,对只读字段的赋值只能在声明的同时进行,或者通过类的实例构造函数或静态构造函数实现。其他情况下,对只读字段只能读不能写。这与常量有共同之处。但const成员的值要求在编译时能计算,如果这个值要到运行时刻才能给出,又希望这个值一量赋值就不能改变,那么就可以把它定义成只读字段。
3、构造函数
当定义了一个类之后,就可以通过new运算符将其实例化,产生一个对象。为了能规范、安全地使用这个对象,C#提供了对对象进行初始化的方法,这就是构造函数。
在C#中,类的成员字段可以分为实例字段和静态字段,与此相应的构造函数也分为实例构造函数和静态构造函数。
3.1 构造函数及其用途
使用构造函数的必要性可以从以下几个方面探讨:
(1)自动完成初始化工作
当创建 一个对象时,编译器为对应实例变量分配内存,这是对象创建过程的一部分。但内存分配完成后,在任何自动或人工初始化发生前,内存和实例变量仅表示上次使用后残留的垃圾,并非有用信息。如果遗忘初始化工作,而直接将这部分垃圾用于计算就会导致错误的结果。
构造函数可以自动完成初始化工作。不会将垃圾信息用于下一次计算中。
(2)使用构造函数完成初始化有时可避免常规初始化方式带来的不便
在声明变量时给实例变量指定初始化值,就是一种常规的初始化方法。其实,在声明一个变量时,即使没有赋值,该变量也已经有了一个缺省值(缺省值根据类型的不同而不同,如short、int、float等类型缺省值为0,char类型的缺省值为Unicode字符’/u0000’,bool类型的缺省值为false,引用类型缺省值为null)。当然,由于这些缺省值可能随编译系统不同而不同,所以,一般并不推荐程序员使用这些缺省值,也不推荐以常规方式来初始化变量。
(3)附带执行创建对象时需要执行的其他重要动作
往往在创建一个对象时,除了实例初始化外,可能需要执行其他重要动作。例如,将对象的创建通知程序其他部分。此时使用构造函数也是一种理想的解决方案。
3.2 实例构造函数的声明
实例构造的声明语法形式:
[构造函数修饰符] 标识符 ([参数列表])
[:base ([参数列表]) [:this ([参数列表])
{
构造函数语句块
}
◆ 构造函数修饰符:public、protected、internal、private、extern。一般地,构造函数总是public类型的。如果是private类型的,表明类不能被外部类实例化。
◆ 标识符([参数列表]):标识符是构造函数名,必须与这个类同名,不声明返回类型,并且没有任何返回值。这与返回值类型为void的函数不同。构造函数可以没有,也可以有一个或多个参数。这表明构造函数在类的声明中可以有函数名相同,但参数个数不同或者参数类型不同的多种形式,这就是所谓的构造函数重载。
◆ :base([参数列表]):表示调用基类中的实例构造函数。
◆ :this([参数列表]):表示调用同一个基类(已使用:base调用的那个基类)中的其它构造函数。
◆ 构造函数语句块:既可以对静态字段赋值,也可以对非静态字段进行初始化。但在构造函数体中不要做对类的实例进行初始化以外的事情,也不要尝试显式地调用构造函数。
实例构造函数是不能被继承的。如果一个类没有声明任何实例构造函数,则系统会自动提供一个默认的实例构造函数。
在一个类的层次结构中,派生类对象的初始化由基类和派生类共同完成。基类的成员由基类的构造函数初始化,派生类的成员由派生类的构造函数初始化。
当创建派生类的对象时,就会展开一个链式的构造函数调用,在这个过程中,派生类构造函数在执行它自己的函数体之前,首先显式或隐式地调用基类构造函数。类似地,如果这个基类也是从另一个类派生而来的,那么这个基类的构造函数在执行之前也会先调用它的基类的构造函数,以此类推,直到Object类的构造函数为止。如果派生类中又有对象成员,则执行基类的构造函数之后,再执行成员对象类的构造函数,最后执行派生类的构造函数。至于执行基类的哪个构造函数,视情形而定,默认情况下执行基类的无参构造函数,如果要执行基类的有参构造函数,则必须在派生类构造函数的基表列表中指出。
下面程序描述了派生类构造函数的格式以级在初始化对象时构造函数的调用次序。
using System;
using System.Collections;
namespace 笔记
{
class Point
{
private int x, y;
public Point()
{
x = 0; y = 0;
Console.WriteLine("Point() constructor:{0}", this);
}
public Point(int x, int y)
{
this.x = x;
this.y = y;
Console.WriteLine("Point(x,y) constructor:{0}", this);
}
}
class Circle : Point
{
private double radius;
public Circle()
{
Console.WriteLine("Circle() constructor:{0}", this);
}
public Circle(double radius): base() //调用基类的构造函数,在这里base()可以省略不写,因为基类中这个构造函数并没有参数,它可以隐式地被派生类继承。
{
this.radius = radius;
Console.WriteLine("Cirlce (radius) constructor:{0}", this);
}
public Circle(int x, int y, double radius) : base(x, y) //调用基类的构造函数,在这里base(x,y)必需要写,因为基类中这个构造函数带有参数,调用这个带参数的构造函数,以保证在基类进行初始化时获得必需的数据。
{
this.radius = radius;
Console.WriteLine("Circle(x,y,radius) constructor:{0}", this);
}
}
class Test
{
static void Main()
{
Point a = new Point();
Circle b = new Circle(3.5);
Circle c = new Circle(1, 1, 4.8);
Console.Read();
}
}
}
3.3 静态构造函数的声明
静态构造函数的声明语法形式:
[静态构造函数修饰符] 标识符()
{
静态构造函数体
}
◆ 静态构造函数修饰符:[extern] static或者static [extern]。如果有extern修饰,则说明这是一个外部静态构造函数,不提供任何实际的实现,所以静态构造函数仅仅是一个分号。
◆ 标识符():标识符是静态构造函数名,必须与包含静态构造函数的类同名,静态构造函数不能有参数。
◆ 静态构造函数体:静态构造函数的目的是用于对静态字段进行初始化,所以它只能对静态数据成员进行初始化,而不能对非静态数据成员进行初始化。
静态构造函数是不可继承的,而且不能被直接调用。只有创建类的实例或者引用类的任何静态成员时,才能激活静态构造函数,所以在给定的应用程序域中静态构造函数至多被执行一次。如果类中没有声明静态构造函数,而又包含带有初始设定的静态字段,那么编译器会自动生成一个默认的静态构造函数。
4、析构函数
一般来说,创建一个对象时需要用构造函数初始化数据,与此相对应释放一个对象实例时就用析构函数。
析构函数声明语法形式:
[extern] ~标识符()
{
析构函数体
}
◆ 标识符必须与类名相同,但为了区分构造函数,前面需要加“~”表明它是析构函数。
◆ 析构函数不能写返回类型,不能带参数,也不能被重载,当然也不能被继承。所以一个类最多只能有一个析构函数。一个类如果没有显式地声明析构函数,则编译器将自动产生一个默认的析构函数。
析构函数不能由程序显式地调用,而是由系统在释放对象时自动调用。如果这个对象是一个派生类对象,那么在调用析构函数时也会产生链式反应,首先执行派生类的析构函数,然后执行基类的析构函数,如果这个基类还有自己的基类,这个过程就会不断重复,直到调用Object类的析构函数为止,其执行顺序正好与构造函数相反。