类(class)是c#类型中最基础的类型。类是一个数据结构,将状态(字段)和行为(方法和其他函数成员)组合在一个单元中。类提供了用于动态创建类实例的定义,也就是对象(object)。类支持继承(inheritance)和多态(polymorphism),即派生类能够扩展和特殊化基类的机制。
使用类声明可以创建新的类。类声明以一个声明头开始,其组成方式如下:先是指定类的特性和修饰符,后跟类的名字,基类(如果有的话)的名字,以及被该类实现的接口名。声明头后面就是类体了,它由一组包含在大括号({})中的成员声明组成。
下面是一个名为point的简单类的声明:
public class point
{
public int x, y;
public point(int x, int y){
this.x = x;
this.y = y;
}
}
使用new运算符创建类的实例,它将为新实例分配内存,调用构造函数初始化实例,并且返回对该实例的引用。下面的语句创建两个point对象,并且将那些对象的引用保存到两个变量中:
point p1 = new point(0, 0);
point p2 = new point(10, 20);
当不再使用对象时,该对象所占的内存将被自动回收。在c#中,没有必要也不可能显式地释放对象。
1.6.1 成员
类的成员或者是静态成员(static member),或者是实例成员(instance member)。静态成员属于类,实例成员属于对象(类的实例)。
表1.6提供了类所能包含的各种成员的描述。
表1.6 类 的 成 员
成 员
描 述
常数
与类关联的常量值
字段
类的变量
方法
能够被类执行的计算和行为
属性
使对象能够读取和写入类的命名属性
索引器
使对象能够用与数组相同的方式进行索引
事件
能够被类产生的通知
运算符
类支持的转换和表达式运算符
构造函数
初始化类的实例或者类本身
析构函数
在永久销毁类的实例之前执行的行为
类型
被类声明的嵌套类型
1.6.2 可访问性
类的每个成员都有关联的可访问性,它控制能够访问该成员的程序文本区域。有5种可能的可访问性形式。表1.7概述了类的可访问性的意义。
表1.7 类的可访问性
可访问性
意 义
public
访问不受限制
protected
访问仅限于包含类或从包含类派生的类型
internal
访问仅限于当前程序集
protected internal
访问仅限于从包含类派生的当前程序集或类型
private
访问仅限于包含类
1.6.3 基类
类的声明可能通过在类名后加上冒号和基类的名字来指定一个基类译注4。省略基类等同于直接从object类派生。在下面的示例中,point3d的基类是point,而point的基类是object:
public class point
{
public int x, y;
public point(int x, int y){
this.x = x;
this.y = y;
}
}
public class point3d: point
{
public int z;
public point3d(int x, int y, int z): point(x, y){
this.z = z;
}
}
point3d类继承了其基类的成员。继承意味着类将隐式地包含其基类的所有成员(除了基类的构造函数)。派生类能够在继承基类的基础上增加新的成员,但是它不能移除继承成员的定义。在前面的示例中,point3d类从point类中继承了x字段和y字段,并且每一个point3d实例都包含三个字段x,y和z。
从类类型到它的任何基类类型都存在隐式的转换。并且,类类型的变量能够引用该类的实例,或者任何派生类的实例。例如,对于前面给定的类声明,point类型的变量能够引用point实例或者point3d实例:
point a = new point(10, 20);
point b = new point3d(10, 20, 30);
1.6.4 字段
字段是与对象或类相关联的变量。
当一个字段声明中含有static修饰符时,由该声明引入的字段为静态字段(static field)。它只标识了一个存储位置。不管创建了多少个类实例,静态字段都只会有一个副本。
当一个字段声明中不含有static修饰符时,由该声明引入的字段为实例字段(instance field)。类的每个实例都包含了该类的所有实例字段的一个单独副本。
在下面的示例中,color类的每个实例都有r,g,b实例字段的不同副本,但是black,white,red,green和blue等静态字段只有一个副本:
public class color
{
public static readonly color black = new color(0, 0, 0);
public static readonly color white = new color(255, 255, 255);
public static readonly color red = new color(255, 0, 0);
public static readonly color green = new color(0, 255, 0);
public static readonly color blue = new color(0, 0, 255);
private byte r, g, b;
public color(byte r, byte g, byte b) {
this.r = r;
this.g = g;
this.b = b;
}
}
如前面的示例所示,通过readonly修饰符声明只读字段。给readonly字段的赋值只能作为声明的组成部分出现,或者在同一类中的实例构造函数或静态构造函数中出现。
1.6.5 方法
方法(method)是一种用于实现可以由对象或类执行的计算或操作的成员。静态方法(static method)只能通过类来访问。实例方法(instance method)则要通过类的实例访问。
方法有一个参数(parameter)列表(可能为空),表示传递给方法的值或者引用;方法还有返回类型(return type),用于指定由该方法计算和返回的值的类型。如果方法不返回一个值,则它的返回类型为void。
在声明方法的类中,该方法的签名必须是惟一的。方法的签名由它的名称、参数的数目、每个参数的修饰符和类型组成。返回类型不是方法签名的组成部分。
1.6.5.1 参数
参数用于将值或者引用变量传递给方法。当方法被调用时,方法的参数译注5从指定的自变量(argument)译注6得到它们实际的值。c#有4种参数:值参数、引用参数、输出参数和参数数组。
值参数(value parameter)用于输入参数的传递。值参数相当于一个局部变量,它的初始值是从为该参数所传递的自变量获得的。对值参数的修改不会影响所传递的自变量。
引用参数(reference parameter)用于输入和输出参数的传递。用于引用参数的自变量必须是一个变量,并且在方法执行期间,引用参数和作为自变量的变量所表示的是同一个存储位置。引用参数用ref修饰符声明。下面的示例展示了ref参数的使用:
using system;
class test
{
static void swap(ref int x, ref int y) {
int temp = x;
x = y;
y = temp;
}
static void main() {
int i = 1, j = 2;
swap(ref i, ref j);
console.writeline("{0} {1}", i, j); //输出 "2 1"
}
}
输出参数(output parameter)用于输出参数的传递。输出参数类似于引用参数,不同之处在于调用方提供的自变量初始值无关紧要。输出参数用out修饰符声明。下面的示例展示了out参数的使用:
using system;
class test {
static void divide(int x, int y, out int result, out int remainder) {
result = x / y;
remainder = x % y;
}
static void main() {
int res, rem;
divide(10, 3, out res, out rem);
console.writeline("{0} {1}", res, rem); //输出 "3 1"
}
}
参数数组(parameter array)允许将可变长度的自变量列表传递给方法。参数数组用params修饰符声明。只有方法的最后一个参数能够被声明为参数数组,而且它必须是一维数组类型。system.console类的write和writeline方法是参数数组应用的很好的例子。它们的声明形式如下:
public class console
{
public static void write(string fmt, params object[] args) {...}
public static void writeline(string fmt, params object[] args) {...}
...
}
在方法中使用参数数组时,参数数组表现得就像常规的数组类型参数一样。然而,带数组参数的方法调用中,既可以传递参数数组类型的单个自变量,也可以传递参数数组的元素类型的若干自变量。对于后者的情形,数组实例将自动被创建,并且通过给定的自变量初始化。示例:
console.writeline("x={0} y={1} z={2}", x, y, z);
等价于下面的语句:
object[] args = new object[3];
args[0] = x;
args[1] = y;
args[2] = z;
console.writeline("x={0} y={1} z={2}", args);
1.6.5.2 方法体和局部变量
方法体指定方法调用时所要执行的语句。
方法体能够声明特定于该方法调用的变量。这样的变量被称为局部变量(local variable)。局部变量声明指定类型名、变量名,可能还有初始值。下面的示例声明了一个局部变量i,其初始值为0;另一个局部变量j没有初始值。
using system;
class squares
{
static void main() {
int i = 0;
int j;
while(i < 10){
j = i * i;
console.writeline("{0} x {0} = {1}", i, j);
i = i + 1;
}
}
}
c#要求局部变量在其值被获得之前明确赋值(definitely)。例如,假设前面的变量i的声明没有包含初始值,那么,在接下来对i的使用将导致编译器报告错误,原因就是i在程序中没有明确赋值。
方法能够使用return语句将控制返回给它的调用方。如果方法是void的,则return语句不能指定表达式;如果方法是非void的,则return语句必须包含表达式,用于计算返回值。
1.6.5.3 静态方法和实例方法
若一个方法声明中含有static修饰符,则称该方法为静态方法(static method)。静态方法不对特定实例进行操作,只能访问静态成员。
若一个方法声明中没有static修饰符,则称该方法为实例方法(instance method)。实例方法对特定实例进行操作,既能够访问静态成员,也能够访问实例成员。在调用实例方法的实例上,可以用 this来访问该实例,而在静态方法中引用this是错误的。
下面的entity类具有静态和实例两种成员:
class entity
{
static int nextserialno;
int serialno;
public entity() {
serialno = nextserialno++;
}
public int getserialno() {
return serialno;
}
public static int getnextserialno() {
return nextserialno;
}
public static void setnextserialno(int value) {
nextserialno = value;
}
}
每一个entity实例包含一个序列号(并且假定这里省略了一些其他信息)。entity构造函数(类似于实例方法)用下一个有效的序列号初始化新的实例。因为构造函数是一个实例成员,所以,它既可以访问serialno实例字段,也可以访问nextserialno静态字段。
getnextserialno和setnextserialno静态方法能够访问nextserialno静态字段,但是如果访问serialno实例字段就会产生错误。
下面的示例展示了entity类的使用:
using system;
class test
{
static void main() {
entity.setnextserialno(1000);
entity e1 = new entity();
entity e2 = new entity();
console.writeline(e1.getserialno()); //输出 "1000"
console.writeline(e2.getserialno()); //输出 "1001"
console.writeline(entity.getnextserialno()); //输出 "1002"
}
}
注意,setnextserialno和getnextserialno静态方法通过类调用,而getserialno实例成员则通过类的实例调用。
1.6.5.4 虚拟方法、重写方法和抽象方法
若一个实例方法的声明中含有virtual修饰符,则称该方法为虚拟方法(virtual method)。若其中没有virtual修饰符,则称该方法为非虚拟方法(nonvirtual method)。
在一个虚拟方法调用中,该调用所涉及的实例的运行时类型(runtime type)确定了要被调用的究竟是该方法的哪一个实现。在非虚拟方法调用中,实例的编译时类型(compile-time type)是决定性因素。
虚拟方法可以由派生类重写(override)译注7实现。当一个实例方法声明中含有override修饰符时,该方法将重写所继承的相同签名的虚拟方法。虚拟方法声明用于引入新方法,而重写方法声明则用于使现有的继承虚拟方法专用化(通过提供该方法的新实现)。
抽象(abstract)方法是没有实现的虚拟方法。抽象方法的声明是通过abstract修饰符实现的,并且只允许在抽象类中使用抽象方法声明。非抽象类的派生类需要重写抽象方法。
下面的示例声明了一个抽象类expression,它表示一个表达式树的节点;它有三个派生类constant,variablereference,operation,它们实现了常数、变量引用和算术运算的表达式树节点。
using system;
using system.collections;
public abstract class expression
{
public abstract double evaluate(hashtable vars);
}
public class constant: expression
{
double value;
public constant(double value) {
this.value = value;
}
public override double evaluate(hashtable vars) {
return value;
}
}
public class variablereference: expression
{
string name;
public variablereference(string name) {
this.name = name;
}
public override double evaluate(hashtable vars) {
object value = vars[name];
if (value == null) {
throw new exception("unknown variable: " + name);
}
return convert.todouble(value);
}
}
public class operation: expression
{
expression left;
char op;
expression right;
public operation(expression left, char op, expression right) {
this.left = left;
this.op = op;
this.right = right;
}
public override double evaluate(hashtable vars) {
double x = left.evaluate(vars);
double y = right.evaluate(vars);
switch(op) {
case '+' : return x + y;
case '-' : return x - y;
case '*' : return x * y;
case '/' : return x / y;
}
throw new exception("unknown operator");
}
}
前面的4个类用于模型化算术表达式。例如,使用这些类的实例,表达式x+3能够被表示为如下的形式:
expression e = new operation(
new variablereference("x"),
'+',
new constant(3));
expression实例的evaluate方法将被调用,以计算表达式的值,从而产生一个double值。该方法取得一个包含变量名(输入的键)和值(输入的值)的hashtable作为其自变量。evaluate方法是虚拟的抽象方法,意味着派生类必须重写它并提供实际的实现。
evaluate方法的constant的实现只是返回保存的常数。variablereference的实现在hashtable中查找变量名,并且返回相应的值。operation的实现则首先计算左操作数和右操作数的值(通过递归调用evaluate方法),然后执行给定的算术运算。
下面的程序使用expression类,对于不同的x和y的值,计算表达式x*(y+2)。
using system;
using system.collections;
class test
{
static void main() {
expression e = new operation(
new variablereference("x"),
'*',
new operation(
new variablereference("y"),
'+',
new constant(2)
)
);
hashtable vars = new hashtable();
vars["x"] = 3;
vars["y"] = 5;
console.writeline(e.evaluate(vars)); //输出 "21"
vars["x"] = 1.5;
vars["y"] = 9;
console.writeline(e.evaluate(vars)); //输出 "16.5"
}
}
1.6.5.5 方法重载
方法重载(method overloading)允许在同一个类中采用同一个名称声明多个方法,条件是它们的签名是惟一的。当编译一个重载方法的调用时,编译器采用重载决策(overload resolution)确定应调用的方法。重载决策找到最佳匹配自变量的方法,或者在没有找到最佳匹配的方法时报告错误信息。下面的示例展示了重载决策工作机制。在main方法中每一个调用的注释说明了实际被调用的方法。
class test
{
static void f() {
console.writeline("f()");
}
static void f(object x) {
console.writeline("f(object)");
}
static void f(int x) {
console.writeline("f(int)");
}
static void f(double x) {
console.writeline("f(double)");
}
static void f(double x, dpuble y) {
console.writeline("f(double, double)");
}
static void main(){
f(); //调用f()
f(1); //调用f(int)
f(1.0); //调用f(double)
f("abc"); //调用f(object)
f((double)1); //调用f(double)
f((object)1); //调用f(object)
f(1, 1); //调用f(double, double)
}
}
如上例所示,总是通过自变量到参数类型的显式的类型转换,来选择特定方法。
1.6.6 其他函数成员
类的函数成员(function member)是包含可执行语句的成员。前面部分所描述的方法是主要的函数成员。这一节讨论其他几种c#支持的函数成员:构造函数、属性、索引器、事件、运算符、析构函数。
表1.8展示一个名为list的类,它实现一个可扩展的对象列表。这个类包含了最通用的几种函数成员的例子。
表1.8 类的函数成员示例
public class list
{
const int defaultcapacity = 4;
常数
object[] items;
int count;
字段
(续表)
public list(): this(defaultcapacity) {}
public list(int capacity) {
items = new object[capacity];
}
构造函数
public int count {
get { return count; }
}
public string capacity {
get {
return items.length;
}
set {
if (value < count) value = count;
if (value != items.length) {
object[] newitems = new object[value];
array.copy(items, 0, newitems, 0, count);
items = newitems;
}
}
}
属性
public object this[int index] {
get {
return items[index];
}
set {
items[index] = value;
onlistchange();
}
}
索引器
public void add(object item) {
if (count == capacity) capacity = count * 2;
items[count] = item;
count++;
onchanged();
}
protected virtual void onchanged() {
if (changed != null) changed(this, eventargs.empty);
}
public override bool equals(object other) {
return equals (this,other as list );
}
static bool equals ( list a,list b) {
if (a == null) return b == null;
if (b == null || a.count != b.count) return false;
for (int i = 0; i < a.count; i++) {
if (!object.equals(a.item[i], b.item[i])) {
return false;
}
}
}
方法
public event eventhandler changed;
事件
public static bool operator ==(list a, list b) {
return equals(a, b);
}
public static bool operator !=(list a, list b) {
return !equals(a, b);
}
运算符
}
1.6.6.1 构造函数
c#既支持实例构造函数,也支持静态构造函数。实例构造函数(instance constructor)是实现初始化类实例所需操作的成员。静态构造函数(static constructor)是一种在类首次加载时用于实现初始化类本身所需操作的成员。
构造函数的声明如同方法一样,不过,它没有返回类型,它的名字与包含它的类名一样。若构造函数的声明中包含static修饰符,则它声明了一个静态构造函数,否则声明实例构造函数。
实例构造函数能够被重载。例如,list声明了两个实例构造函数,一个不带参数,一个带有一个int参数。使用new运算符可以调用实例参数。下面的语句使用各个list类的构造函数创建了两个list实例。
list list1 = new list();
list list2 = new list(10);
实例构造函数不同于其他方法,它是不能被继承的。并且,一个类除了自己声明的实例构造函数外,不可能有其他的实例构造函数。如果一个类没有声明任何实例构造函数,则会自动地为它提供一个默认的空的实例构造函数。
1.6.6.2 属性
属性(property)是字段的自然扩展,两者都是具有关联类型的命名成员,而且访问字段和属性的语法是相同的。然而,属性与字段不同,不表示存储位置。相反,属性有访问器(accessor),这些访问器指定在它们的值被读取或写入时需执行的语句。
属性的声明类似于字段,不同之处在于属性的声明以定界符{}之间的get访问器和/或set访问器结束,而不是分号。同时包含get访问器和set访问器的属性称为读写属性(read-write property)。只具有get访问器的属性称为只读属性(read-only property)。只具有set访问器的属性称为只写属性(write-only property)。
get访问器相当于一个具有属性类型返回值的无参数方法。除了作为赋值的目标外,当在表达式中引用属性时,会调用该属性的get访问器以计算该属性的值。
set访问器相当于一个具有单个名为value的参数和无返回类型的方法。当一个属性作为赋值的目标,或者作为++或--运算符的操作数被引用时,就会调用set访问器,所传递的自变量将提供新值。
list类声明了两个属性count和capacity,依次是只读和只写的。下面是使用这些属性的示例:
list names = new list();
names.capacity = 100; //调用set访问器
int i = names.count; //调用get访问器
int j = names.capacity; //调用get访问器
与字段和方法类似,对于实例属性和静态属性,c#两者都支持。静态属性是声明中具有static修饰符,而实例属性则没有。
属性的访问器可以是虚拟的。当属性声明中包含virtual,abstract,override修饰符时,它们将运用到属性访问器。
1.6.6.3 索引器
索引器是这样一个成员:它使对象能够用与数组相同的方式进行索引。索引器的声明与属性很相似,不同之处在于成员的名字是this,后面的参数列表是在定界符([])之间。参数在索引器的访问器中是可用的。与属性类似,索引器可以是读写、只读、只写的,并且索引器的访问器也可以是虚拟的。
list类声明了单个读写索引器,接受一个int型的参数。通过索引器就可能用int值索引list实例。例如:
list names = new list();
names.add("liz");
names.add("martha");
names.add("beth");
for (int i = 0; i < names.count; i++) {
string s = (string) names[i];
names[i] = s.toupper();
}
索引器能够被重载,意味着可以声明多个索引器,只要它们的参数个数或类型不同。
1.6.6.4 事件
事件是使对象或类能够提供通知的成员。事件的声明与字段的类似,不同之处在于事件声明包含一个event关键字,并且事件声明的类型必须是委托类型。
在包含事件声明的类中,事件可以像委托类型的字段一样使用(这样的事件不能是 abstract,而且不能声明访问器)。该字段保存了一个委托的引用,表示事件处理程序已经被添加到事件上。如果尚未添加任何事件处理程序,则该字段为null。
list类声明了名为changed的单个事件成员,changed事件表明有一个新项添加到事件处理程序列表,它由onchanged虚拟方法引发,它首先检查事件是否为null(意思是没有事件处理程序)。引发事件的通知正好等价于调用事件所表示的委托——因此,不需要特殊的语言构件引发事件。
客户通过事件处理程序(event handler)响应事件。使用“+=”运算符添加或者使用“-=”移除事件处理程序。下面的示例添加一个事件处理程序到list类的changed事件:
using system;
class test
{
static int changecount;
static void listchanged(object sender, eventargs e) {
changcount++;
}
static void main() {
list names = new list();
names.changed += new eventhandler(listchanged);
names.add("liz");
names.add("martha");
names.add("beth");
console.writeline(changecount); //输出 "3"
}
}
对于要求控制事件的底层存储的更高级场景译注8,事件的声明可以显式地提供add和remove访问器,它们在某种程度上类似于属性的set访问器。
1.6.6.5 运算符
运算符(operator)是一种函数成员,用来定义可应用于类实例的特定表达式运算符的含义。有三种运算符能够被定义:一元运算符、二元运算符和转换运算符。所有的运算符必须声明为public和static。
list类声明了两个运算符,运算符 “==”和运算符 “!=”,并且向表达式赋予新的含义,而这些表达式将这些运算符应用到list实例上。特别指出,这些运算符定义了两个list对象的相等比较,即使用它们的equals方法进行比较。下面的示例使用“==”运算符比较两个list实例。
using system;
class test
{
static void main() {
list a = new list();
a.add(1);
a.add(2);
list b = new list();
b.add(1);
b.add(2);
console.writeline(a == b); //输出 "true"
b.add(3);
console.writeline(a == b); //输出 "false"
}
}
第一个console.writeline输出true,原因是两个list集合对象包含个数和值都相同的对象。假如list没有定义运算符 “==”,那么第一个console.writeline将输出false,因为a和b引用不同的list实例。
1.6.6.6 析构函数
析构函数(destructor)是用于实现析构类实例所需操作的成员。析构函数不能带参数,不能具有可访问性修饰符,也不能被显式地调用。垃圾回收期间会自动调用所涉及实例的析构函数。
垃圾回收器在决定何时回收对象和运行析构函数方面采取宽松的策略。特别指出,析构函数的调用时机是不确定的,并且析构函数可能运行在任何线程上。由于这些或者其他原因,只有没有其他可行的解决方案,类才实现析构函数。