总体要求
掌握静态类与静态成员的定义与使用
理解类的集成与多态性,掌握其应用方法
理解抽象类、接口的概念,掌握抽象类与接口的定义及使用方法
理解嵌套类、分部类和命名空间的概念,掌握它们的使用方法
相关知识点
熟悉C#的结构、类、数组的区别
熟悉类及其成员的定义与使用
学习重点
静态成员与静态类
类的继承性与多态性
抽象类与接口的定义与使用
学习难点
静态成员的作用,静态方法和实例方法的区别
多态的概念和实现,虚方法和抽象方法的区别
接口的作用和使用,抽象类和接口的区别
5.1静态成员与静态类
通常,“类”只是统一了其所有实例的定义格式,也就是规定了诸如字段、属性等成员的数据类型、名字和可访问性,规定了方法的返回值类型、名字、参数和可访问性等。“类”一般不包含数据信息,真正的数据信息属于类的特定实例(对象)。例如,若Student类的实例a的name值为“令狐冲”,age值为“21”,则这些数据只属于对象a,同样,若实例b的name值为“郭靖”,age值为“20”,则这些数据只属于对象b。
那么,数据信息有没有可能属于类,而不属于特定的实例呢?答案是肯定的。例如,若要确定Student类一共有多少个对象,可以定义一个变量number来记录。显然,number不属于特定实例,而是属于整个类。在C#中,为了区别属于特定实例的成员,要求把所有属于类的成员定义为静态成员。
5.1.1类的静态成员
静态成员通过static关键字来标识,可以是静态方法、字段、属性等。
静态成员与非静态成员的区别在于:前者属于类,而不属于类的实例,因此必须通过类来访问,而不能通过类的实例来访问;后者则总是与特定的实例(对象)相联系。
在实际应用中,当类的成员所引用或操作的信息与类有关而与类的实例无关时,就应该将它设置为静态成员。例如,想统计同类对象的数量,就可使用静态字段和静态方法来实现。
【实例5-1】利用静态成员统计人数。

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } //创建Student型的数组ps,用来记录5个人的信息 Student[] ps = new Student[5]; public class Student { public static int males = 0; //记录男生人数 public static int females = 0; //记录女生人数 public static int number = 0; //记录总人数 public string Name; public char Sex; public int Age; //构造函数,用来初始化对象 public Student(string name,char sex,int age) { Name = name; Sex = sex; Age = age; if (sex == '男') males++; if (sex == '女') females++; } //静态方法,返回男生人数 public static int NumberOfMales() { return males; } //静态方法属性,返回女生人数 public static int NumberOfFemales { get{ return females; } } } private void button1_Click(object sender, EventArgs e) //将输入保存到数组 { char sex = char.Parse(textBox2.Text); int age = int.Parse(textBox3.Text); ps[Student.number] = new Student(textBox1.Text, sex, age); Student.number++; //静态成员只能通过类名引用 richShow.Text = string.Format("添加成功:{0}人", Student.number); } private void button2_Click(object sender, EventArgs e) { richShow.Text += string.Format("\n男生人数:{0}", Student.NumberOfMales()); richShow.Text += string.Format("\n女生人数:{0}", Student.NumberOfFemales); richShow.Text += string.Format("\n学生名单如下:\n"); foreach(Student p in ps) { if (p != null) richShow.Text += string.Format("{0}", p.Name); } } } }
PS:使用静态成员时,要注意以下几点。
(1)静态成员属于类,只能通过类名引用,而不能通过对象名引用(如 Student.number)因此C#中表示当前实例的关键字this不能再静态方法中使用。
(2)静态数据成员再所有对象之外单独开辟内存空间,只要在类中定义了静态数据成员,即使没有类的实例化操作,系统也会为静态成员分配内存空间,因此允许随时引用。例如,在实例5-1中,如果单击“统计”按钮,将显示“男生人数:0,女生人数:0“。
(3)非静态方法也叫实例方法。在实例方法中,可以直接访问实例成员和实例方法,也可以直接访问静态成员和静态方法。但在静态方法中,只能访问静态成员,不可以直接访问实例成员,也不能直接调用实例方法。
5.1.2静态构造函数
类的构造函数也可以时静态的,静态构造函数不是为了创建对象而设计的,二十用来初始化类的静态字段的。请牢记,只有非静态的构造函数才用来创建对象。用于创建对象的构造函数也称为实例构造函数。静态构造函数因为并不对类的特定实例进行操作,所以也被称为全局构造函数或共享构造函数。
在C#应用程序中,不能直接调用静态构造函数。在类的第一个实例创建之前或者调用类的任何静态方法之前,系统会自动执行静态构造函数,而且最多执行依次。因此,静态构造函数适合于对类的静态数据成员进行初始化。
静态构造函数可以与实例构造函数共存,其一般形式如下:
static 静态构造函数名() { //语句 }
其中,静态构造函数名与类名相同,声明静态构造函数时不能带访问修饰符(如public),并且不能有任何参数列表和返回值。
例如,可在实例5-1的基础上增加一个静态构造函数,实现三个静态字段变量的初始化,代码如下。
public class Student { public static int males = 0; //记录男生人数 public static int females = 0; //记录女生人数 public static int number = 0; //记录总人数 static Student() //静态构造函数,用来初始化静态字段 { males=0;females=0;number=0; } //....其他代码 }
注意,静态构造函数不支持重载,也就是不允许定义多个静态构造函数。
5.1.3静态类
当类只包含静态成员时,C#建议用static关键字把它声明为静态类。由于静态类仅包含静态成员,因此完全没有必要将它实例化。事实上,C#也不允许使用new关键字来创建静态类的实例。在实际应用中,只要类的成员与特定对象无关,就可以把它创建为静态类。
静态类的特点:
(1)静态类仅包含静态成员
(2)静态类不能被实例化
(3)静态类是密封的
(4)静态类不能包含实例构造函数
由于静态类是密封的,因此不可能被继承。静态类不能包含实例构造函数,但仍可声明静态构造函数。
静态类的优点如下:
(1)编译器能够自动执行检查,以确保不添加实例成员
(2)静态类能够使程序的实现更简单、迅速,常用的静态类有Console和Math。其中,Console、提供了与控制台操作有关的各种方法,实现控制台应用程序的输入和输出操作。Math提供了与数学有关的各种函数运算。表5-2和表5-3分别罗列了Console类和Math类的常用内部成员及其功能。
表5-2 Console类及其成员
成员名称 | 功能说明 |
Beep() | 通过控制台扬声器播放提示音 |
Clear() | 清除控制台缓冲区和相应的控制台窗口的显示信息 |
Read() | 从标准输入流读取下一个字符 |
ReadLine() | 从标准输入流读取下一行字符 |
Write(data) | 将指定数据data写入标准输出流,其中data可以是布尔值、字符、整数、小数、字符、字符串等 |
WriteLine(data) | 将指定数据data写入标准输出流并换行,其中data可以是布尔值、字符、整数、小数、字符串等 |
表5-4 Math类及其成员
成员名称 | 功能说明 |
Abs() | 返回绝对值 |
Acos() | 返回余弦值为指定数字的角度 |
Asin() | 返回正弦值为指定数字的角度 |
Atan() | 返回正切值为指定数字的角度 |
Ceiling() | 返回大于或等于指定的十进制数的最小整数值 |
Cos() | 返回指定角度的余弦值 |
Exp() | 返回e的指定次幂 |
Floor() | 返回小于或等于指定数的最大整数 |
Log() | 返回指定数字的自然对数(底为e) |
Log(a,b) | 返回指定数字在使用指定底时的对数,例如求log28写成Log(8,2) |
Log10 | 返回指定数字以10 为底的对数 |
Max(a,b) | 返回两个数中较大的一个 |
Min(a,b) | 返回两个数中较小的一个 |
Pow(x,n) | 返回指定数字的指定次幂,例如xn 写成Pow(x,n) |
Round() | 将小数值舍入到最接近的整数值 |
Sin | 返回之地角度的正弦值 |
Sqrt | 返回指定数字的平方根 |
Tan | 返回指定角度的正切值 |
5.2 类的继承
类的继承性是指在一个已存在的类的基础之上定义的一个新的类。其中,这个已经存在的类称为基类或者父类,而新定义的类称为派生类或子类。在C#中,当派生类从基类派生时,派生类就具有基类中的所有成员,这样,基类中已定义的成员代码,不需要再派生类定义中重写,在派生类的定义中,只需添加自己的成员即可。这样,既提高了代码的重用性,从而提高了程序涉及的效率,又提供了已有程序设计的可扩展性。
类的继承性性为面向对象程序设计构件一个分层结构体系创造了条件。例如,.NET Framework类库就是一个庞大的分层类结构体系。其中Object类是一个最上层的基类,其他所有类都是直接或间接由Object类派生而来的。即使用户自定义的类没有指定继承关系,系统仍然将该类作为Object类的派生类。
在C#中,类的继承遵循以下原则:
(1)派生类只能从一个类中继承,即单一继承
(2)派生类自然继承基类的成员,但不能继承基类的构造函数
(3)类的继承可以传递,例如,假设类C继承于类B,类B又继承类A,那么C类即具有类B和类A的成员,可以认为类A是类C的祖先。
5.2.1派生类的声明
在C#中,派生类可以拥有自己的成员,也可以隐式地从它的基类继承所有成员,包括方法、字段、属性和事件,但私有成员、构造函数和析构函数等除外。另外,派生类只能从一个类的继承,即单一继承。
C#中声明派生类的一般形式如下:
[访问修饰符] class 类名:基类名
{
//类的成员;
}
例如:
public class Animal //这是一个基类 { protected string name; //基类的数据成员 protected int age; public string Eat() //基类的方法 { return string.Format("动物{0}:我要吃东西!",name); } } public class Dog:Animal //这是一个派生类 { private string type; //派生类数据成员 public string GetMessage() //派生类方法 { return string.Format("狗狗{0}:我是{1},今年{2}岁了",name,type,age); } }
其中,Dog类继承了Animal类的所有成员,包括字段成员(name和age)、方法成员(Eat),同时Dog类也扩展了Animal类,具有Animal类没有的字段成员(type)和方法(GetMessage)。
基类在定义数据成员name和age时,使用了访问修饰符protected,而如果使用private修饰符,则只能由所属类的成员才能访问,无法在派生类中被访问。使用public修饰符不光可以在派生类中被访问,同时也可以在类外被访问。而由protectde声明的成员,只能在所属类及其派生类的成员访问,所以通常用protected修饰符限定基类成员 ,这样既保证了不能再类定义外直接访问成员,又允许其派生类成员访问。
5.2.2构造函数
在C#中,因为派生类不能继承其他基类的构造函数,因此通常需要为派生类定义构造函数。此时,基类的构造函数和派生类的构造函数各司其职,即基类的构造函数负责初始化基类的成员字段,派生类的构造函数只初始化新添加的成员字段。在创建派生类对象时,系统会使用它们来初始化对象的所有成员字段;调用顺序是先调用基类的构造函数,完成基类部分的成员初始化,再调用派生类的构造函数,完成派生类的新添加的成员初始化。
由于类的继承具有传递性,例如,当类C继承于类B,类B又继承类A时,若创建类C的实例,则类A、B、C的构造函数都会被调用,调用次序按由高到低顺序依次调用,即先调用A的构造函数,再调用B的构造函数,最后调用C的构造函数。
1、无参数的默认构造函数
由于构造函数可重载,基类的构造函数可能有若干个,因此在这种情况下,在创建派生类的实例时,系统将自动调用不带参数的默认构造函数。
例如,
class Father { protected string name; //基类的字段 public Father() //基类的构造函数 { name="父亲"; } } class Son:Father { int age; //派生类的新成员 public Son() //派生类的构造函数 { age=0; } public string getInfo() { return string.Format("{0},今年{1}岁",name,age); } }
若执行语句“Son son=new Son();”则系统自动先调用基类Father的构造函数,将数字段name的初始值设置为“父亲”,再调用自己的构造函数,将字段age的初始值设置为0。因此,若继续执行语句“son.getInfo();”,则只能显示以下信息:“父亲,今年0岁”。
2、带参数的构造函数
从上述代码可知,通过调用无参数的默认的构造函数来创建对象,得到的初始数据没有实际意义。为此,需要重载构造函数,为构造函数指定参数,从而创建具有意义的对象。
当基类的构造函数带参数时,由于系统只能自动调用默认构造函数,因此再创建派生类的实例时必须强迫系统调用基类的带参数的构造函数。为此,在声明派生类的构造函数时必须使用base关键字向基类的构造函数传递参数。
其格式如下:
public派生类构造函数名(形参列表):base(向基类构造函数传递的参数列表){}
例如,
class Father { protected string name; //基类的字段 public Father(string name) //基类的构造函数 { this.name =name; } } class Son:Father { int age; //派生类的新成员 public Son(string name,int age):base(name) //派生类的构造函数 { this.age=0; } public string getInfo() { return string.Format("{0},今年{1}岁.",name,age); } }
若执行语句“Son son =new Son("儿子",18);”则系统先将字符串“儿子”传递给派生类的构造函数的形参变量name,再调用基类的构造函数,把name作为实参传递给基类Father的构造函数的形参变量name,从而将字段name的初始值设置为“儿子”,最后调用自己的构造函数,将字段age的初始值设置为18,参数传递过程如图5-2所示。因此,若继续执行“son.getInfo();”,则只能显示以下信息:“儿子,今年18岁”。
图5-2 构造函数的参数传递
【实例 5-2】 调用基类带参构造函数演示
源代码:

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class 实例5_2 : Form { public 实例5_2() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { Dog d; if (textBox1.Text == "") d = new Dog(); //创建派生类对象,调用默认构造函数 else { int age = Convert.ToInt32(textBox2.Text); d = new Dog(textBox1.Text, age, textBox3.Text); //调用带参数的构造函数 } lblShow.Text = d.GetMessage(); lblShow.Text += "\n\n" + d.Eat(); } public class Animal //这是一个基类 { protected string name; //基类的数据成员 protected int age; public Animal() //基类的默认构造函数 { this.name = "未知"; this.age = 0; } public Animal(string name,int age) //基类的带参构造函数 { this.name = name; this.age = age; } public string Eat() //基类的方法 { return string.Format("动物{0}:我要吃东西!", name); } } public class Dog : Animal //这是一个派生类 { private string type; //派生类数据成员 public Dog() //派生类默认构造函数 { type = "未知"; } public Dog(string name,int age,string type) : base(name, age) //调用基类的带参数构造函数 { this.type = type; } public string GetMessage() { return string.Format("狗狗({0}):我是{1},今年{2}岁了。", name, type, age); } } } }
在本例中,由于基类Animal和派生类Dog都包含两个构造函数,一个时无参的默认构造函数,另一个是有参的构造函数。因此,在创建派生类Dog的实例时,若不指定参数,则系统自动调用默认构造函数,经初始化后输出的信息如图5-3所示。若指定了参数,则通过base关键字来调用基类Animal的构造函数,初始化从基类继承的字段,而派生类的构造函数只负责对自己扩展的字段进行初始化,之后输出的信息如图5-4所示。
图5-3 调用基类默认构造函数运行效果
图 5-4 调用基类带参的构造函数运行效果
在本例中,基类和派生类都定义了默认构造函数,因此可调用默认构造函数或带参数的构造函数创建派生类的实例。但是,如果基类只有带参数的构造函数,而没有默认构造函数,那么该如何定义派生类的默认构造函数呢?答案仍然是通过base关键字来调用基类的带参数的构造函数,代码如下所示
public Dog():base("未知",0) //派生类的默认构造函数 { type="未知"; }
5.2.3密封类
为了阻止一个类的代码被其他类继承,可以使用密封类,因为在.NET中,加载密封类时将对密封类的方法调用进行优化,因此使用密封类可以提高应用程序的可靠性和性能。另外,软件开发者通过使用密封类还可以把自己的知识产权保护起来,避免他人共享代码。
在C#中,如果在声明Animal类时添加关键字sealed。
public sealed class Animal //这是一个密封类
{
...
}
则Dog类就无法继承Animal类,其所有代码需要重新书写。
5.3类的多态性
多态的字面意思是事物有多种形态,其实质是不同事物在发展过程中逐渐体现出来的差异性。例如,打印机能在纸张上打印文字或图案,而以打印技术为基础开发的3D打印机能打印出实际的物品来。
多态性是面向对象程序设计的一个重要特征,它体现为一个派生类对基类的特征和行为的改变,表面山上给看这些特征或行为还是相似的。例如,子女遗传了父母的相貌和性格,表面上相似,实质上仔细对比区别很大。也就是说,当派生类从基类继承时,派生类不仅会获得基类的所有字段、属性和方法等成员,还会扩展基类的成员,甚至会重写基类的成员,以更改基类的数据和行为。
为了使用派生类能更改基类的数据和行为,C#提供了两种选择:一是使用新的派生成员替换基类成员,二是重写虚拟的基类成员。
5.3.1使用new重新定义类的成员
使用new关键字来定义于基类中同名的成员,即可替换基类的成员。如果基类定义了一个方法、字段或属性,则new关键字用于在派生类中创建该方法、字段或属性的全新定义。注意:把new关键字放置在要替换的类成员的数据类型之前。
例如,在实例5-2的派生类Dog中,添加以下代码:
public new string Eat() //重新定义方法成员 { return string.Format("动物{0}:我要吃骨头!", name); }
此时,派生类Dog方法Eat替换了基类Animal的方法Eat。若执行以下语句:
Dog d=new Dog();
lblShow.Text=d.Eat();
则调用的是新的类成员方法,而不是基类的成员方法。因此,最终得到以下结果:
狗狗未知:我要吃骨头!
5.2.3 用virtual和override定义类的成员
使用new关键字在派生类中重写基类的成员,实际上是对基类中的相应代码的彻底废除。这如同一个人通过器官移植手术将通过遗传得到的组织器官给替换了一样,因此有些人认为这不是真正的面向对象的多态性。不过由于它所达到的效果于下面要讨论的重载虚拟方法的效果差不多,所以把它放在类的多态性中阐述。
在C#中,要想实现真正的多态性,可采用以下步骤:首先在基类中用virtual关键字声明类的成员(这种成员称为虚拟成员),然后再派生类中用override关键字重载虚拟成员或覆盖虚拟成员。
1、虚拟法及其重载
在基类中声明虚方法的格式如下:
public virtual 返回值类型 方法名称([参数列表])
{
//方法体语句
}
在派生类中覆盖虚拟方法的格式如下:
public override 返回值类型 方法名称([参数列表])
{
//方法体语句
}
其中,基类于派生类中的方法名称于参数列表必须完全一致,当不需要参数时省略参数列表。
例如:
public class Animal //这是一个基类 { //... ...其他代码 public virtual string Eat() //基类的虚方法 { return string.Format("动物{0}:我要吃东西!", name); } } public class Dog : Animal //这是一个派生类 { //... ... 其他代码 public override string Eat() //派生类的覆盖基类的虚方法 { return string.Format("动物{0}:我要吃骨头!", name); } }
2、虚属性及其重载
在基类中声明虚属性的格式如下:
public virtual 返回值类型 属性名称
{
//属性体
}
在派生类中覆盖虚属性的格式如下:
public override 返回值类型 属性名称
{
//属性体
}
其中,必须保证在基类和派生类中的属性的定义格式完全一致,包括可访问性、返回值类型、属性名称和属性体。属性体由get和set访问器组成,省略set表示只读属性,省略get表示只写属性,不能同时省略get和set访问器。
例如:
public class Animal //这是一个基类 { //... ...其他代码 public virtual string Name //基类的虚属性,是一个只读属性 { get{
if(name==""or name==null) //name 是字段成员
return "该动物未起名!";
else
return name;
} } } public class Dog : Animal //这是一个派生类 { //... ... 其他代码 public override string Name //派生类的覆盖基类的虚属性 {
get{
if(name==""or name==null) //name 是字段成员
return "该狗狗未起名!";
else
return name;
}
}
}
注意,覆盖与替换是不一样的。例如,餐桌上已经由一张台布,撤下这张台布再铺上新的台布叫替换,而不撤下原来的台布直接在上面再铺上一张台布叫覆盖。C#中的替换(即 new)操作发生再程序编译之时,覆盖(即override)操作发生在程序运行之时。
使用virtual和override时要注意以下几点。
(1)字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的
(2)使用virtual修饰符后,不允许再使用static、abstract或override修饰符
(3)派生类对象即使被强制转换未基类对象,所引用的仍然是派生类的成员
(4)派生类可以通过密封来停止虚拟继承,此时派生类的成员使用sealed override声明。
5.3.3 访问基类的成员
1、基类与派生类之间的转换
C#允许把派生类转换为基类,但不允许把基类转换为派生类。这样,一个基类的对象既可以指向基类的实例,也可以指向派生类的实例。
例如,以下语句都是合法的。
Animal a=new Animal(); //a指向基类实例
Animal b=new Dog(); //b指向派生类实例
Dog d=new Dog(); //d指向派生类实例
a=d; //a指向派生类实例
当基类的对象指向派生类实例时,系统将进行隐式转换,把数据类型从派生类转换为基类。例如,在“Animal b=new Dog();”中,虽然b指向了派生类的实例,但他的数据类型还是基类。此时,若通过基类对象 来调用一个基类与派类都具有的同名的方法,则系统将调用基类的方法,而不会调用派生类的方法。
例如,
public class Animal //这是一个基类 { //... ...其他代码 public string Eat() //基类的方法 { return string.Format("动物{0}:我要吃东西!", name); } } public class Dog:Animal //这是一个派生类 { //... ... 其他代码 public new string Eat() //派生类的替换基类的同名方法 { return string.Format("动物{0}:我要吃骨头!", name); } }
若执行“Animal b=new Dog();b.Eat();”,则调用Animal中的Eat()方法,因此返回的类似“动物....:我i要吃东西!”的信息,而不会返回类似“狗狗......:我要啃骨头”的信息。
注意,当基类的对象指向派生类的实例时,虽然其数据类型被转换成了基类,但其本质仍然没有改变,还是派生类的实例,因此可以再次强制转换为派生类型。
例如,以下语句时合法的。
Animal a=new Dog(); //a指向派生类实例
Dog d =(Dog)a; //把a的类型强制转换为Dog,再复制给d
【思考】以下4条语句是否合法?
Dog x =new Animal();
x=(Dog)a;
Dog d= new Dog();
((Animal)d).Eat();
【答案】前两条非法,后两条合法,其原因是C#不允许把基类的实例隐式或强制转换成派生类,但允许把派生类的实例强制转换成基类。
2.在派生类中调用基类的成员
当派生类重载或覆盖基类方法后,如果想在派生类中调用基类的同名方法,可以使用base关键字。
例如,在Dog类的Eat方法中,希望使用基类的Eat方法,可以通过base来调用,代码如下所示。
public override void Eat()
{
base.Eat();
}
3.类的多态性的意义
C#允许基类的对象引用派生类的实例,一旦使用virtual和override实现类的多态性,那么系统将具有自适应的能力,它会更具对象所引用的基类的实例,还是派生类的实例来自动调用覆盖之前还是覆盖之后的方法。这样,对象引用将变得更加灵活。
【实例 5-3】 虚方法演示

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class 实例5_3 : Form { public 实例5_3() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { int age = Convert.ToInt32(textBox2.Text); Animal a = new Animal(textBox1.Text, age); lblShow.Text = AnimalEat(a); //调用方法,实参为基类对象 } private void button2_Click(object sender, EventArgs e) { int age = Convert.ToInt32(textBox2.Text); Dog d = new Dog(textBox1.Text, age, textBox3.Text); lblShow.Text = AnimalEat(d); //调用方法,实参为派生类对象 } private string AnimalEat(Animal x) //可接收基类型的实参,也可以接收派生类型的实参 { return x.Eat(); } public class Animal //这是一个基类 { protected string name; //基类的数据成员 protected int age; public Animal (string name,int age) { this.name = name; this.age = age; } public virtual string Eat() //基类的方法 { return string.Format("动物{0}:我要吃东西!", name); } } public class Dog:Animal //这是一个派生类 { private string type; //派生类数据成员 public Dog(string name,int age,string type) : base(name, age) { this.type = type; } public string GetMessage() //派生类方法 { return string.Format("狗狗({0}):我是{1},今年{2}岁了。",name,type,age); } public override string Eat() //派生类覆盖基类方法 { return string.Format("狗狗({0}):我要吃骨头!",name); } } } }
【分析】在该程序中定义了一个方法:private void AnimalEat(Animal animal)。该方法的形参是Animal型对象x,该方法在被调用时,可以接收Animal型的实参,也可以接收Animal的派生类型的实参。由于整个程序实现类的多态性,系统能根据所接收的实参类型来自动调用相应类的方法,因此当单击“创建基类对象并调用方法”按钮时,以基类对象作为实参,将调用基类的Eat方法,运行效果如图5-5所示;当单击“创建子类对象并调用方法”按钮时,以派生类对象作为实参,将调用派生类的Eat方法,运行效果如图5-6所示。
图 5-5 创建基类对象并调用方法运行效果
图 5-6 创建子类对象并调用方法运行效果
5.4抽象类
虽然通过重载基类的虚成员可以实现多态,但是虚成员仍然是一个完整的已经实现了具体操作功能的成员。实际上,有些操作是不可能实现的。例如,有关几何形状体积计算问题,若把几何形状定义为类,把体积计算方法定义为方法,显然该方法就是不可能实现的,只有一个具体的几何形状的体积才能计算,如球体的体积计算、圆柱体的体积计算等。
在C#中,凡是包含无法实现的成员的类就是抽象类,其中那些无法实现的操作就是类的抽象成员。显然,抽象类包含抽象成员,但也可以声明非抽象成员,甚至还声明虚成员。注意,抽象成员必须在抽象类中声明,但抽象类不要求必须包含抽象成员。
5.4.1抽象类及其抽象成员
1、抽象类与抽象方法
抽象方法是指在基类的定义中,不包含任何实现代码的方法,实际上就是一个不具有任何具体功能的方法。这样的方法唯一的作用就是让派生类来重写。
在C#中,抽象类和抽象方法使用关键字abstract声明,一般形式如下:
public abstract class 抽象类名
{
[访问修饰符] abstract 返回值类型 方法名([参数列表]);
}
例如,下面定义了一个代表几何形状的抽象类。
public abstract class Shape { protected double radius; public Shape(double r) //构造函数 { radius = r; } public abstract double Cubage(); //声明抽象方法,用来计算体积 }
声明抽象方法时,抽象方法没有方法体,只有在方法声明后跟一个分号,加上例中的Cubage()方法。一个类只要包含抽象方法,该类就必须定义成为一个抽象类,如果将上例中Shape类前面的abstract去掉,程序将无法通过编译,会出现“Cubage()是抽象的,但它包含在抽象Shape中”的错误提示。
抽象类只能当作基类使用,而不能直接实例化。例如,若出现类似“Shape s =new Shape(5);”的语句,编译时将出现“无法创建抽象类Shape的实例”的错误。同时,抽象类不能是密封或静态的,也就是说,只能用abstract关键字来标识。
抽象类的用途是提供多个派生类可共享的基类的公共定义。例如,一旦在几何形状Shape类中声明求体积的计算方法Cubage(),则未来以此类为基类的所有派生类在实现求体积的计算方法时都必须按Cubage()方法的声明去书写代码,这样将保证所有代码的格式是统一的、规范的。
2、抽象类与抽象属性
除了抽象方法,一个抽象类也可以包含抽象属性。类的属性成员添加了abstract关键字后,就成了抽象属性。抽象属性不提供具体实现,它只声明该属性的数据类型、名字、可访问性等,而具体实现代码留给派生类。抽象属性同样可以是只读的、只写的或可读的属性。
抽象属性的一般形式如下:
public abstract 数据类型 属性名
{
get;
set;
}
例如,下面的代码包含一个能返回几何形体腰围的抽象的只读属性。
public abstract class Shape { protected double radius; public Shape(double r) //构造函数 { radius = r; } public abstract double Cubage(); //声明抽象方法,用来计算体积 public abstract double Length //声明只读的抽象属性,用来返回几何形体的腰围 { get; } }
5.4.2重载方法
抽象类中的抽象方法和抽象属性都没有提供实现,因此在定义抽象类的派生类时,派生类必须重载基类的抽象方法和抽象属性。如果在派生类中没有重载,则派生类也必须声明为抽象类,即在类定义前加上abstract。这一点是与虚方法不同的,因为对于基类的虚方法,其派生类可以不重载。重载抽象类的方法和属性必须使用override关键字。重载抽象方法的格式为:
public override 方法名称([参数列表]){}
其中,方法名称和参数列表必须与抽象类中的抽象方法完全一致。
【实例 5-4】抽象方法和抽象类演示

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class 实例5_4 : Form { public 实例5_4() { InitializeComponent(); } private void display(Shape s) //显示几何形体的体积,该方法的形参类型时抽象类 { label3.Text = "体积为:" + s.Cubage(); } public abstract class Shape //定义抽象类,表示几何形状 { protected double radius; public Shape(double r) //构造函数 { radius = r; } public abstract double Cubage(); //声明抽象方法,用来计算体积 } public class Globe : Shape //定义派生类Globe(圆球体) { public Globe(double r) : base(r) { } //构造函数 public override double Cubage() //重载抽象方法 { return 3.14 * radius * radius * radius * 4.0 / 3; ; } } public class Cone : Shape //定义派生类Cone(圆锥体) { private double high; public Cone(double r,double h):base(r) //构造函数 { high = h; } public override double Cubage() { return 3.14 * radius * radius * high / 3; } } public class Cylinder : Shape //定义派生类Cylinder(圆柱体) { private double high; public Cylinder(double r,double h):base(r) //构造函数 { high = h; } public override double Cubage() //重载抽象方法 { return 3.14 * radius * radius * high; } } private void button1_Click(object sender, EventArgs e) { double r = Convert.ToDouble(textBox1.Text); Globe g = new Globe(r); //创建球体对象 display(g); //显示球体体积 } private void button2_Click(object sender, EventArgs e) { double r = Convert.ToDouble(textBox1.Text); double h = Convert.ToDouble(textBox2.Text); Cone c = new Cone(r, h); //创建圆锥对象 display(c); //显示圆锥体积 } private void button3_Click(object sender, EventArgs e) { double r = Convert.ToDouble(textBox1.Text); double h = Convert.ToDouble(textBox2.Text); Cylinder c = new Cylinder(r, h); //创建圆柱对象 display(c); //显示圆柱体积 } } }
【分析】其中,基类Shape的Cubage方法为抽象方法,所以Shape也定义为抽象类,而派生类Globe、Cone和Cylin分别重载了Cubage方法。当单击“圆球”、“圆锥”或“圆柱”按钮时,将分别创建Globe、Cone或Cylinder对象,并将其作为实参传给display方法,显示不同几何形状的体积,图5-7为单击“圆锥”按钮时的运行效果。
图 5-7 单击“圆锥”按钮时的运行效果
5.5接口
在现实生活中,常常需要一些规范和标准,如汽车轮胎坏了,只需更换一个同样规格的轮胎,计算机的硬盘要升级,只需买一个有同样接口和尺寸的硬盘进行更换,而一个支持USB接口的设备如移动硬盘、MP3、手机等都可以插入计算机的USB接口进行数据传输,这些都是由于有一个统一的规范和标准,轮胎、硬盘和USB就可以互相替换或连接。在软件开发领域,也可以定义一个接口规定一系列规范和标准,继承同一接口的程序也就遵循同一种规范,这样程序可以互相替换,便于程序的扩展。
接口(interface)是C#的一种数据类型,属于引用类型。一个接口定义一个协定。接口可以包含方法、属性等成员,它只描述这些成员的签名(即成员的数据类型、名称和参数等),不提供任何实现代码,具体实现由继承该接口的类来实现。实现某个接口的类必须遵守该接口定义的协定,即必须按接口所规定的签名格式来进行实现,不能修改签名格式。
5.5.1接口的声明
在C# 中,声明接口使用interface关键字,一般形式如下:
[访问修饰符] interface 接口名
{
//接口成员
}
其中,访问修饰符只能使用public和internal,默认为public,可以省略;接口名的命名规则与类名的命名规则相同,为了与类相区别,建议使用大写字母I打头。接口可以继承其他接口,基接口列表表示其继承的接口名。
接口成员可以是属性、方法等,不能包含常量、字段、构造函数和析构函数,是。所有接口成员隐式地具有了public访问修饰符,因此,不能为接口成员添加任何访问修饰符。
例如:
interface IUsb { int MaxSpeed { get; } string TransData(string from, string to); }
上述代码定义了一个名为IUsb的接口,它规定了只读属性MaxSpeed和方法成员TransData的签名格式。
5.5.2接口的实现
接口主要用来定义一个规则,让企业内部或行业内部的软件开发人员按标准去实现应用程序的功能。在C#中,一个接口的派生类必须实现该接口声明的所有成员。
例如,派生类MP3从接口IUsb接口派生,它实现该接口的所有成员,代码如下:
public class Mp3 : IUsb { public int MaxSpeed { get { return 480; } } public string TransData(string from,string to) { return string.Format("数据传输:从{0}到{1}",from,to); } }
在上述代码中,Mp3实现了IUsb接口规定的TransData方法和MaxSpeed属性,而如果删除TransData方法的实现,编译时将出现“MP3不实现接口成员IUsb.TransData(string,string)”的错误。
在C#中,结构型也可以从接口派生。例如,将上述代码中Mp3前面的class修改为struct也是正确的。不过,请读者注意结构型和类的区别。在C#中,结构型属于值类型,它不具备面向对象的特性,从继承性的角度来看,仅限于从接口派生,无法从一个结构型派生一个新的结构型。相反,类属于引用类型,完全体现面向对象的思想。因此,在使用C#开发应用软件时应尽量使用类,而不使用结构型。
5.5.3接口的继承性
在C#中,接口本身也支持继承性,也就是说可以从一个接口派生新的接口。与类的继承性不同,类只支持单一继承,而接口支持多重继承,即一个接口可以从多个基接口派生,基接口名之间用逗号分隔。
例如,
interface IUsb { int MaxSpeed { get; } string TransData(string from, string to); } interface IBluetooth { int MaxSpeed { get; } string TransData(string from, string to); } interface IMp3 : IUsb, IBluetooth { string Play(string mp3); }
其中,本例中的IMp3接口继承了IUsb和IBluetooth两个接口,同时还添加了一个新的方法成员。这样,IMp3接口支持USB数据传送,也支持Bluetooth数据传送,还支持MP3播放。
5.5.4多重接口实现
C#不允许多重类继承,但是C#允许多重接口实现,这意味着一个类可以实现多个接口,即一个类可以从多个基接口派生,各基接口之间用逗号分隔。
例如,
public class Mobile:IUsb,IBluetooth
{
//其他代码
}
就表示Mobile类同时实现IUsb和IBluetooth接口,因此既支持USB功能,也支持Bluetooth功能。
C#允许类同时从基类和基类接口派生,但要求类名必须位于基接口名的前面。
例如,
public class Mobile:Phone,IUsb,IBluetooth
{
//其他代码
}
就表示Mobile类既是从Phone基类派生的类,也是实现了IUsb和IBluetooth接口的派生类。再次强调,基类必须在所有的接口之前。
当类继承的多个接口中存在同名的成员时,在实现时为了区分是从哪个接口继承来的,C#使用“接口名称.接口成员”格式数显代码(称为显示实现)。显示实现的成员不能带任何访问修饰符,也不能通过类的实例来引用或调用,必须通过所属的接口来引用或调用。
例如,上例中的IUsb和IBluetooth有同名的TransData方法和MaxSpeed属性,为了区分必须显示实现,代码如下:
public abstract class Phone //这是一个抽象基类 { public abstract string Call(string name); //抽象方法 } public class Mobile : Phone, IUsb, IBluetooth //这是一个派生类 { int IUsb.MaxSpeed //显示实现IUsb的MaxSpeed属性 { get { return 480; } } string IUsb.TransData(string from, string to) //显示实现IUsb的TransData方法 { return string.Format("USB数据传输:从{0}到{1}", from, to); } int IBluetooth.MaxSpeed //显示实现IBluetooth的MaxSpeed属性 { get { return 64; } } string IBluetooth.TransData(string from, string to) //显式实现IBluetooth的TransData方法 { return string.Format("蓝牙数据传输:从{0}到{1}", from, to); } public override string Call(string name) { return string.Format("正在同{0}通话中...", name); }
5.5.5访问接口的成员
1、派生类对象转换为接口的实例
当接口的派生类实现了接口所有成员之后,访问这些成员有以下两种方式。
一种是通过派生类的实例来访问。例如,当类Mp3实现了IUsb接口时,就可以通过Mp3类的对象访问IUsb的成员,代码如下。
Mp3 m=new Mp3();
lblShow.Text=m.TransData("计算机","MP3设备");
二是通过接口的实例来访问。但请注意,接口是不能直接实例化的,只能间接实例化。其具体操作步骤是:先创建其派生类对象,再将该对象强制转换未接口类型并赋给接口型变量从而创建接口的实例,之后就可以通过接口型的变量来访问接口成员了,代码如下所示。
Mp3 m=new Mp3();
IUsb iu=(IUsb)m; //把m强制类型转换
lblShow.Text=iu.TransData("计算机","MP3设备");
请读者思考:以下语句是否正确?
IUsb iu= new IUsb();
答案:该语句是错误的,其原因是接口不能直接实例化。
表面上,第二种方式比第一种方式要负责一些,显得多此一举,实际上通过接口的实例来访问内部成员是一种好的设计策略。通过接口访问,可以更好地体现面向对象地多态性。例如,有两个或更多地类实现了接口,如果通过接口地实例来访问它们的成员,这样就不用区分所属的类名。这正如不管是MP3设备还是移动硬盘,只要插接到计算机的USB接口,就可以在两者之间相互复制数据文件,计算机也不用区分它们一样。
此外,当采用派生类显示实现接口时,只能通过接口来访问其成员。
2、测试对象是否支持接口
一个派生列的实例能成功转换为接口的实例的前提是该派生类实现了对应的接口。例如,能将Mp3型的变量m转换成IUsb,这是因为Mp3实现了IUsb
接口。
但是,在很多情况下,无法预知对象是否实现了某个接口,一旦弄错就会造成程序异常。例如,以下两条语句就是一条错误的语句;
Mp3 m= new Mp3();
IBluetooth bt=(IBluetooth)m;
其原因就是Mp3类没有实现IBluetooth。
可见,在实际编程时,需要先确定一个对象是否支持某个接口,再调用相应的方法。在C#中,有两种方式可测试一个对象是否支持某个接口。
第一种方式是使用is操作符,其格式如下:
表达式 is 类型
当表达式(必须是引用类型)可以安全地转换为指定”类型“时,结果为true,否则为false。
例如,下面地示例说明了is操作符的用法。
Mp3 m = new Mp3(); if(m is IUsb) //能安全转换,表达式为true,下面语句将执行 { IUsb iu =(IUsb)m; blShow.Text=iu.TransData("计算机","MP3设备"); } if(m is IBluetooth) //不能安全转换,表达式为假,下面语句将不会执行 { IBluetooth ib=(IBluetooth)m; lblShow.Text = ib.TransData("计算机","蓝牙设备"); }
另一种方法时使用as操作符,as操作符将is和转换操作结合起来,首先测试转换是否合法,若是则进行转换,否则返回null。as操作符使用形式如下:
表达式 as 类型
例如,下面的示例说明了as操作符的用法。
Mp3 m = new Mp3(); IUsb iu =m as IUsb; if(iu != null) //能安全转换,表达式为true,下面语句将执行 { IUsb iu =(IUsb)m; blShow.Text=iu.TransData("计算机","MP3设备"); } IBluetooth ib =m as IBluetooth; if(ib != null) //不能安全转换,表达式为假,下面语句将不会执行 { IBluetooth ib=(IBluetooth)m; lblShow.Text = ib.TransData("计算机","蓝牙设备"); }
is和as操作符也可测试对象是否属于所需类型和转换为所需类型。
例如,以下代码也是合法的。
Mobile m=new Mobile(); if(m is Phone) { Phone p = (Phone)m; } Phone p =m as Phone;
上述代码正确的原因是:Mobile是Phone的派生类,可以利用is来判断m是否是Phone,由于有继承关系,m既是一个Mobile也是一个Phone,这个转换是成功的。
下面的实例完整地演示了接口的声明、实现和访问。
【实例5-5】接口演示。

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnMp3_Click(object sender, EventArgs e) { Mp3 m = new Mp3(); if(m is IUsb) //能安全转换,表达式为true,下面语句将执行 { IUsb iu = (IUsb)m; lblShow.Text = iu.TransData("计算机", "MP3设备"); } if(m is IBluetooth) //不能安全转换,表达式为假,下面语句将不会执行 { IBluetooth ib = (IBluetooth)m; lblShow.Text = ib.TransData("计算机","蓝牙设备"); } } private void btnMobile_Click(object sender, EventArgs e) { Mobile m = new Mobile(); IUsb iu = m as IUsb; if (iu != null) lblShow.Text = iu.TransData("计算机", "手机"); IBluetooth ib = m as IBluetooth; if (ib != null) lblShow.Text += "\n"+ib.TransData("手机", "计算机"); lblShow.Text += "\n" + m.Call("父亲"); } } interface IUsb //声明接口 { int MaxSpeed { get; } //成员属性 string TransData(string from, string to); //成员方法 } interface IBluetooth //声明接口 { int MaxSpeed { get; } string TransData(string from, string to); } interface IMp3 : IUsb, IBluetooth //声明接口,该接口继承基接口的定义 { string Play(string fileName); } public class Mp3:IUsb //只实现一个接口的定义 { public int MaxSpeed { get { return 480; } } public string TransData(string from,string to) { return string.Format("数据传输:从{0}到{1}", from, to); } } public abstract class Phone //定义抽象类 { public abstract string Call(string name); //声明抽象方法 } public class Mobile : Phone, IUsb, IBluetooth //同时从基类和多个接口派生 { int IUsb.MaxSpeed //实现指定接口成员 { get { return 480; } } string IUsb.TransData(string from, string to) //实现指定接口的成员 { return string.Format("USB数据传输:从{0}到{1}",from,to); } int IBluetooth.MaxSpeed //实现指定接口的成员 { get { return 64; } } string IBluetooth.TransData(string from, string to) //实现指定接口的成员 { return string.Format("蓝牙数据传输:从{0}到{1}", from, to); } public override string Call(string name) //实现从基类继承来的抽象方法 { return string.Format("正在和{0}通话中...", name); } } }
【分析】首先该程序声明了三个接口IUsb、IBluetooth和IMp3,然后声明了三个类Mp3、Phone和Mobile类。其中,IMp3是IUsb的IBluetooth派生接口,Mp3类实现IUsb接口,Phone是一个抽象类,Mobile类继承Phone并实现IUsb和IBluetooth接口。由于IUsb和IBluetooth都包含同名的方法TransData和属性MaxSpeed,因此在Mobile类中用接口名作为标签分别显示实现它们的各个成员。最后,MP3按钮的事件中,通过MP3类的对象成功地访问了IUsb的成员。在“手机”按钮事件方法中,将Mobile对象转换成对应的接口类型,然后通过接口引用访问了IUsb和IBluetooth的方法。程序运行效果如图5-8和图5-9。
图 5-8 单击MP3按钮时的运行效果
图 5-9 单机“手机”按钮时的运行效果
5.5.6抽象类与接口的比较
抽象类是一种不能实例化的类,抽象类可以包含抽象成员,也可以包含非抽象成员,即抽象类可以时完全实现的,也可以时部分实现的,或者完全不实现的。抽象类可以用来封装所有派生类的通用功能。
与抽象类不同的是,接口顶多像一个完全没有实现的只包含抽象成员的抽象类,因此无法使用接口来封装所有派生类的通用功能,接口更多地用来制定程序设计开发规范,接口地代码实现由开发者完成。例如,有关XML文档地处理,万维网联盟(W3C)就制定了一个DOM(文档对象模型)规范,而具体的代码实现由诸如Microsoft、Sun等公司去实现。C#规定一个类只能从一个基类派生,但允许从多个基接口派生。
抽象类为管理组件版本提供了一个简单易行的方法。通过更新基类,所有派生类都将自动进行相应改动。而接口在创建后就不能再更改,如果需要修改接口,即必须创建新的接口。
5.6 嵌套类、分部类与命名空间
5.6.1 嵌套类
在类的内部或结构的内部定义的类型称为嵌套类型,又称内部类型。不论是类还是结构,嵌套类型均默认为private,嵌套类型也可以设置为public、internal、protected或protected internal。嵌套类型通常需要实例化为对象之后,才能引用其他成员,其使用方法与类的普通成员使用基本相同。
【实例 5-6】使用嵌套类计算长方形面积。

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class Test5_6 : Form { public Test5_6() { InitializeComponent(); } private void btnCalculate_Click(object sender, EventArgs e) { int x1, x2, y1, y2; x1 = Convert.ToInt32(txtLx.Text); x2 = Convert.ToInt32(txtRx.Text); y1 = Convert.ToInt32(txtLy.Text); y2 = Convert.ToInt32(txtRy.Text); Rectangle ra = new Rectangle(x1, y1, x2, y2); //创建一个矩形对象 lblShow.Text = string.Format("长方形的面积为:{0}.", ra.Area()); } } class Rectangle //矩形类 { private Point topLeft; //矩形的左上角 private Point bottomRight; //矩形的右下角 public Rectangle(int lx,int ly,int rx,int ry) //构造函数 { topLeft = new Point(lx, ly); bottomRight = new Point(rx, ry); } class Point //点类,嵌套在矩形类之中,表示一个矩形由若干个点组成 { private int x; private int y; public Point(int x,int y) //构造函数 { this.x = x; this.y = y; } public int X { get { return x; } } public int Y { get { return y; } } } public int Area() //矩形的面积计算 { return (bottomRight.X - topLeft.X) * (bottomRight.Y - topLeft.Y); } } }
【分析】该程序中,类Rectangle的嵌套类Point是它的私有成员,只能在Rectangle类中使用,不能在其他类中使用。该程序的允许效果如图5-10所示。
图 5-10 嵌套类示例运行效果
5.6.2 分布类
分布类允许将类、结构或接口的定义拆分到两个或多个源文件中,让每个源文件只包含其中的一部分代码,编译时C#编译器自动把所有部分组合起来进行编译。
有了分部类,一个类的源代码可以分布于多个独立文件中,在处理大型项目时,过去很多只能由一个人进行的编程任务,现在可以由多个人同时进行,这样将大大加快了程序设计的工作进度。
有了分布类,使用自动生成的源代码时无需重新创建源文件便可将代码添加到类中。事实上,当创建Windows应用程序或Web应用程序时,就是在VS自动生成源代码的基础之上专注于项目的业务处理,编译时VS会自动把手工录入的代码与自动生成的代码进行合并编译。
在C#中,分部类使用partial关键字进行修饰。
例如:
//Test1.cs public partial class Test //这是一个分布类 { public string Fun1() { return "这是第1部分"; } } //Test2.cs using System; public partial class Test //这是一个分布类 { public void Fun2() { Console.WriteLine("这是第2部分"); } }
其中,Test1.cs和Test2.cs中的类Test是分布类,在同一个应用程序项目中,编译时,将被合并为一个完整的类进行编译。如下列代码中的对Test对象的方法Fun和Fun2的调用。
Test t=new Test(); Console.WriteLine(t.Fun1()); t.Fun2();
【注意】
处理分部类的定义时需遵循以下几个规则。
(1)同一类型的各个部分的所有分部类的定义都必须使用partial进行修饰。各个部分必须具有相同的可访问性,如public、private等。
(2)如果将任意部分声明为抽象的,则整个类型都被视为抽象的。如果将任意部分声明为密封的,则整个类型都被视为密封的。
(3)partial修饰符只能出现在紧靠关键字class、struct或interface前面的位置。
(4)分部类的各部分或者各个源文件都可以独立引用类库,且坚持“谁使用谁负责添加引用”的原则。例如,上例中,Test1.cs没有使用类库,则不添加类库的引用,而Test2.cs调用了方法Console.WriteLine,则必须使用using System以添加系统类库的引用。
(5)分部类的定义中允许使用嵌套的分部类,例如:
partial class A { partial class B{} } partial class A { partial class B{} }
其中,A和B都是分部类,但B嵌套在A中。
(6)同一类型的各个部分的所有分部类的定义都必须在同一程序集或同一模块(.exe或.dll文件)中进行定义,分部定义不能跨越多个程序集。
5.6.3 命名空间
对于一个大型软件项目来说,当多个程序员共同参与开发时,这些程序员可能以同样的名字来创建类。例如,一个程序员在开发客户管理子系统时把客户类命名为User,而另一个程序员在开发后台权限管理子系统时把系统管理员类也命名为User因此最终无法集成项目。命名空间可将相互关联的类组织起来,形成一个逻辑上相关联的层次结构,命名空间既可以对内组织应用程序,也可以对外避免命名冲突。
1..NET Framework的常用命名空间
.NET Framework是由许多命名空间组成的,.NET就是利用这些命名空间来管理庞大的类库,见表5-9.例如,命名空间System.Web.UI.WebControls就提供了用来创建Web网页的所有可用类,包括文本框(TextBox)、命令按钮(Button)、标签(Lable)和列表框(ListBox)等;而System.Windows.Forms则提供了用于创建基于Windows的应用程序的所有可用类,同样包括文本框、命令按钮和标签等。
表 5-9 列出了.NET Framework中常用的命名空间
命名空间 | 描述 |
System | 提供用于定义常用值类型、引用数据类型、事件和事件处理程序、接口、属性和处理异常的基础类 |
System.IO | 提供用于对数据流和文件进行的读写类 |
System.Data | 提供用于数据访问的类 |
System.Drawing | 提供用于处理图形的类 |
System.NET | 提供用于网络通信的有关类 |
System.Text | 提供用于处理不同字符编码间转换的类 |
System.Web | 提供用于创建Web应用程序的类 |
System.Windows.Forms | 提供用于创建Windows应用程序的类 |
System.Xml | 提供用于处理XML文档的类 |
2.自定义命名空间
在C#程序中,使用关键字namespace就可以定义自己的命名空间,一般形式如下:
namespace命名空间名
{
//类型的声明
}
其中,命名空间名必须遵守C#的命名规范,命名空间内一般由若干个类型组成,包括声明枚举型、结构型、接口和类等。
例如:
namespace CompanyName { public class Customer(){} }
另外,命名空间也可以嵌套,即在一个命名空间中定义一个命名空间。
namespace Sohu { namespace Sales { public class Customer(){} } }
命名空间也可以用“.”标记分隔定义命名空间,这样就可以直接定义一个嵌套的命名空间,例如:
namespace Sohu.Sales { public class Customer(){} }
3.引用命名空间中的类
引用命名空间中的类有以下两种方法。
一是采用完全限定名来引用。
例如:
Sohu.Sales.Customer c= new Sohu.Sales.Customer();
就是通过完全限定名来引用命名空间Sohu.Sales,并使用该命名空间中Customer类的构造函数创建一个新对象。
二是首先通过using关键字导入命名空间,再直接引用。
例如:
using Sohu.Sales;
Customer c =new Customer();
就是先通过using关键字导入命名空间,再直接引用。
由于命名空间允许嵌套,所嵌套子命名空间的层次数量没有限制,如果采用完全限定名来引用命名空间中的类,则程序的可读性将大大下降。在实际编程中,建议采用第二种方法来引用命名空间,相应的using语句一般放在.cs源文件的顶部。
【课后实例】
一、学生成绩实例
(1)设计一个Windows应用程序,在该程序中首先构造一个学生基本类,再分别构造小学生、中学生、大学生等派生类,当输入相关数据,单机不同的按钮(小学生、中学生、大学生)将分别创建不同的学生对象,并输入当前的学生总人数、该学生的姓名、学生类型和平均成绩。运行效果如图5-11所示,要求如下:
图 5-11 运行效果
1、每个学生都有姓名和年龄
2、小学生有语文、数学成绩
3、中学生有语文、数学和英语成绩
4、大学生有必修课学分总数和选修课学分总数,不包含单科成绩
5、学生类提供向外输出信息的方法
6、学生类提供统计个人总成绩或总学分的方法
7、通过静态类成员自动记录学生总人数
8、能通过构造函数完成各字段成员初始化

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class 学生记录小程序 : Form { public 学生记录小程序() { InitializeComponent(); } private void btnPupil_Click(object sender, EventArgs e) { int age = Convert.ToInt32(txtAge.Text); double sub1 = Convert.ToDouble(txtChina.Text); double sub2 = Convert.ToDouble(txtMath.Text); Pupil p = new Pupil(txtName.Text, age, sub1, sub2); lblShow.Text += p.getInfo(); } private void btnMiddle_Click(object sender, EventArgs e) { int age = Convert.ToInt32(txtAge.Text); double sub1 = Convert.ToDouble(txtChina.Text); double sub2 = Convert.ToDouble(txtMath.Text); double sub3 = Convert.ToDouble(txtEnglish.Text); Middle m = new Middle(txtName.Text, age, sub1, sub2, sub3); lblShow.Text += m.getInfo(); } private void btnCollege_Click(object sender, EventArgs e) { int age = Convert.ToInt32(txtAge.Text); double sub1 = Convert.ToDouble(txtChina.Text); double sub2 = Convert.ToDouble(txtMath.Text); College c = new College(txtName.Text, age, sub1, sub2); lblShow.Text += c.getInfo(); } } //抽象基类 public abstract class Student { protected string name; protected int age; public static int number; public Student (string name,int age) //构造函数 { this.name = name; this.age = age; number++; } public string Name //普通属性成员 { get { return name; } } public virtual string type //虚属性成员 { get { return "学生"; } } public abstract double total(); //抽象方法成员 public string getInfo() //普通方法成员 { string result = string.Format("总人数:{0},姓名:{1},{2},{3}岁", number, Name, type, age); if (type == "小学生") result += string.Format(",平均成绩为{0:N2}:\n", total() / 2); if (type == "中学生") result += string.Format(",平均成绩为{0:N2}:\n", total() / 3); if (type == "本科生") result += string.Format(",总学分为{0:N2}:\n", total() ); return result; } } public class Pupil : Student //派生小学生类 { protected double chinese; protected double math; public Pupil(string name,int age,double chinese,double math) : base(name, age) { this.chinese = chinese; this.math = math; } public override string type //重载虚属性 { get { return "小学生"; } } public override double total() //重载抽象方法 { return chinese + math; } } public class Middle : Student //派生中学生类 { protected double chinese; protected double math; protected double english; public Middle(string name,int age,double chinese,double math,double english) : base(name, age) { this.chinese = chinese; this.math = math; this.english = english; } public override string type //重载虚属性 { get { return "中学生"; } } public override double total() //重载抽象方法 { return chinese + math + english; } } public class College : Student //派生大学生类 { protected double chinese; protected double math; public College(string name,int age,double chinese,double math) : base(name, age) { this.chinese = chinese; this.math = math; } public override string type //重载虚属性 { get { return "本科生"; } } public override double total() //重载抽象方法 { return (chinese * 5 + math * 2) / 7; } } }
二、图形面积计算
设计一个Windows应用程序,在该程序中定义平面图形抽象类和其派生类圆、矩形、三角形、该程序实现的功能包括:输入相应图形的参数,如矩形的长和宽,单击相应的按钮,根据输入参数创建图形类并输出该图形的面积。程序运行结果如图5-12所示。
图 5-12 运行效果

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class 图像面积计算 : Form { public 图像面积计算() { InitializeComponent(); } private void btnCircle_Click(object sender, EventArgs e) { double r =Convert.ToDouble(textBox1.Text); Circle c = new Circle(r); lblShow.Text = "圆形面积为:" + c.Area(); } private void btnRect_Click(object sender, EventArgs e) { double w = Convert.ToDouble(textBox2.Text); double l = Convert.ToDouble(textBox1.Text); Rect r = new Rect(l,w); lblShow.Text = "矩形面积为:" + r.Area(); } private void btnTran_Click(object sender, EventArgs e) { double h = Convert.ToDouble(textBox2.Text); double b = Convert.ToDouble(textBox1.Text); Tran t = new Tran(b,h); lblShow.Text = "三角形面积为:" + t.Area(); } } //抽象基类 public abstract class Figure { public abstract double Area(); } //派生子类:圆形类 public class Circle : Figure { double radius; public Circle(double r) { radius = r; } public override double Area() { return radius * radius * 3.14; } } //派生子类:矩形类 public class Rect : Figure { double length; double wide; public Rect(double l,double w) { length = l; wide = w; } public override double Area() { return length * wide; } } //派生子类:三角形类 public class Tran : Figure { double bottom; double high; public Tran(double b,double h) { bottom = b; high = h; } public override double Area() { return (bottom * high) / 2; } } }
三、音乐播放小程序
声明一个播放器接口IPlayer,包含5个接口方法,播放、停止、暂停、上一首、和下一首。设计一个Windows应用程序,在该程序中定义一个MP3播放器类和一个AVI播放器类,以实现该接口,最后创建相应类的实例测试程序,如图5-13所示为当单击MP3按钮后,再单击”播放“按钮的效果。与此类似,如果单击AVI按钮后,再单击”播放“按钮则应显示”正在播放AVI视频!“。
图 5-12 运行效果

using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace 第五章 { public partial class 音乐视频播放 : Form { IPlayer ip; MP3 m; AVI a; public 音乐视频播放() { InitializeComponent(); } private void btnMp3_Click(object sender, EventArgs e) { m = new MP3(); ip = (IPlayer)m; } private void btnAvi_Click(object sender, EventArgs e) { a = new AVI(); ip = (IPlayer)a; } private void btnF_Click(object sender, EventArgs e) { richTextBox1.Text = ip.Pre(); } private void btnStop_Click(object sender, EventArgs e) { richTextBox1.Text = ip.Stop(); } private void 音乐视频播放_Load(object sender, EventArgs e) { } private void btnStart_Click(object sender, EventArgs e) { richTextBox1.Text = ip.Play(); } private void btnSuspend_Click(object sender, EventArgs e) { richTextBox1.Text = ip.Pause(); } private void btnNext_Click(object sender, EventArgs e) { richTextBox1.Text = ip.Next(); } } interface IPlayer //接口定义 { string Play(); //播放 string Stop(); //停止 string Pause(); //暂停 string Pre(); //上一首 string Next(); //下一首 } public class MP3 : IPlayer { public string Play() { return "正在播放MP3歌曲!"; } public string Stop() { return "停止播放MP3歌曲!"; } public string Pause() { return "暂停播放MP3歌曲!"; } public string Pre() { return "播放上一首MP3歌曲!"; } public string Next() { return "播放下一首MP3歌曲!"; } } public class AVI : IPlayer { public string Play() { return "正在播放AVI视频!"; } public string Stop() { return "停止播放AVI视频!"; } public string Pause() { return "暂停播放AVI视频!"; } public string Pre() { return "播放上一个AVI视频!"; } public string Next() { return "播放下一个AVI视频!"; } } }