C#简介- 类和对象,选自《C#编程语言详解》,送给新手

** 类( 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);

}

Published At
Categories with Web编程
Tagged with
comments powered by Disqus