第4章 类与对象
前面3章介绍了C#编程的基础知识,利用这些知识可以编写字符界面的控制台应用程序。一个控制台应用程序主要由一个扩展名为.cs的源文件组成,默认情况下该源文件包含一个命名空间,在其中声明一个类并为该类定义了Main方法。由此可知,即使使用C#语言创建比较简单的控制台应用程序,也是采用面向对象的程序设计方法来实现的。
从本章开始,将通过9 章的篇幅来详细讲述C#面向对象编程方面的内容。本章介绍面向对象编程(Object-Oriented-Programming,OOP)的基础知识,主要包括对象与类、类的声明、构造函数和析构函数、字段和常量、继承、嵌套类型,以及分部类和静态类等。
4.1 面向对象编程基本概念
面向对象编程是当今最流行的编程模式,C#是面向对象的编程语言,使用类和结构来实现类,例如Windows窗体、用户界面控件,以及数据结构等。典型的C#应用程序由开发人员定义的类和 .NET Framework的类组成,其中至少包含一个类。而对象是给定类的实例,类和对象构成了C#面向对象编程的基础。
4.1.1 类
类是C#中功能最为强大的数据类型,与结构一样,它定义了数据的类型和行为。在程序中可以创建作为类的实例的对象,类支持继承,而继承是面向对象编程的基础部分。
类是一种数据结构,它可以包含数据成员(常量和字段)、函数成员(方法、属性、事件、索引器、运算符、实例构造函数、静态构造函数和析构函数),以及嵌套类型。类类型支持继承,继承是一种机制,它使派生类可以对基类进行扩展和专用化。
类具有以下特性。
(1)封装:指对象隐藏其内部数据和方法的能力,使得只有对象的预期部分以编程方式可访问。通过类把数据和处理数据的方法(函数)封装成一个整体,以实现具有明确功能的模块。使得用户只能看到对象的外在特性(对象能接收哪些消息及具有哪些处理能力等),而对象的内在特性(保存内部状态的私有数据和实现加工能力的算法)对用户是不可见的。封装的目的是把对象设计者与对象使用者分开,使用者不必知道实现的细节,只需要使用设计者提供的一组接口来访问对象并通过对象的专用函数来访问对象的私有数据。
(2)继承:一种机制,它使派生类可以对基类进行扩展和专用化。通过继承可以使一个类获得先前已定义类的全部特征,这样可以在一个类的基础上快速创建一个新类。从而增强代码的可重用性,以提高应用开发的效率。
(3)多态:通过继承一个类可以将其用做多种类型,如自己的类型、任何基类型,或者在实现接口时的任何接口类型,此即为多态性。C#中的每种类型都是多态的,类型可用做其自己的类型或用做Object实例,因为任何类型都自动将Object当做基类型。
4.1.2 对象
对象是面向对象应用程序的一个部件,是具有数据、行为和标识的编程结构。它封装了部分应用程序,如一个过程、一些数据或一些抽象实体。对象的数据包含在对象的字段、属性和事件中,对象的行为则由其方法和接口定义。
对象是具有标识的,数据集相同的两个对象并不一定是同一对象。在C#中,对象的类型是类。它通过类或结构来定义,是从类中创建的(称为“实例化”)。表示该类的一个实例,该类的所有对象都按照其构成的同一蓝图操作。每个对象都有一个生命周期,当从类中创建对象时将调用构造函数来完成对象的初始化;当删除对象时,将调用析构函数来完成一些清理工作。
对象具有以下特点。
(1)在C#中使用的全都是对象,包括Windows窗体和控件。
(2)对象是从类和结构所定义的模板中创建的实例。
(3)对象可以通过属性来获取和更改其包含的信息。
(4)对象通常具有允许其执行操作的方法和事件。
(5)Visual Studio提供了操作对象的工具,例如,使用属性窗口可以更改对象(如Windows窗体)的属性,使用对象浏览器可以检查对象的内容。
(6)在C#中所有对象都继承自Object。
4.2 声明类
类是一种自定义数据类型,是最基础的C#类型。它将状态(字段)和操作(方法和其他函数成员)组合在一个单元中,可以包含数据成员、函数成员和其他嵌套类型。类为动态创建的类实例提供了定义,实例也称为“对象”。类支持继承和多态性,这是派生类可用来扩展和专用化基类的机制。在C#应用编程中首先声明类,然后创建类的实例对象并通过访问该对象的成员来实现特定的功能,或者把该类作为基类来创建新的派生类。
4.2.1 类声明语法
在C#中,类声明由声明头和类体组成。在声明头中,首先指定类的属性和修饰符。然后是class关键字和类的名称,接着是基类(若有的话)及该类实现的接口。声明头后面跟着类体,它由一组位于一个花括号{和}之间的成员声明组成。类声明的基本语法格式如下:
[attributes] [class-modifiers] class identifier { // 类成员声明 }
其中attributes是可选项,指定类的属性集;class-modifiers为类修饰符,也是可选项,可用的类修饰符包括new、public、protected、internal、private、abstract、sealed和static;identifier是必选项,它是一个标识符,用于指定类的名称;花括号内是类成员声明,类成员包括常量、字段、方法、属性、事件、索引器、运算符、实例构造函数、析构函数、静态构造函数,以及其他嵌套类型(如类、接口和结构)。
如果在类声明中省略类了修饰符,则默认项为internal。这将声明内部类,内部类只有在同一程序集(EXE或DLL)的文件中才是可访问的。
如果在类声明中没有显式指定基类,则该类的基类为System.Object类,所声明的类将继承System. Object类的所有成员。例如,可以通过调用GetType方法获取当前实例的Type,也可以通过调用ToString方法来返回表示当前实例的字符串。
根据类声明处的上下文,类可分为非嵌套类和嵌套类。非嵌套类不包含在其他类中,在编译单元或命名空间内声明;嵌套类则包含在其他类中,是其他类的成员。
类声明通常包含在命名空间中,所声明的类类型成为所在命名空间的成员,可以通过限定名来访问实体的成员。限定名以对实体的引用开头,后跟一个“.”标记,再接成员名称。若在编译单元(源文件)中声明一个A类,则该类是非嵌套类,其限定名为A;若在命名空间N中声明A类,则该类也是非嵌套类,其限定名为N.A;若在此A类内部声明了一个B类,则该类为嵌套类,其完整限定名为N.A.B。
声明一个类之后,可以通过new运算符来创建类的实例,其语法格式如下:
class-type obj = new class-type([argument-list]);
其中class-type是类名,obj是一个引用型变量,argument-list为参数列表。
new运算符为类的一个新实例分配存储空间,如果没有足够的可用内存,则引发System.OutOfMemoryException,并且不执行进一步的操作;否则将新实例的所有字段初始化为其默认值,然后根据函数成员调用的规则来调用实例构造函数。并将对新分配的实例的引用自动传递给实例构造函数,在实例构造函数中可以用this关键字来访问将该实例。
【例4-1】创建一个C#控制台应用程序,说明如何定义非嵌套类和嵌套类并分别用来创建类实例,程序运行结果如图4-1所示。
图4-1 程序运行结果
【设计步骤】
(1)在Visual C#中创建一个控制台应用程序项目,项目名称为“ConsoleApplication4-01”,解决方案名称为“chapter04”,保存在F:\Visual C sharp 2008\chapter04文件夹中。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_01中Program类声明之上声明一个Container类,代码如下:
class Container // 声明非嵌套类 { }
(3)在Program类内部Main方法之前声明一个Nested类,代码如下:
class Nested // 声明嵌套类 { }
(4)为Main方法编写以下代码:
Console.Title = "类与对象应用示例"; Container myObject1 = new Container(); Nested myObject2 = new Nested(); Console.WriteLine("{0}\n", myObject1.GetType()); Console.WriteLine("{0}\n", myObject2.GetType());
(5)按Ctrl+F5组合键编译并运行程序。
4.2.2 类修饰符
在类的声明头中可以使用的类修饰符为new、public、protected、internal、private、abstract、sealed及static。
其中new修饰符适用于嵌套类,用于指定类隐藏同名的继承成员,详见10.3.4节中的介绍。如果在不是嵌套类声明的类声明中使用该修饰符,则会导致编译时错误。
public、protected、internal和private修饰符控制类的可访问性,也称为“访问修饰符”。使用这些访问修饰符可指定下列5个可访问性级别。
(1)public:声明该类为公共类,其访问不受限制,可以在当前程序集和外部程序集中访问。
(2)protected:声明该类为受保护类,其访问仅限于包含类或从包含类派生的类,该修饰符仅用于嵌套类。
(3)internal:声明该类为内部类,其访问仅限于当前程序集。
(4)protected internal:声明该类为受保护内部类,其访问仅限于当前程序集或从包含类派生的类,该修饰符仅用于嵌套类。
(5)private:声明该类为私有类,其访问仅限于包含类,该修饰符仅用于嵌套类。
根据类声明处的上下文,上述访问修饰符中有些可能不允许使用。未嵌套在其他类中的顶级类的可访问性只能是internal或public,这些类的默认可访问性是internal;包含在其他类中的嵌套类可以使用上述5个可访问性级别。一个类只能有一个访问修饰符,但使用protected internal组合时除外。如果在一个类声明中多次出现同一修饰符,则出现编译时错误。
abstract、sealed和static修饰符分别用于声明抽象类、密封类和静态类,详见4.5.2、4.5.3和4.7.2节中的介绍。
在一个Visual C#项目中,可以把类放在同一个或者不同的源文件中。在解决方案chapter04的控制台应用程序项目ConsoleApplication4-01中,源文件Program.cs包含Container和Program两个内部类的声明。在Program类中包含一个嵌套类Nested和Main方法,该方法是应用程序的入口点。Program类是该应用程序项目的入口点类,可以在其Main方法中创建对象和调用其他方法。在编译C#程序时,控制台应用程序项目被打包为程序集,其文件扩展名为“.exe”。
也可以把类放在不同的项目中。在应用开发中经常把类放在另一种类型的项目中,这种项目只包含类及相关的类型定义。但这种项目本身没有程序入口点,因此不能独立运行。这种类型的项目称为“类库”,它由命名空间组成。每个命名空间都包含可在程序中使用的类型,即类、结构、枚举、委托和接口。类库被编译为.dll程序集,称为“DLL”。类库项目与引用类库的项目可以位于同一个或不同的解决方案中。
在创建其他项目时通过添加对DLL程序集的引用,可以访问DLL包含的类及其他类型。当在Visual Studio中创建Visual C#项目时,已经引用了最常用的基类DLL程序集。
如果需要使用尚未引用的DLL中的类型,则必须添加对此DLL的引用。为此在解决方案资源管理器中右击一个项目,单击快捷菜单中的“添加引用”命令,在“添加引用”对话框中找到所需的DLL文件。然后单击“确定”按钮,此时在解决方案资源管理器的“引用”文件夹下面出现对DLL的引用。
在使用DLL中的类型之前,还需要使用using指令导入这些类型所在的命名空间。
下面结合示例说明如何通过类库项目生成DLL并在控制台应用程序中引用DLL中的公共类。
【例4-2】创建一个类库项目,定义一个公共类Class1并生成DLL文件。然后创建一个C#控制台应用程序项目,添加对DLL程序集的引用。并在Program类的Main方法中创建Class1类的实例,程序运行结果如图4-2所示。
图4-2 程序运行结果
【设计步骤】
(1)在Visual C#中打开解决方案chapter04,然后单击“文件”→“添加”→“新项目”命令,打开“添加新项目”对话框。
(2)在“模板”列表框中选择“类库”选项,在“名称”文本框中输入类库名称ClassLibrary4-01。然后单击“确定”按钮,如图4-3所示。
图4-3 添加类库项目
(3)在代码编辑器中打开源文件Class1.cs,其内容如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ClassLibrary4 01 // 声明命名空间ClassLibrary4 01 { public class Class1 // 声明公共类Class1作为命名空间的成员 { } }
(4)单击“生成”→“生成ClassLibrary4-01”命令,或者按Shift+F6组合键,以编译项目生成DLL文件。
(5)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-02”,保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(6)在解决方案资源管理器中右击项目ConsoleApplication4-02,然后单击快捷菜单中的“添加引用”命令,显示“添加引用”对话框。
(7)打开“浏览”选项卡,然后定位到F:\Visual C sharp 2008\chapter04 \ClassLibrary01\bin\Debug文件夹,选择ClassLibrary01.dll文件。单击“确定”按钮,如图4-4所示。此时在解决方案资源管理器中将出现对命名空间ClassLibrary4-01的引用,如图4-5所示。
图4-4 添加对DLL的引用
图4-5 新添加的引用
(8)在代码编辑器中打开源文件Progmam.cs,在命名空间ConsoleApplication4_02声明上方添加以下using指令:
using ClassLibrary4-01;
(9)在Program类声明上方声明一个内部类MyClass,代码如下:
class MyClass { }
(10)在Main方法中编写以下代码:
Console.Title = "引用DLL中的公共类示例"; Class1 myObject1 = new Class1(); Console.WriteLine(myObject1.ToString() + "\n"); MyClass myObject2 = new MyClass(); Console.WriteLine(myObject2.ToString() + "\n");
(11)按Ctrl+F5组合键编译并运行程序。
4.2.3 类成员
类的成员包括两个部分,一是类本身声明成员;二是从基类继承的成员。如果在类声明中未指定基类,则该类将继承System.Object类的所有成员。
类的成员分为两种类型,其中数据成员包括常量和字段;函数成员包括方法、属性、事件、索引器、运算符、实例构造函数、析构函数、静态构造函数和类型。
(1)常量:表示与该类相关联的常量值。
(2)字段:即该类的变量。
(3)方法:用于实现可由该类执行的计算和操作。
(4)属性:用于定义一些命名特性,以及与读取和写入这些特性相关的操作。
(5)事件:用于定义可由该类生成的通知。
(6)索引器:使该类的实例可按与数组相同的(语法)方式索引。
(7)运算符:用于定义表达式运算符,以对该类的实例进行运算。
(8)实例构造函数:用于实现初始化该类的实例所需的操作。
(9)析构函数:用于实现在永久地放弃该类的一个实例之前需要执行的操作。
(10)静态构造函数:用于实现初始化该类自身所需的操作。
(11)类型:用于表示一些类型,它们是该类的局部类型。
在类声明中,对类成员可以使用public、protected、internal和private这4个访问修饰符来指定以下5个可访问级别。
(1)public:声明公共成员,其访问不受限制,公共访问是允许的最高访问级别。
(2)protected:声明受保护成员,其访问仅限于包含类或从包含类派生的类。
(3)internal:声明内部成员,其访问仅限于当前程序集。
(4)protected internal:声明受保护内部成员,其访问仅限于当前程序集或从包含类派生的类。
(5)private:声明私有成员,其访问仅限于包含类。私有访问是允许的最低访问级别,私有成员只有在声明它们的类中才是可访问的。
根据出现成员声明的上下文,在声明中只允许使用某些可访问性。如果在成员声明中未指定访问修饰符,则使用默认的可访问性。
根据是否使用static修饰符,类成员分为静态成员和实例成员。
使用static修饰符时,声明的是静态成员。静态成员属于整个类所有,为该类的所有实例所共享,可通过类名和成员名来访问。类名与成员名之间用圆点“.”分隔,不必创建类的实例。例如,Console类的WriteLine方法就是一个静态方法,在代码中可通过Console.WriteLine()形式来调用。未使用static修饰符时,声明的是实例成员。实例成员属于类的每个实例,首先需要创建类的实例。然后通过实例变量名和成员名来访问,实例变量名和成员名之间用圆点“.”分隔。
【例4-3】创建一个控制台应用程序项目,声明一个内部类Student并为该类声明一些实例成员和静态成员,程序运行结果如图4-6所示。
图4-6 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-03”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4-03中声明一个内部类Student,代码如下:
class Student { private string studentID; // 私有字段成员 private string studentName; // 私有字段成员 static int count; // 静态字段成员 public Student(string id, string name) // 公共成员,类的构造函数 { studentID = id; studentName = name; count++; } public void showInfo() // 公共方法成员 { Console.WriteLine("学号:{0}\t姓名:{1}\t总人数:{2}\n", studentID, studentName, count); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "类成员应用示例"; Student student1 = new Student("080101", "张强"); // 创建类实例 student1.showInfo(); // 调用实例方法 Student student2 = new Student("080102", "李明"); // 创建另一类实例 student2.showInfo(); // 调用实例方法
(4)按Ctrl+F5组合键编译并运行程序。
4.2.4 结构与类的区别
C#中的结构使用struct关键字定义,例如:
public struct MyStruct { // 声明结构成员,包括字段、属性、方法和事件等 }
结构与类使用基本相同的语法,但与类相比结构存在一些限制。结构具有以下特点。
(1)结构是值类型,而类是引用类型。如果从结构中创建一个对象并将该对象赋给某个变量,则该变量包含结构的全部值。为结构类型变量赋值意味着将创建所赋值的一个副本。复制包含结构的变量时将复制所有数据,对新副本所做的任何修改都不会改变原副本的数据。
(2)在结构声明中不能声明无形参的实例构造函数(即默认构造函数),也不能声明析构函数,但可以声明带参数的构造函数。
(3)在结构声明中除非字段被声明为const或static,否则无法初始化。
(4)一个结构不能从另一个结构或类继承,而且不能作为一个类的基类。所有的结构都直接继承自System.ValueType,而后者继承自System.Object。结构可实现接口,其方式与类完全一样。
(5)结构可为null的类型,因而可为其赋null值。
(6)与类不同,结构的实例化也可以不使用new运算符。如果不使用new,则在初始化所有字段之前,字段都保持未赋值状态且对象不可用;如果使用new运算符创建结构对象,则会创建该结构对象并调用适当的构造函数,将所有值类型字段设置为其默认值。并将所有引用类型字段设置为null,这样产生该结构的默认值。
(7)使用装箱和取消装箱操作在结构类型与object之间转换,编译器可以在装箱过程中将值类型转换为引用类型。
【例4-4】创建一个C#控制台应用程序,说明如何使用默认构造函数和参数化构造函数初始化结构对象,以及不使用new运算符创建结构对象。并且结构与类之间的区别(前者是值类型,后者是引用类型),程序运行结果如图4-7所示。
图4-7 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-04”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在命名空间ConsoleApplication4-04中的Program类定义上方分别声明一个名为“MyStruct”结构和一个名为“MyClass”的类,代码如下:
// 定义结构MyStruct public struct MyStruct { public int x; // 公共字段 public MyStruct(int value) // 参数化构造函数 { x = value; } public void SetX(int value) // 公共方法 { x = value; } } // 定义类MyClass public class MyClass { public int x; // 公共字段 public MyClass() // 无参数构造函数(默认构造函数) { x = 0; } public void SetX(int value) // 公共方法 { x = value; } }
(3)在Program类的Main方法中输入以下代码:
Console.Title = "结构与类应用示例"; // 创建结构对象 MyStruct myStructObject1 = new MyStruct(); // 使用无参数构造函数创建结构对象 MyStruct myStructObject2 = new MyStruct(2); // 使用参数化构造函数创建结构对象 MyStruct myStructObject3; // 不使用new运算符创建结构对象 myStructObject3.x = 3; // 对结构对象的字段赋值 // 显示结构对象成员的值 Console.Write("myStructObject1.x = {0}\t", myStructObject1.x); Console.WriteLine("myStructObject2.x = {0}", myStructObject2.x); Console.WriteLine("myStructObject3.x = {0}", myStructObject3.x); myStructObject1 = myStructObject2; // 结构对象赋值 myStructObject1.SetX(6); // 调用结构对象的方法,以设置其字段值 myStructObject2.x = 7; // 修改结构对象的字段值 Console.Write("myStructObject1.x = {0}\t", myStructObject1.x); Console.WriteLine("myStructObject2.x = {0}", myStructObject2.x); if (Object.Equals(myStructObject1, myStructObject2)) // 调用静态方法Equals,判断两个对象是否相等 { Console.WriteLine("myStructObject1和myStructObject2是同一个对象\n"); } else { Console.WriteLine("myStructObject1和myStructObject2是不同的对象\n"); } // 从类创建对象 MyClass myObject1 = new MyClass(); // 创建对象时调用默认构造函数 Console.Write("MyObject1.x = {0}\t\t", myObject1.x); myObject1.x = 8; // 设置类的字段值 Console.WriteLine("MyObject1.x = {0}", myObject1.x); MyClass myObject2 = myObject1; // 类实例对象赋值 myObject2.SetX(9); // 调用对象的方法其设置字段值 Console.Write("myObject1.x = {0}\t\t", myObject1.x); Console.WriteLine("myObject2.x = {0}", myObject2.x); if (Object.Equals(myObject1, myObject2)) // 判断两个对象是否相等 { Console.WriteLine("myObject1和myObject2是同一个对象\n"); } else { Console.WriteLine("myObject1和myObject2是不同的对象\n"); }
(4)按Ctrl+F5组合键编译并运行程序。
4.3 构造函数与析构函数
构造函数和析构函数是两种特殊的类成员函数,构造函数是在创建给定类的实例时执行的类方法。它具有与类相同的名称,可以带或不带参数。它没有返回值,可以被重载,但不能被继承。构造函数在创建类实例时将被自动调用,通常用于初始化新对象的数据成员;析构函数则是一种用于实现销毁类实例所需操作的成员,其名称是在类名称前面添加一个“~”字符,不能带参数,也没有返回值。析构函数不能被继承,也不能被重载,在销毁对象时将被自动调用。
4.3.1 实例构造函数
实例构造函数是实现初始化类实例所需操作的成员,可用于创建和初始化类实例,创建新对象时将调用实例构造函数。
1. 声明实例构造函数
实例构造函数可以通过以下语法格式来声明:
[attributes] [constructor-modifiers] identifier([formal-parameter-list]): constructor-initializer constructor-body
实例构造函数声明可以包含一组attributes(属性)、4个访问修饰符(public、protected、internal和private)的有效组合或一个extern修饰符,在一个构造函数声明中同一修饰符不能多次出现。identifier必须是声明了该实例构造函数类的名称,若指定了任何其他名称,则发生编译时错误。
formal-parameter-list是可选项,用于定义构造函数的形参表,包含一个或多个由逗号分隔的形参。各个形参的类型必须至少具有与构造函数本身相同的可访问性,此形参表定义实例构造函数的签名,并且在函数调用中控制重载决策过程以选择某个特定实例的构造函数。
constructor-initializer是可选的,用于指定在执行此实例构造函数的constructor-body中给出的语句之前需要调用的另一个实例构造函数。
当构造函数声明中包含extern修饰符时,该构造函数称为“外部构造函数”。由于外部构造函数声明不提供任何实际的实现,所以其constructor-body仅由一个分号组成。所有其他构造函数的constructor-body都由一个语句块组成,用于指定初始化该类的一个新实例时需要执行的语句。
实例构造函数是不能继承的,因此一个类除了自己声明的实例构造函数外,不可能有其他实例构造函数。如果一个类不包含任何实例构造函数声明,则会自动地为该类提供一个默认实例构造函数。
2. 构造函数初始值设定项
除了类object的实例构造函数外,所有其他实例构造函数都隐式地包含一个对另一个实例构造函数的调用。该调用紧靠在constructor-body的前面,隐式调用的构造函数由constructor-initializer确定。
(1)base([argument-list]):调用基类的实例构造函数。
(2)this([argument-list]):调用该类本身所声明的实例构造函数。
如果一个实例构造函数中没有为构造函数初始值设定项,将会隐式地添加一个base()形式的构造函数为初始值设定项。因此对于类C来说,实例构造函数声明:
C(...) {...}
完全等效于:
C(...): base() {...}
3. 构造函数的参数
声明构造函数时可以为其提供一个形参列表。
例如,在下面的示例中为MyClass类声明了3个实例构造函数,其中第1个实例构造函数不包含形参;第2个实例构造函数包含一个形参;第3个实例构造函数包含两个形参,前面两个构造函数都没有执行代码,但其都包含构造函数初始值设定项,从而可以实现对第3个实例构造函数的调用:
class MyClass { public MyClass(): this(0, 0){} public MyClass(int x): this(x, 0){} public MyClass(int x, int y) { // 执行代码 } } // 若在创建类实例时调用带参数的构造函数,则必须提供参数 MyClass myObject = new MyClass(); // 等效于:MyClass(0, 0) MyClass myObject = new MyClass(1); // 等效于:MyClass(1, 0) MyClass myObject = new MyClass(1, 2);
4. 实例变量初始值设定项
当实例构造函数没有为构造函数初始值设定项时,或仅具有base(...)形式的构造函数初始值设定项时,该构造函数就会隐式地执行在该类中声明的实例字段的初始化操作。这对应于一个赋值序列,它们会在进入构造函数时,在隐式直接基类的构造函数调用之前执行。
5. 默认构造函数
如果一个类不包含任何实例构造函数声明,则会自动地为该类提供一个默认实例构造函数,默认构造函数只是调用直接基类的无形参构造函数。如果直接基类没有可访问的无形参实例构造函数,则发生编译时错误。对于类C来说,默认构造函数的形式如下:
protected C(): base() {}
或:
public C(): base() {}
在下面的示例中声明了一个Message类,其中不包含任何实例构造函数声明:
class Message { object sender; string text; }
此时将为该类提供一个默认构造函数,因此上述类声明完全等效于:
class Message { object sender; string text; public Message(): base() {} }
若要取消默认构造函数的自动生成,则必须在类声明中至少包含一个实例构造函数。
【例4-5】创建一个C#控制台应用程序,说明如何声明和调用默认实例构造函数和带参数实例构造函数,程序运行结果如图4-8所示。
图4-8 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-05”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,并在命名空间ConsoleApplication4_05中声明MyClass类,代码如下:
class MyClass { private int x; // 私有字段 private int y; // 私有字段 public MyClass() // 默认构造函数 { } public MyClass(int x, int y) // 带参数构造函数 { this.x = x; this.y = y; } public void showFields() // 公共实例方法 { Console.WriteLine("x = {0}\ty = {1}\n", this.x, this.y); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "实例构造函数应用示例"; MyClass myObect1 = new MyClass(); // 创建类实例,调用默认构造函数 myObect1.showFields(); // 调用实例方法 MyClass myObect2 = new MyClass(3,6); // 创建类实例,调用带参数构造函数 myObect2.showFields(); // 调用实例方法
(4)按Ctrl+F5组合键编译并运行程序。
4.3.2 私有构造函数
当在构造函数声明中包含private修饰符时,该构造函数就是一个私有构造函数。它是一种特殊的实例构造函数,通常用在只包含静态成员的类中。如果类具有一个或多个私有构造函数,而没有公共构造函数,则其他类(除嵌套类外)无法创建该类的实例。
如果在一个类中只声明了一个私有实例构造函数,则在该类的程序文本外部既不可能从该类派生出新的类,也不可能直接创建该类的任何实例。因此如果希望设计这样一个类,它只包含静态成员,而且有意使其不能被实例化,则只需为其添加一个空的私有实例构造函数。
声明空构造函数可阻止自动生成默认构造函数。如果不对构造函数使用任何访问修饰符,则在默认情况下它仍然是私有构造函数,但是通常显式地使用private修饰符来清楚地表明该类不能被实例化。
当没有实例字段或实例方法或当调用方法以获得类的实例时,私有构造函数可用于阻止创建类的实例。如果类中的所有方法都是静态的,可考虑使整个类成为静态的。
例如,在下面的示例中声明了一个Trig类,并为其声明了一个空的私有实例构造函数。该类可用于将相关的方法和常量组合在一起,但是不能被实例化:
public class Trig { private Trig() {} // 私有构造函数,用于阻止创建类的任何实例 public const double PI = 3.14159265358979323846; public static double Sin(double x) {...} public static double Cos(double x) {...} public static double Tan(double x) {...} }
【例4-6】创建一个C#控制台应用程序,说明如何在类中声明私有构造函数。同时为类声明了一些静态成员,程序运行结果如图4-9所示。
图4-9 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-06”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,并在命名空间ConsoleApplication4_06中声明Counter类,代码如下:
public class Counter { private Counter() { } // 私有构造函数 public static int count; // 静态字段 public static int IncrementCount() // 静态方法 { return ++count; // 对字段使用前缀增量运算符 } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "私有构造函数应用示例"; Counter.count = 1000; // 设置静态字段的值 Console.WriteLine("静态字段count的原值:{0}\n", Counter.count); Counter.IncrementCount(); // 调用静态方式 Console.WriteLine("静态字段count的新值:{0}\n", Counter.count);
(4)按Ctrl+F5组合键编译并运行程序。
4.3.3 静态构造函数
当在构造函数名前面使用static修饰符时,该构造函数就是一个静态构造函数。在创建第1个实例或引用任何静态成员之前将自动调用静态构造函数,可用于初始化任何静态数据,或用于执行仅需执行一次的特定操作。
静态构造函数是按照以下语法格式来声明:
[attributes] static-constructor-modifiers identifier( ) static-constructor-body
静态构造函数声明可包含一组attributes和一个extern修饰符,identifier必须是声明该静态构造函数的类的名称。如果指定了任何其他名称,则发生编译时错误。
当静态构造函数声明包含extern修饰符时,该静态构造函数称为“外部静态构造函数”。由于外部静态构造函数声明不提供任何实际的实现,所以其static-constructor-body由一个分号组成。对于所有其他静态构造函数声明,static-constructor-body都是一个语句块,用于指定当初始化该类时需要执行的语句。
静态构造函数具有以下特点。
(1)静态构造函数既没有访问修饰符,也没有参数。
(2)在创建第1个实例或引用任何静态成员之前,将自动调用静态构造函数来初始化类,将类中的每个静态字段初始化为默认值。
(3)静态构造函数是不可继承的,而且无法直接调用。
(4)在程序中用户无法控制何时执行静态构造函数。
(5)静态构造函数的典型用途是当类使用日志文件时,将使用这种构造函数在日志文件中写入项。
(6)如果类中包含用来开始执行的Main方法,则该类的静态构造函数将在调用Main方法之前执行。
(7)静态构造函数在给定应用程序域中至多执行一次。
【例4-7】创建一个C#控制台应用程序,说明如何通过调用类的静态方法来触发静态构造函数的调用。从而将类中的每个静态字段初始化为默认值,程序运行结果如图4-10所示。
图4-10 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-07”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)打开源文件Program.cs,并在命名空间ConsoleApplication4_07中声明Class1和Class2类:
class Class1 { static int x; // 静态字段 static Class1() // 静态构造函数 { Console.WriteLine("初始化类Class1"); } public static void F() // 静态方法 { Console.WriteLine("Class1.x = {0}", Class1.x); Console.WriteLine("调用静态方法Class1.F()\n"); } } class Class2 { static int x; // 静态字段 static Class2() // 静态构造函数 { Console.WriteLine("初始化类Class2"); } public static void F() // 静态方法 { Console.WriteLine("Class2.x = {0}", Class2.x); Console.WriteLine("调用静态方法Class2.F()\n"); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "静态构造函数应用示例"; Class1.F(); // 通过调用静态方法触发静态构造函数的执行 Class2.F(); // 通过调用静态方法触发静态构造函数的执行
(4)按Ctrl+F5组合键编译并运行程序。
4.3.4 析构函数
析构函数是一种用于实现销毁类实例所需操作的成员,其声明语法格式如下:
[attributes] [exter] ~identifier() destructor-body
构造函数声明可以包括一组attribute和一个可选的extern修饰符,identifier必须是声明该析构函数类的名称。如果指定任何其他名称,则发生编译时错误。
当析构函数声明包含extern修饰符时,称该析构函数为“外部析构函数”。由于外部析构函数声明不提供任何实际的实现,所以其destructor-body由一个分号组成。对于所有其他析构函数,destructor-body都由一个语句块组成,其中包含当销毁该类的一个实例时需要执行的语句。
使用析构函数时,应注意以下几点。
(1)不能在结构中定义析构函数,只能对类使用析构函数。
(2)一个类至多只能有一个析构函数。
(3)析构函数既没有修饰符及参数。
(4)析构函数通过重写System.Object中的虚方法Finalize来实现,C#程序中不允许重写此方法,也不允许直接调用此方法。
(5)不应使用空析构函数,否则将会导致不必要的性能损失。
(6)无法继承或重载析构函数。
(7)不能显式调用析构函数,它们是被自动调用的,程序退出时也会调用析构函数。
(8)何时调用析构函数是由垃圾回收器决定的,开发人员无法控制。
【例4-8】创建一个C#控制台应用程序,用于说明析构函数的声明和自动调用,程序运行结果如图4-11所示。
图4-11 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-08”,保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_08中声明Class1类并基于其声明派生Class2类,代码如下:
class Class1 // 声明内部类Class1 { ~Class1() // 析构函数 { Console.WriteLine("调用Class1的析构函数\n"); } } class Class2 : Class1 // 以Class1为基类,声明派生类Class2 { ~Class2() // 析构函数 { Console.WriteLine("调用Class2的析构函数\n"); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "析构函数应用示例"; Class2 obj = new Class2(); // 创建类实例 obj = null; // null关键字是表示不引用任何对象 // GC类位于System命名空间,表示控制系统垃圾回收器,这是一种自动回收未使用内存的服务 GC.Collect(); // 强制对所有代进行即时垃圾回收 GC.WaitForPendingFinalizers(); // 挂起当前线程,直到处理终结器队列的线程清空该队列为止
(4)按Ctrl+F5组合键编译并运行程序。
4.4 常量与字段
字段和常量都是类数据成员,字段是一种表示与对象或类关联的变量成员,类和结构使用字段可以封装数据;常量则是表示常量值(即可在编译时计算的值)的类成员,是在编译时设置其值并且永远不能更改其值的字段。
4.4.1 声明常量
通过声明常量可以为类可引入一个或多个给定类型的常量,语法格式如下:
[attributes] [constant-modifiers] const type identifier = constant-expression, identifier = constant-expression ;
其中attributes指定常量的属性;constant-modifiers是常量修饰符,包括new、public、protected、internal和private;const关键字用于声明常量;type指定常量的数据类型,必须是sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string、枚举类型或引用类型;identifier是标识符,用于命名常量;在标识符后面可以接“= constant-expression”,用于为常量赋值,要求每个constant-expression所产生的值必须属于目标类型,或者可以隐式转换为目标类型。
常量声明可以包含一组attributes、一个new修饰符和一个由4个访问修饰符构成的有效组合,属性和修饰符适用于声明的所有成员。虽然常量被认为是静态成员,但在常量声明中既不要求,也不允许使用static修饰符,同一个修饰符在一个常量声明中多次出现是错误的。常量的type必须至少与常量本身具有同样的可访问性。
常量本身可以出现在constant-expression中,因此可用在任何需要constant-expression的构造中。这样的构造示例包括case标签、goto case语句、枚举成员声明、属性和其他常量声明。
constant-expression是在编译时就可以计算出来的表达式。由于创建string以外的引用类型的非null值的唯一方法是应用new运算符,但constant-expression中不允许使用new运算符,因此除string以外的引用类型常量的唯一可能的值是null。
如果需要一个具有常量值的符号名称,但是该值的类型不允许在常量声明中使用,或在编译时无法由constant-expression计算出该值,则可以改用只读字段(4.4.4节)。
在一个常量声明中可以引入具有相同属性、修饰符和类型的多个常量,例如,在下面的类声明中通过一个常量声明引入了3个常量:
class A { public const double X = 1.0, Y = 2.0, Z = 3.0; }
上述语句相当于:
class A { public const double X = 1.0; public const double Y = 2.0; public const double Z = 3.0; }
一个常量可以依赖于同一程序内的其他常量,只要这种依赖关系不是循环的,编译器会自动地安排适当的顺序来计算各个常量声明。
【例4-9】创建一个C#控制台应用程序,用于说明同一程序中常量之间的依赖关系,程序运行结果如图4-12所示。
图4-12 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-09”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,并在命名空间ConsoleApplication4_09中声明A和B两个类,代码如下:
class A { public const int X = B.Z + 1; public const int Y = 10; } class B { public const int Z = A.Y + 1; }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "常量应用示例"; // 编译器首先计算A.Y,然后计算B.Z,最后计算A.X Console.WriteLine("A.X = {0}", A.X); Console.WriteLine("A.Y = {0}", A.Y); Console.WriteLine("B.Z = {0}\n", B.Z);
(4)按Ctrl+F5组合键编译并运行程序。
4.4.2 声明字段
字段可用以下语法格式来声明:
[attributes] [field-modifiers] type identifier = variable-initializer, identifier = variable-initializer, . . .;
其中attributes指定字段的属性;field-modifiers为字段修饰符,包括new、public、protected、internal、private、static、readonly和volatile;type指定由该声明引入的成员的数据类型;identifier是一个标识符,用于命名字段成员;根据需要标识符后可接“= variable-initializer”,用于指定字段成员的初始值。
字段声明可以包含一组attributes、一个new修饰符、由4 个访问修饰符(public、protected、internal和private)组成的一个有效组合和一个static修饰符。此外,字段声明可以包含一个readonly或一个volatile修饰符。但不能同时包含这两个修饰符。属性和修饰符适用于所声明的所有成员,同一个修饰符在一个字段声明中不能多次出现。在字段声明中,type必须至少与字段本身具有同样的可访问性。
在一个字段声明中可以声明具有相同属性、修饰符和类型的多个字段,例如,在声明A类时通过一个字段声明引入了3个字段:
class A { public static int X = 1, Y, Z = 100; }
上述代码等效于在类声明中使用了3个字段声明,即:
class A { public static int X = 1; public static int Y; public static int Z = 100; }
4.4.3 静态字段和实例字段
当一个字段声明中含有static修饰符时,由该声明引入的字段为静态字段;否则为实例字段。静态字段和实例字段是C#所支持的多种变量中的两种,有时被分别称为“静态变量”和“实例变量”。
静态字段不是特定实例的一部分,而是在类的所有实例之间共享。无论创建了多少个类的实例,对于关联的应用程序域来说,在任何时候静态字段都只会有一个副本。实例字段属于某个实例,具体而言,类的每个实例都包含了该类的所有实例字段的一个单独集合。
当用E.M形式来引用一个字段时,如果M是静态字段,则E必须表示含有M的类名;如果M是实例字段,则E必须表示一个含有M的类的某个实例。
字段声明可以包含初始化表达式,对于静态字段,变量初始值设定项相当于在类初始化期间执行的赋值语句;对于实例字段,变量初始值设定项相当于创建类的实例时执行的赋值语句。在程序中,也可以对字段执行前缀增量、前缀减量、后缀增量和后缀减量运算。
类的静态字段变量初始值设定项对应于一个赋值序列,这些赋值按其在相关的类声明中出现的文本顺序执行。如果类中存在静态构造函数,则静态字段初始值设定项的执行在该静态构造函数即将执行前发生,否则静态字段初始值设定项在第1次使用该类的静态字段之前被执行,但实际执行时间依赖于具体的实现;
类的实例字段变量初始值设定项对应于一个赋值序列,在控制进入该类的任一个实例构造函数时立即执行,这些变量初始值设定项按其出现在类声明中的文本顺序执行。实例字段的变量初始值设定项不能引用正在创建的实例,因此在变量初始值设定项中引用this是编译时错误。
【例4-10】创建一个C#控制台应用程序,用于说明静态字段与实例字段的区别。即静态字段在类的所有实例之间共享,实例字段则属于类的每个特定实例,程序运行结果如图4-13所示。
图4-13 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-10”,保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,并在命名空间ConsoleApplication4_10中声明一个内部类Count,代码如下:
class Count { static int count; // 静态字段 int number; // 实例字段 public Count() // 实例构造函数 { number = ++count; // 对字段进行前缀增量运算 } public void ShowFields() { Console.WriteLine("第 {0} 个实例:count = {1}", number, count); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "静态字段与实例字段应用示例"; Count obj1 = new Count(); obj1.ShowFields(); Console.WriteLine(); Count obj2 = new Count(); obj1.ShowFields(); obj2.ShowFields(); Console.WriteLine(); Count obj3 = new Count(); obj1.ShowFields(); obj2.ShowFields(); obj3.ShowFields(); Console.WriteLine();
(4)按Ctrl+F5组合键编译并运行程序。
4.4.4 只读字段
当字段声明中含有readonly修饰符时,该声明所引入的字段为只读字段。为其直接赋值只能作为声明的组成部分出现,或在同一类中的实例构造函数或静态构造函数中出现。在这些上下文中,只读字段可以被多次赋值。
只能在以下情况下为只读字段直接赋值。
(1)在包含字段声明的类中为字段设置初始值。
(2)在包含字段声明的类的实例构造函数中为实例字段赋值。
(3)在包含字段声明的类的静态构造函数中为静态字段赋值,也是可以将只读字段作为out或ref形参进行传递的仅有的上下文。
在其他任何上下文中试图为只读字段赋值或将只读字段作为out或ref形参传递都会导致编译时错误。
如果需要一个具有常量值的符号名称,但该值的类型不允许在const声明中使用或者无法在编译时计算出该值,则静态只读字段可以发挥作用,这种字段可以通过在字段声明中使用static readonly修饰符组合来引入。在下面的示例中,为Color类声明了5个静态只读字段:
public class Color { // 声明5个静态只读字段 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 red, green, blue; // 私有字段 public Color(byte r, byte g, byte b) // 在实例构造函数中设置静态只读字段的初始值 { red = r; green = g; blue = b; } }
Black、White、Red、Green和Blue成员不能被声明为Color类的常量成员,这是因为在编译时无法计算其值,将其声明为该类的静态只读字段可达到基本相同的效果。
【例4-11】创建一个C#控制台应用程序,用于说明如何通过静态构造函数为静态只读字段设置初始值,程序运行结果如图4-14所示。
图4-14 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-11”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,并在命名空间ConsoleApplication4_11中声明一个内部类Graph,代码如下:
class Graph { public static readonly int ScreenWidth; // 静态只读字段 public static readonly int ScreenHeight; // 静态只读字段 // 静态构造函数 static Graph() { // 在静态构造函数中设置静态只读字段的值 ScreenWidth = 1280; ScreenHeight = 1024; } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "只读字段应用示例"; // 当引用Graph类的静态字段时,将触发对静态构造函数的调用 Console.WriteLine("ScreenWidth = {0}\n", Graph.ScreenWidth); Console.WriteLine("ScreenHeight = {0}\n", Graph.ScreenHeight);
(4)按Ctrl+F5组合键编译并运行程序。
4.4.5 可变字段
当字段声明中含有volatile修饰符时,该声明引入的字段为可变字段。volatile关键字指示一个字段可以由多个同时执行的线程修改。声明为volatile的字段不受编译器优化(假定由单个线程访问)的限制,这样可以确保该字段在任何时间呈现的都是最新值。
由于采用优化技术会重新安排指令的执行顺序,所以在多线程的程序运行环境下,如果未采取同步控制手段,则对于非可变字段的访问可能会导致意外或不可预见的结果。这些优化可以由编译器、运行时系统或硬件执行,可变字段在优化时的这种重新排序必须遵循以下规则。
(1)读取一个可变字段称为“可变读取”,按照指令序列所有排在可变读取之后的对内存的引用在执行时也一定排在其后。
(2)写入一个可变字段称为可变写入;按照指令序列,所有排在可变写入之前的对内存的引用在执行时也一定排在其前。
【例4-12】创建一个C#控制台应用程序,用于说明如何声明可变字段并读取,程序运行结果如图4-15所示。
图4-15 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-12”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,并在using指令节末尾添加以下指令:
using System.Threading;
(3)在Program类中声明一个静态字段result、一个可变字段finished和一个静态方法Thread2,并通过静态方法Thread2为两个字段赋值,代码如下:
public static int result; // 静态字段 public static volatile bool finished; // 静态可变字段 static void Thread2() { result = 169; finished = true; }
(4)在Program类的Main方法中编写以下代码:
Console.Title = "可变字段应用示例"; finished = false; // 在一个新进程中执行Thread2() new Thread(new ThreadStart(Thread2)).Start(); // 等待Thread2发信号,通过设置finished为true而得到结果 for ( ; ; ) { if (finished) { Console.WriteLine("result = {0}\n", result); return; } }
【代码说明】
在本例中,Main方法启动了一个新线程,该线程运行方法Thread2。它把一个值存储在名为“result”的非可变字段中,并把true存储在可变字段finished中。主线程等待字段finished被设置为true,然后读取字段result。由于finished已被声明为volatile,所以主线程从字段result读取的值一定是169。如果字段finished未被声明为volatile,则存储finished之后主线程可看到存储result。因此主线程从字段result读取值0,把finished声明为volatile字段可以防止这种不一致性。
(5)按Ctrl+F5组合键编译并运行程序。
4.5 继承
继承是面向对象编程的重要特征之一,通过继承可以在已有类的基础上创建新类。新类获得已有类的所有非私有成员,而且允许重新定义或添加新的成员,从而形成类的层次结构。在这个层次结构中,已有类称为“基类”,新类则称为“派生类”。通过继承机制可以在现有类的基础上快速创建一个新类,从而增强代码的可重用性,以提高应用开发的效率。
4.5.1 类的继承
在派生类的声明中可以指定新类要继承的基类名称,语法格式如下:
[attributes] [class-modifiers] class identifier: class-type { // 类成员声明 }
其中attributes指定类的属性,class-modifiers为类修饰符,identifier指定类的名称,class-type指定该类要继承的基类名称。如果省略类名称后面的冒号(:)和基类名称,则新类从System.Object类继承。
一个类继承其直接基类类型的成员,继承意味着一个类隐式地将其直接基类类型的所有成员作为自己的成员,但基类的实例构造函数、析构函数和静态构造函数除外。
类的继承具有以下特点。
(1)继承是可传递的。如果C从B派生,而B从A派生,则C就会既继承在B和A中声明的成员。
(2)派生类扩展其直接基类。派生类能够在继承基类的基础上添加新的成员,但是不能移除继承成员的定义。
(3)实例构造函数、析构函数和静态构造函数都是不可继承的,但所有其他成员是可继承的,无论它们所声明的可访问性如何。在派生类中可以使用base关键字来访问基类的成员。但是根据它们所声明的可访问性,有些继承成员在派生类中可能是无法访问的。
(4)在派生类中可以使用new修饰符来声明具有相同名称或签名的新成员,以隐藏被继承的成员。但是应当注意隐藏继承成员并不移除该成员,而只是使被隐藏的成员在派生类中不可直接访问。
(5)类的实例含有在该类及其所有基类中声明的所有实例字段集,并且存在一个从派生类类型到其任何一种基类类型的隐式转换,因此可以将对某个派生类实例的引用视为对它的任一个基类实例的引用。
(6)在类中可以使用virtual修饰符来声明虚的方法、属性和索引器,而派生类可以重写(使用override修饰符)这些函数成员的实现。从而使得类展示出多态性行为特征。即同一个函数成员调用所执行的操作可能是不同的,这取决于用来调用该函数成员的实例的运行时类型。
【例4-13】创建一个C#控制台应用程序,首先声明一个Person类。然后以Person类作为基类来创建派生类Student,程序运行结果如图4-16所示。
图4-16 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-13”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_13中声明一个Person类。然后以Person类作为基类声明派生类Student,代码如下:
class Person { protected string name; // 受保护字段成员,可在当前类和派生类中访问 protected string gender; // 受保护字段成员 public static int count; // 静态字段成员 public Person(string name,string gender) // 实例构造函数 { this.name = name; this.gender = gender; count++; } public void ShowInfo() // 公共方法 { Console.WriteLine("姓名:{0}\t性别:{1}", name, gender); } } class Student : Person // 以Person作为基类 { private string studentID; // 新的类成员 private int age; // 新的类成员 public Student(string studentID, string name, string gender, int age) : base(name, gender) // 调用直接基类的实例构造函数 { this.studentID = studentID; this.age = age; } new public void ShowInfo() // 用new关键字隐藏基类中的同名方法 { Console.WriteLine("学生{0}\n------------------------",count); base.ShowInfo(); // 调用基类的方法 Console.WriteLine("学号:{0}\t年龄:{1}\n", studentID, age); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "类的继承示例"; Student student1 = new Student("080101", "张华", "男", 20); // 创建类的实例 student1.ShowInfo(); // 调用类的方法 Student student2 = new Student("080102", "李倩", "女", 19); student2.ShowInfo();
(4)按Ctrl+F5组合键编译并运行程序。
4.5.2 抽象类
如果在类声明中使用abstract修饰符,则表示所修饰的类是不完整的。并且它只能用做基类,该类称为“抽象类”。抽象类的用途是为多个派生类提供可共享基类的公共定义。例如,在类库中可以定义一个抽象类来作为其多个函数的参数,并要求使用该库的开发人员通过创建派生类来提供自己的类实现。
抽象类与非抽象类的差别如下。
(1)抽象类不能直接实例化,并且对抽象类使用new运算符会导致编译时错误。虽然一些变量和值在编译时的类型可以是抽象的,但是这样的变量和值必须或者为null,或者含有对非抽象类的实例的引用(此非抽象类是从抽象类型派生的)。
(2)允许(但不要求)抽象类包含抽象成员,例如在抽象类中可以定义抽象方法,为此可将关键字abstract添加到方法的返回类型的前面。抽象方法没有实现,所以方法定义后面是分号,而不是常规的方法块。
(3)当抽象类从基类继承虚方法时,可以使用抽象方法重写该虚方法。
(4)抽象类不能被密封。
(5)当从抽象类派生非抽象类时,这些非抽象类必须具体实现所继承的所有抽象成员,从而重写那些抽象成员(在返回类型前面使用override修饰符)。
在下面的示例中抽象类A引入抽象方法F,类B引入另一个方法G。但由于它不提供F的实现,所以B也必须声明为抽象类。类C重写F,并提供一个具体实现。由于C没有抽象成员,因此C可以(但不要求)是非抽象的:
abstract class A // 抽象类 { public abstract void F(); // 抽象方法 } abstract class B : A // 抽象类 { public void G() {} } class C : B // 非抽象类 { public override void F() { // 重写抽象方法 // F的实现代码 } }
在下面的示例中类D引入了一个虚方法DoWork并提供了实现代码,在抽象类E中用抽象方法重写此虚方法,类F中的DoWork无法调用类D上的DoWork。在这种情况下,抽象类可以强制派生类为虚方法提供新的方法实现:
public class D { public virtual void DoWork(int i) // 虚方法,可在派生类中重写 { // 虚方法的实现代码 } } public abstract class E : D { public abstract override void DoWork(int i); // 用抽象方法重写虚方法 } public class F : E { public override void DoWork(int i) // 重写抽象方法 { // 新的实现代码 } }
【例4-14】创建一个C#控制台应用程序,首先声明一个抽象类Shape并为其声明一个抽象方法GetArea。然后在Rectangle和Circle类中重写此抽象方法,程序运行结果如图4-17所示。
图4-17 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-14”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_14中声明一个抽象类Shape。然后以Shape类作为基类来声明派生类Rectangle和Circle,代码如下:
abstract class Shape // 抽象类 { protected double x = 0, y = 0; public Shape(double x, double y) { this.x = x; this.y = y; } public abstract double GetArea(); // 抽象方法 } class Rectangle : Shape { public Rectangle(double x, double y) : base(x, y) { } public override double GetArea() // 重写GetArea方法 { return x * y; } } class Circle : Shape { public Circle(double r): base(r, r) { } public override double GetArea() // 重写GetArea方法 { return x * x * Math.PI; } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "抽象类应用示例"; Rectangle r = new Rectangle(3, 6); Console.WriteLine("矩形面积为:{0}\n",r.GetArea()); Circle c = new Circle(10); Console.WriteLine("圆形面积为:{0}\n", c.GetArea());
(4)按Ctrl+F5组合键编译并运行程序。
4.5.3 密封类
如果在类声明中使用sealed修饰符,则可防止从所修饰的类中派生其他类,该类称为“密封类”。sealed修饰符主要用于防止非有意的派生,但是可以促使某些运行时优化。当把一个密封类指定为其他类的基类时,会发生编译时错误。
密封类也不能同时为抽象类,将abstract修饰符用于密封类是错误的做法,因为抽象类必须由提供抽象方法或属性的实现的类继承。
在下面的示例中类B从类A继承,但是任何类都不能从类B继承:
class A {} sealed class B : A {}
【例4-15】创建一个C#控制台应用程序,用于说明如何声明密封类并创建类实例,程序运行结果如图4-18所示。
图4-18 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-15”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_15中声明一个密封类SealedClass,代码如下:
sealed class SealedClass // 声明密封类 { private int x,y; // 私有字段 public SealedClass(int x, int y) // 构造函数 { this.x = x; this.y = y; } public void ShowFields() // 公共方法 { Console.WriteLine("x = {0}\ty = {1}\n", x, y); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "密封类应用示例"; SealedClass obj = new SealedClass(100, 200); obj.ShowFields();
(4)按Ctrl+F5组合键编译并运行程序。
在本例中,如果试图在命名空间ConsoleApplication4_15中编写以下代码:
class DerivedClass : SealedClass {}
则会出现编译错误,即“无法从密封类型ConsoleApplication4_15.SealedClass派生”。
4.5.4 System.Object类
Object类支持 .NET Framework类层次结构中的所有类,并为派生类提供低级别服务。它是 .NET Framework中所有类的最终基类,即类型层次结构的根。Object类位于命名空间System中,程序集为mscorlib(在mscorlib.dll中)。
在C#语言中通常不要求类声明从Object的继承,因为继承是隐式的。由于 .NET Framework中的所有类均从Object派生,所以Object类中定义的每个方法可用于系统中的所有对象,而且派生类可以重写其中的某些方法。
Object类的方法成员如下。
(1)Equals:确定两个Object实例是否相等,有以下两种语法形式:
public virtual bool Equals(Object obj) public static bool Equals(Object objA, Object objB)
第1种语法格式确定指定的Object是否等于当前的Object,参数obj为System.Object类型,指定要与当前Object进行比较的Object,返回值为System.Boolean类型。如果指定的Object等于当前的Object,则为true;否则为false。
第2种语法格式确定两个指定的Object实例是否被视为相等,参数objA为System.Object类型,指定要比较的第1个Object。objB也是System.Object类型,以及要比较的第2个Object,返回值为System.Boolean类型。如果objA是与objB相同的实例,或者二者均为空引用,或者objA.Equals(objB)返回true,则为true;否则为false。
(2)Finalize:允许Object在“垃圾回收”回收Object之前尝试释放资源并执行其他清理操作,语法如下:
protected virtual void Finalize()
默认情况下,Object.Finalize不执行任何操作。如果必须执行Finalize操作,垃圾回收过程中的回收往往需要很长的时间,因此只有在必要时才必须由派生类重写它。
如果Object保存对任何资源的引用,则Finalize必须由派生类重写,以便在垃圾回收过程中放弃Object之前释放这些资源。
(3)GetHashCode:用做特定类型的哈希函数,生成一个与对象值相对应的数字以支持哈希表的使用,语法如下:
public virtual int GetHashCode()
GetHashCode方法适用于哈希算法和诸如哈希表之类的数据结构。
(4)GetType:获取当前实例的Type,语法如下:
public Type GetType()
返回值为类型System.Type实例,表示当前实例的运行时类型。
对于具有相同运行时类型的两个对象x和y来说,Object.ReferenceEquals(x.GetType(),y.GetType())返回true。
(5)MemberwiseClone:创建当前Object的浅表副本,语法如下:
protected Object MemberwiseClone()
返回值为System.Object类型,表示当前Object的浅表副本。
(6)ReferenceEquals:确定指定的Object实例是否是相同的实例,语法如下:
public static bool ReferenceEquals(Object objA, Object objB)
其中参数objA的类型为System.Object,表示要比较的第1个Object;objB的类型为System.Object,表示要比较的第2个Object。
返回值的类型为System.Boolean,如果objA是与objB相同的实例,或者二者均为空引用,则返回true;否则返回false。
(7)ToString:返回表示当前Object的字符串。语法如下:
public virtual string ToString()
返回值的类型为System.String,表示当前的Object,默认实现返回Object的类型的完全限定名。在派生类中可以重写此方法,以返回对该类型有意义的值。
4.5.5 可视化OOP工具
Visual Studio 2008提供了一些可视化工具,例如类视图窗口、对象浏览器及类关系图等,可以用来帮助创建面向对象的应用程序。
1. 类视图窗口
类视图用于显示正在开发的应用程序中定义、引用或调用的符号。单击“视图”→“类视图”命令,打开“类视图”窗口。其中包括上部的“对象”窗格和下部的“成员”窗格,“对象”窗格包含一个可以展开的符号树,其顶级节点表示项目。若要展开树中选定的节点,可单击其加号图标或按数字小键盘上的加号(+)键。
图标用于标识项目中使用的分层结构,如命名空间、类型、接口、枚举和类,通过展开这些结构可以列出其成员。“成员”窗格中列出了属性、方法、事件、变量、常量和包含的其他项,这种按项目分层的视图阐明了代码中的符号结构,使用“类视图”可以打开文件并直接定位到显示符号的各个行。
例如,在项目ConsoleApplication4-13中定义了Person、Student和Program类。其中Student从基类Person派生,所有这些类都继承了Object类的方法,如图4-19和图4-20所示。从图4-20中可以看出,派生类Student除了拥有其基类Ojbect和Person的成员外,还包含其特有的成员。
图4-19 Program类成员
图4-20 Student类成员
2. 对象浏览器
使用对象浏览器可以选择和检查可用于项目的符号,为打开对象浏览器,单击“视图”→“对象浏览器”命令,或者单击主工具栏上的“对象浏览器”按钮。其中有3个窗格,即“对象”(左侧)、“成员”(右上侧)和“说明”(右下侧)窗格。如图4-21所示,在“对象”窗格中选择项目ConsoleApplication4-13下的Object类,并在“成员”窗格中选择ToString方法,此时将在“说明”窗格中显示出该方法的签名、所属的类、方法摘要、参数,以及返回值信息。
图4-21 对象浏览器
根据需要,也可以为自定义的类添加描述信息并显示在“说明”窗格中,为此可采用以下语法格式来撰写XML文档说明:
/// <summary> /// 摘要信息 /// </summary> /// <param name="value"> /// 参数信息 /// </param> /// <returns> /// 返回值信息 /// </returns>
在代码编辑器中输入3个斜杠“///”后将会自动添加其他相关内容,输入所需的说明信息即可。
3. 类关系图
类关系图可以帮助理解项目的类结构,用其可以自定义并展示项目信息,为此首先应创建一个可显示需要显示内容的类关系图。在一个项目中可以创建多个类关系图,用于显示项目的不同视图、项目类型的所选子集,以及类型成员的所选子集。除了定义每个类关系图显示的内容外,也可以更改信息的展示方式。对一个或多个类关系图进行微调之后,可以将其复制到Microsoft Office文档并打印,也可以作为图像文件导出。
通过在项目中添加类关系图,在Visual Studio项目中可以使用类设计器开始设计、编辑并重构类与其他类型。既可以在项目中添加空白的类关系图,也可以用现有的类型在项目中创建关系图。每个项目可以包含多个类关系图,用于可视化项目的不同代码区域。若要在项目中添加空白的类关系图,则执行以下操作。
(1)在解决方案资源管理器中单击项目名称,然后单击“项目”→“添加新项”命令。
(2)在如图4-22所示的“添加新项”对话框中,选择“模板”下拉列表框中的“类关系图”选项,在“名称”文本框中输入类关系图的名称(文件扩展名为“.cd”),然后单击“添加”按钮。
图4-22 “添加新项”对话框
类关系图随即在类设计器中打开,并在解决方案资源管理器的项目层次结构中以一个带.cd扩展名的文件出现。同时出现如图4-23所示的类设计器工具箱,其中包含一些形状和连线。可以将其拖到关系图上,以创建新的类、接口及结构等,或者在两个类之间定义继承关系。
图4-23 类设计器工具箱
若要在类关系图上显示项目中的类型,可从解决方案资源管理器中的项目节点打开一个类关系图(.cd)文件。然后从解决方案资源管理器中的项目节点将一个源代码文件拖动到类关系图中,此时表示在源代码文件中定义的类型的形状显示在关系图上。此外,也可以将一个或多个类型从类视图中的项目节点拖动到类关系图中来查看项目中的类型。
也可以为解决方案创建类关系图,为此可在解决方案资源管理器中单击一个源文件。然后单击工具栏中的“类关系图”按钮,或从解决方案资源管理器中的“类”和“项目”节点,以及“类视图”中的“命名空间”和“类型”节点的快捷菜单中选择“查看类关系图”命令。若要在关系图的默认位置显示类型,可在类视图中右击一个或多个类型,然后单击“查看类关系图”命令。
例如,在解决方案chapter04中展开项目ConsoleApplication4-13,选择源文件Program.cs。单击“类关系图”按钮(如图4-24所示),此时将在类设计器中打开一个名为“ClassDiagram1.cs”的类关系图文件,如图4-25所示。
图4-24 “查看类关系图”按钮
图4-25 源文件中的类型
在类设计器中单击一个类型的形状会在屏幕下方的“类详细信息”窗格中显示该类所有成员的信息,如图4-26所示。根据需要,也可以在所选择的类中添加新的成员。
图4-26 所有类成员的信息
在类设计器中可以通过从类设计器工具箱拖动类型在类关系图中创建类型,工具箱包含类、枚举、接口、抽象类、结构和委托等类型。若要在类关系图中创建新的类型,则执行以下操作。
(1)从解决方案资源管理器中的项目打开一个类关系图(.cd)文件。
(2)在工具箱中单击“类设计器”标签,打开类设计器工具箱。
(3)将一个类型形状从类设计器工具箱拖动到类关系图中,此时将出现“新类”对话框。其中<Type>标识要创建的类型,如“类”或“枚举”。例如,当把表示类的形状拖动到类关系图时将出现如图4-27所示的“新类”对话框,可以在其中指定类型的名称、用来保存类型的代码的文件名,以及该类型的访问级别。
图4-27 “新类”对话框
(4)在“新类”对话框中指定类型的名称和文件名,并在“访问”下拉列表框中选择类型的访问级别,若要选择现有的文件,单击“文件名”框旁边的省略号按钮,设置后单击“确定”按钮。
此时,表示类型的形状即会出现在类关系图上。如果指定新的文件名,则Visual Studio会创建一个源代码文件,其名称与解决方案资源管理器中项目节点中类型的名称相同;如果选择现有的文件,则Visual Studio将新类型的代码添加到指定文件中。
4.6 方法
方法是在类中定义的一种函数成员,其中包含一系列语句的代码块集中体现了对象或类的行为,可以用于实现由对象或类执行的计算或操作。在C#程序中,每个执行指令都是在方法的上下文中执行的。创建方法是类设计的重要内容之一,通过方法可以把应用程序划分为一些小的功能模块。
4.6.1 声明方法
在类定义中,可以使用以下语法格式来声明方法:
[attributes] [method-modifiers] [partial] return-type identifier [type-parameter-lis] ([formal-parameter-list]) method-body
其中attributes是可选项,用于指定方法的属性;method-modifiers是可选的方法修饰符,包括new、public、protected、internal、private、static、virtual、sealed、override、abstract和extern;partial是可选的,用于声明分部方法。
return-type用于指定由该方法计算和返回的值的类型,如果方法不返回一个值,则其return-type为void;如果方法声明包含partial修饰符,则返回类型必须为void。
identifier为标识符,用于指定方法的名称;type-parameter-list是可选的,用于指定方法的类型形参。如果指定了type-parameter-list,则方法是泛型方法。
如果方法具有extern修饰符,则不能指定类型形参。formal-parameter-list是可选的,用于指定方法的形参表。各个形参括在圆括号中,并使用逗号隔开。空括表示方法不需要形参。
return-type和formal-parameter-list中引用的各个类型必须至少具有与方法本身相同的可访问性。
method-body是方法体,abstract和extern方法的方法体仅有一个分号;partial方法的方法体由一个分号或由一个语句块组成;其他所有方法的方法体由一个语句块组成,用于指定在调用方法时要执行哪些语句。
方法声明可以包含一组attributes和由4个访问修饰符(public、protected、internal及private)组成的有效组合,还可以含有new、static、virtual、override、sealed、abstract和extern修饰符。
如果下列所有条件为真,则方法声明具有一个有效的修饰符组合。
(1)该声明包含一个由访问修饰符组成的有效组合。
(2)该声明中所含的修饰符彼此不相同。
(3)该声明最多包含static、virtual和override修饰符中的一个。
(4)该声明最多包含new和override修饰符中的一个。
(5)如果声明中包含abstract修饰符,则不包含修饰符static、virtual、sealed或extern。
(6)如果声明中包含private修饰符,则不包含修饰符virtual、override或abstract。
(7)如果声明包含sealed修饰符,则还包含override修饰符。
(8)如果声明中包含partial修饰符,则不包含修饰符new、public、protected、internal、private、virtual、sealed、override、abstract或extern。
一个方法的名称、类型形参列表和形参表定义了该方法的签名,即一个方法的签名由名称、类型形参的数目及其形参的数目、修饰符和类型组成。方法的任何类型形参都不按名称标识,而是按其在方法的类型实参列表中的序号位置标识。返回类型、类型形参或形参的名称并不是方法签名的组成部分。
方法的名称必须不同于在同一个类中声明的所有其他非方法成员的名称。此外方法的签名在声明该方法的类中必须是唯一的,必须不同于在同一类中声明的所有其他方法的签名,并且在同一类中声明的两种方法的签名不能只有ref和out不同。
方法具有一个形参列表(可能为空),表示传递给该方法的值或变量引用。也可以有一组类型形参,当调用方法时必须为类型形参指定类型实参。
方法还具有一个返回类型,指定该方法计算和返回的值的类型。如果方法不返回值,则其返回类型为void,此时不允许该方法体中的return语句指定表达式。如果一个void方法的方法体执行正常完成,即控制自方法体的结尾离开,则该方法只是返回到其调用方。
当方法的返回类型不是void时,该方法体中的每个return语句都必须指定一个可隐式转换为返回类型的表达式。
4.6.2 方法形参
为了向方法传递值或变量引用,声明一个方法时可以为其定义一个形参表。形参的4种形式为声明时不带任何修饰符的值形参、用ref修饰符声明的引用形参、用out修饰符声明的输出形参和用params修饰符声明的形参数组。其中ref和out修饰符是方法签名的组成部分,但params修饰符不是。
1. 值形参
在方法声明中不带修饰符的形参是值形参,一个值形参对应于一个局部变量,只是其初始值来自该方法调用所提供的相应实参。当形参是值形参时,方法调用中的对应实参必须是一个表达式,并且其类型可以隐式转换为形参的类型。
在方法内部可以把新值赋给值形参,这样的赋值只影响由该值形参对应的局部变量,而不会影响在方法调用时由调用方给出的实参。
【例4-16】创建一个C#控制台应用程序,说明在方法内部为值形参变量赋值不会影响调用方法时给出的实参变量,程序运行结果如图4-28所示。
图4-28 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-16”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在Program类中声明一个静态方法Swap,代码如下:
static void Swap(int x,int y) // x和y为值形参 { int temp; // 在方法内部交换值形参变量x和y的值 temp = x; x = y; y = temp; }
(3)在Program类的Main方法中编写以下代码:
int a = 200, b = 300; Console.Title = "值形参应用示例"; Console.WriteLine("调用Swap之前:\na = {0}\t\tb = {1}\n", a, b); // 调用静态方法,实参a和b是Main方法中的局部变量 Swap(a, b); // 与Swap调用前相比,a和b保持不变 Console.WriteLine("调用Swap之后:\na = {0}\t\tb = {1}\n", a, b);
(4)按Ctrl+F5组合键编译并运行程序。
2. 引用形参
在方法声明中用ref修饰符声明的形参是引用形参,它并不创建新的存储位置;相反,引用形参表示的存储位置恰是在方法调用中作为实参给出变量所表示的存储位置。
当形参为引用形参时,方法调用中的对应实参必须由关键字ref后接一个与形参类型相同的变量引用组成。变量在可以作为引用形参传递之前,必须明确赋值。在方法内部,引用形参始终被认为是明确赋值的。如果在方法内部为引用形参赋值,将会影响方法调用时由调用方给出的实参变量。
【例4-17】创建一个C#控制台应用程序,说明在方法内部为引用形参变量赋值将会更改调用方法时给出的实参变量,程序运行结果如图4-29所示。
图4-29 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-17”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在Program类中声明一个静态方法Swap,代码如下:
static void Swap(ref int x,ref int y) // x和y为引用形参 { int temp; // 在方法内部交换引用形参变量x和y的值 temp = x; x = y; y = temp; }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "引用形参应用示例"; int a = 200, b = 300; Console.WriteLine("调用Swap之前:\na = {0}\t\tb = {1}\n", a, b); // 调用Swap方法时,要对实参使用ref修饰符 Swap(ref a, ref b); // 与Swap调用前相比,变量a和b的值被交换 Console.WriteLine("调用Swap之后:\na = {0}\t\tb = {1}\n", a, b);
(4)按Ctrl+F5组合键编译并运行程序。
3. 输出形参
在方法声明中用out修饰符声明的形参是输出形参,类似于引用形参,该形参不创建新的存储位置;相反,它表示的存储位置恰是在该方法调用中作为实参给出的变量所表示的存储位置。
当形参为输出形参时,方法调用中的相应实参必须由关键字out后接一个与形参类型相同的变量引用组成。变量在可以作为输出形参传递之前不一定需要明确赋值,但是在将变量作为输出形参传递的调用之后,该变量被认为是明确赋值的。
在方法内部与局部变量相同,输出形参最初被认为是未赋值的,因而必须在使用其值之前明确赋值。在方法返回之前该方法的每个输出形参都必须明确赋值,输出形参通常用在需要产生多个返回值的方法中。
【例4-18】创建一个C#控制台应用程序,在Circle类中定义一个带有两个输出形参的GetData方法用于计算圆面积和圆周长,程序运行结果如图4-30所示。
图4-30 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-18”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在命名空间ConsoleApplication4_18中定义一个内部类Circle,代码如下:
class Circle { private double radius; // 私有字段 public Circle(double r) // 实例构造函数 { radius = r; } // 公共方法,其中area和perimeter为输出形参 public void GetData(out double area,out double perimeter) { area = radius * radius * Math.PI; // 为输出形参area赋值 perimeter = 2 * radius * Math.PI; // 为输出形参perimeter赋值 } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "输出形参应用示例"; double r = 10, a, c; Circle circle1 = new Circle(r); // 创建类实例 // 调用GetData方法之前,a和c尚未赋值 circle1.GetData(out a, out c); //调用GetData方法之后,a和c被明确赋值 Console.WriteLine("当半径 = {0}时:", r); Console.WriteLine("圆面积 = {0,6:F2}\t圆周长 = {1,6:F2}\n", a, c);
(4)按Ctrl+F5组合键编译并运行程序。
4. 形参数组
在方法声明中,用params修饰符声明的形参是形参数组。如果形参表包含一个形参数组,则这个形参数组必须位于列表的最后,而且必须是一维数组类型。例如,类型int[]、string[]和string[][]可以用做形参数组的类型,但是类型string[,]不能。不能将params修饰符与ref和out修饰符组合使用。
对于包含形参数组的方法,调用该方法时可以通过如下方式之一来为形参数组指定对应的实参。
(1)赋予形参数组的实参是一个表达式,其类型可以隐式转换为该形参数组的类型。在这种情况下,形参数组的作用与值形参完全一样。
(2)为形参数组指定零个或多个实参,其中每个实参都是一个表达式,其类型可隐式转换为该形参数组的元素的类型。在这种情况下,调用方法时会创建一个该形参数组类型的实例,其中包含的元素个数等于给定的实参个数。并用给定的实参值初始化此数组实例的每个元素,然后将新创建的数组实例用作实参。
除了允许在调用中使用可变数量的实参,形参数组与同一类型的值形参完全等效。
【例4-19】创建一个C#控制台应用程序,用于说明如何为类的方法成员定义形参数组并为其指定对应的形参,程序运行结果如图4-31所示。
图4-31 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-19”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在Program类中定义带有形参数组的静态方法F,代码如下:
static void F(params int[] args) // args为形参数组 { Console.Write("数组包含 {0} 个元素:", args.Length); foreach (int i in args) { Console.Write("{0} ", i); } Console.WriteLine("\n"); }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "形参数组应用示例"; F(); // 实参个数为0 F(1, 2, 3, 4, 5); // 传递了5个实参 int[] arr={10, 20, 30, 40}; // 定义一个数组 F(arr); // 以数组引用作为实参 F(new int[] {100, 200, 300}); // 以新建数组作为实参
(4)按Ctrl+F5组合键编译并运行程序。
4.6.3 方法重载
每个类型成员都有一个唯一的签名,方法签名由方法名称、类型形参的个数及其每个形参(按从左到右的顺序)的类型和种类(值、引用或输出)组成。只要签名不同,就可以在一种类型内定义具有相同名称的多种方法。当定义两种或多种具有相同名称的方法时,称为“重载”。
方法重载是指在类、结构或接口中用同一个名称来声明多个方法,前提是其签名在该类、结构或接口中是唯一的,签名是对类、结构和接口的成员实施重载的机制。
下面的示例演示了一组重载方法声明及其签名:
class Test { void F() {. . .} // F() void F(int x) {. . .} // F(int) void F(ref int x) {. . .} // F(ref int) void F(out int x) {. . .} // F(out int),错误 void F(int x, int y) {. . .} // F(int, int) int F(string s) {. . .} // F(string) int F(int x) {. . .} // F(int),错误 void F(string[] a) {. . .} // F(string[]) void F(params string[] a) {. . .} // F(string[]),错误 }
由于ref和out参数修饰符都是签名的组成部分,因此F(int)和F(ref int)这两个签名都具有唯一性。但是F(ref int)和F(out int)不能在同一个类型中声明,因为其签名仅ref和out不同。此外,返回类型和params修饰符不是签名的组成部分。不能仅基于返回类型或是否存在params修饰符来实施重载,因此上面列出的关于方法F(int)和F(params string[])的声明会导致编译时错误。
【例4-20】创建一个C#控制台应用程序,通过方法重载分别计算两个整数和两个双精度数的最大值,程序运行结果如图4-32所示。
图4-32 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-20”,保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在命名空间ConsoleApplication4_20中定义一个内部类Test,代码如下:
class Test { public int MaxValue(int x, int y) { return (x > y ? x : y); } public double MaxValue(double x, double y) { return (x > y ? x : y); } }
(3)在Program类的Main方法中编写以下代码:
onsole.Title = "方法重载示例"; Test t = new Test(); int a = 123, b = 456; Console.WriteLine("a = {0}\t\tb = {1}\t\t最大值 = {2}\n",a,b,t.MaxValue(a,b)); double x = 123.456, y = 678.345; Console.WriteLine("x = {0}\ty = {1}\t最大值 = {2}\n", x, y, t.MaxValue(x,y));
(4)按Ctrl+F5组合键编译并运行程序。
4.6.4 静态方法和实例方法
如果一个方法声明中含有static修饰符,则声明该方法属于类本身。而不是属于类的特定对象,该方法称为“静态方法”;如果一个方法声明中没有static修饰符,则该方法属于类的特定对象,并称为“实例方法”。
静态方法不对特定实例进行操作,它只能访问类的静态成员,在静态方法中引用this会导致编译时错误;实例方法对类的某个给定的实例进行操作,在其中可以访问类的静态成员和实例成员,而且可以用this来访问该实例。
静态方法和实例方法的调用形式有所不同。当以E.M形式来调用一个方法时,如果M是静态方法,则E必须表示含有M的一个类型;如果M是实例方法,则E必须表示含有M的类型的一个实例。
下面的示例说明了访问静态和实例成员的规则:
class Test { int x; // 实例字段 static int y; // 静态字段 void F() { // 实例方法,可访问实例成员和静态成员 x = 1; // 正确,等效于this.x = 1(访问实例字段) y = 1; // 正确,等效于Test.y = 1(访问静态字段) } static void G() { // 静态方法,可访问静态成员,不能访问实例成员 x = 1; // 错误,在静态方法中不能访问实例成员this.x y = 1; // 正确,等效于Test.y = 1(访问静态字段) } static void Main() { Test t = new Test(); // 创建类实例 t.x = 1; // 正确,访问实例成员 t.y = 1; // 错误,不能通过实例访问静态成员 Test.x = 1; // 错误,不能通过类访问实例成员 Test.y = 1; // 正确,通过类访问静态成员 } }
F方法表明在实例方法成员中通过变量名可以访问实例成员及静态成员;G方法表明在静态函数成员中通过变量名访问实例成员会导致编译时错误;Main方法表明在成员访问中实例成员必须通过实例访问,静态成员必须通过类型访问。
【例4-21】创建一个C#控制台应用程序,用于说明如何定义和调用静态方法,程序运行结果如图4-33所示。
图4-33 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-21”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在命名空间ConsoleApplication4_21中定义一个公共类Student,代码如下:
public class Student { public string id; // 公共字段 public string name; // 公共字段 public Student(string name, string id) // 带参数实例构造函数 { this.name = name; this.id = id; } public static int studentCounter; // 静态字段 public static int AddStuent() // 静态方法 { return ++studentCounter; // 使静态字段值加1并返回该值 } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "静态方法应用示例"; Console.Write("请输入姓名:"); string name = Console.ReadLine(); Console.Write("请输入学号:"); string id = Console.ReadLine(); Student st = new Student(name, id); // 创建类实例 Console.Write("请输入当前学生人数:"); string n = Console.ReadLine(); Student.studentCounter = Int32.Parse(n); // 设置静态字段的值 Student.AddStuent(); // 调用静态方法 Console.WriteLine(); Console.WriteLine("姓名:{0}", st.name); Console.WriteLine("学号:{0}", st.id); Console.WriteLine("新的学生人数:{0}\n", Student.studentCounter); // 访问静态字段
(4)按Ctrl+F5组合键编译并运行程序。
4.6.5 虚方法和重写方法
如果一个实例方法的声明中含有virtual修饰符,则使该方法可以在派生类中被重写,称为“虚方法”;否则称为“非虚方法”。
虚方法与非虚方法有如下不同。
(1)非虚方法的实现是一成不变的,无论该方法是在声明其类的实例,还是在派生类的实例中调用,方法实现都是相同的。虚方法的实现可以由派生类来取代,取代所继承虚方法的实现过程称为“重写该方法”。
(2)在虚方法调用中相关实例的运行时类型决定要调用的实现方法,在非虚方法调用中相关实例的编译时类型是决定性因素。
如果在一个实例方法声明中包含override修饰符,则该方法称为“重写方法”,由override声明重写的方法称为“已重写的基方法”。重写方法用相同的签名重写所继承的虚方法。虚方法声明用于引入新方法;而重写方法声明则通过为所继承的虚方法提供新的实现,以便对该方法进行扩展和修改。
对于在类C中声明的重写方法M,已重写的基方法通过检查C的各个基类类型来确定。该检查过程从C的直接基类类型开始检查,然后依次检查每个后续的直接基类类型,直到在给定的基类类型中至少找到一个在用类型实参替换后与M具有相同签名的可访问方法。
声明重写方法时应注意以下几点。
(1)能按照上述规则找到一个已重写的基方法。
(2)已重写的基方法是一个虚的、抽象或重写方法,而不能是静态或非虚方法。
(3)已重写的基方法不是密封方法。
(4)重写方法和已重写的基方法具有相同的返回类型及可访问性。
【例4-22】创建一个C#控制台应用程序,用于说明虚方法和非虚方法之间的区别,程序运行结果如图4-34所示。
图4-34 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-22”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_22中定义虚方法G的内部类A。并以A为基类声明派生类B,在B中重写虚方法实现,代码如下:
class A { public void F() // 非虚方法 { Console.WriteLine("调用A.F"); } public virtual void G() // 虚方法 { Console.WriteLine("调用A.G"); } } class B : A { // 用new修饰符可以显式隐藏从基类继承的F,F的派生版本将替换基类版本 new public void F() { Console.WriteLine("调用B.F"); } // 使用override修饰符扩展或修改继承的方法的虚实现 public override void G() { Console.WriteLine("调用B.G"); } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "虚方法与非虚方法的区别示例"; B b = new B(); // b的编译时类型和运行时类型均为B A a = b; // a的编译时类型为A,运行时类型为B Console.WriteLine("a的运行时类型:{0}", a.GetType()); Console.WriteLine("b的运行时类型:{0}\n", b.GetType()); Console.Write("a.F():"); a.F(); // F为非虚方法,调用A.F Console.Write("b.F():"); b.F(); // F为非虚方法,调用B.F Console.Write("a.G():"); a.G(); // G为虚方法,a的运行时类型为B,故调用B.F Console.Write("b.G():"); b.G(); // G为虚方法,b的运行时类型为B,故调用B.F Console.WriteLine();
(4)按Ctrl+F5组合键编译并运行程序。
4.6.7 密封方法
如果在实例方法声明中使用sealed修饰符,则可以防止派生类进一步重写该方法,该方法称为“密封方法”。当实例方法声明包含sealed修饰符时,同时也必须使用override修饰符。
在重写基类虚方法的派生类上,可以将该虚方法声明为密封方法。在用于以后的派生类时,将取消该方法的虚效果,从而防止派生类重写该方法。
在下面的示例中,首先声明类A。然后以A为基类声明派生类B,接着以B为基类声明派生类C。声明类B时,为其提供了两个重写方法,一个是带有sealed修饰符的F方法;另一个是没有sealed修饰符的G方法。通过使用sealed修饰符,B可以防止C进一步重写F:
using System; class A // 声明类A { public virtual void F() // 虚方法 { Console.WriteLine("A.F"); } public virtual void G() // 虚方法 { Console.WriteLine("A.G"); } } class B : A // 以A为基类,声明派生类B { sealed override public void F() // 密封方法,不可重写 { Console.WriteLine("B.F"); } override public void G() // 重写方法,可重写 { Console.WriteLine("B.G"); } } class C : B // 以B为基类,声明派生类C { override public void F() // 错误,重写密封方法F,将会导致编译错误 { Console.WriteLine("C.F"); } override public void G() // 正确,重写非密封方法G { Console.WriteLine("C.G"); } }
4.6.8 抽象方法
当实例方法声明包含abstract修饰符时称为抽象方法。
抽象方法具有以下特性。
(1)是隐式的虚方法。
(2)只允许在抽象类中使用抽象方法声明。
(3)声明引入了一个新的虚方法,但不提供实际的实现,所以没有方法体。声明只是以一个分号结束,并且在签名后没有大括号({ })。例如:
public abstract void MyMethod();
(4)其实现由一个重写方法提供,此重写方法是非抽象类的一个成员。
(5)在其声明中使用static或virtual修饰符是错误的。
(6)不能通过base关键字来引用。
(7)在其声明中可以重写虚方法,使得一个抽象类可以强制从其派生类重新实现该方法,并使该方法的原始实现不再可用。
在下面的示例中,Shape类定义了一个可以绘制自身的几何形状对象的抽象概念。其Paint方法是抽象的,这是因为没有有意义的默认实现。Ellipse和Box类是具体的Shape实现,由于这些类是非抽象的,因此要求它们重写Paint方法并提供实际的实现:
public abstract class Shape // 抽象类 { // Graphics类为密封类,位于命名空间System.Drawing中,用于封装一个GDI+绘图图面 // Rectangle结构,位于命名空间System.Drawing中,用于存储4个整数,表示一个矩形的位置和大小 public abstract void Paint(Graphics g, Rectangle r); // 抽象方法 } public class Ellipse : Shape // 非抽象类Ellipse { public override void Paint(Graphics g, Rectangle r) // 重写抽象方法Paint { g.DrawEllipse(r); } } public class Box : Shape // 非抽象类Box { public override void Paint(Graphics g, Rectangle r) // 重写抽象方法Paint { g.DrawRect(r); } }
在下面的示例中为抽象类A提供了一个抽象方法F,在非抽象类B中试图通过base.F()来调用抽象方法,将会导致编译时错误:
abstract class A // 抽象类 { public abstract void F(); // 抽象方法 } class B : A // 非抽象类 { public override void F() // 重写抽象方法 { base.F(); // 错误,base.F为抽象方法 } }
在下面的示例中类A声明一个虚方法,抽象类B用一个抽象方法重写此方法,而非抽象类C重写该抽象方法以提供其自己的实现:
using System; class A // 声明类A { public virtual void F() // 虚方法 { Console.WriteLine("A.F"); } } abstract class B : A // 声明A的派生类B { public abstract override void F(); // 用抽象方法重写虚方法 } class C : B // 声明B的派生类C { public override void F() // 重写方法F { Console.WriteLine("C.F"); } }
4.6.9 外部方法
当方法声明包含extern修饰符时为外部方法,它是在外部实现的,编程语言通常使用C#以外的语言。由于外部方法声明不提供任何实际实现,因此其方法体只由一个分号组成。外部方法不可以是泛型。
extern修饰符通常与DllImport属性一起使用,从而使外部方法可以由DLL动态链接库实现。执行环境可以支持其他用来提供外部方法实现的机制。当外部方法包含DllImport属性时,该方法声明必须同时包含一个static修饰符。
下面的示例说明如何使用extern修饰符和DllImport属性:
using System.Text; using System.Security.Permissions; using System.Runtime.InteropServices; class Path { [DllImport("kernel32", SetLastError=true)] static extern bool CreateDirectory(string name, SecurityAttribute sa); [DllImport("kernel32", SetLastError=true)] static extern bool RemoveDirectory(string name); [DllImport("kernel32", SetLastError=true)] static extern int GetCurrentDirectory(int bufSize, StringBuilder buf); [DllImport("kernel32", SetLastError=true)] static extern bool SetCurrentDirectory(string name); }
【例4-23】创建一个C#控制台应用程序,使用从User32.dll库导入的MessageBox方法接收来自用户的字符串并将其显示在消息框中,程序运行结果如图4-35所示。
图4-35 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-23”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Progmam.cs,在命名空间ConsoleApplication4_23声明上方添加以下using指令:
using System.Runtime.InteropServices;
(3)在Program类的Main方法上方,用DllImport导入User32.dll库的MessageBox方法:
[DllImport("User32.dll")] public static extern int MessageBox(int h, string m, string c, int type);
(4)将Main方法的返回类型从void更改为int,并为该方法编写以下代码:
static int Main(string[] args) { Console.Title = "外部方法应用示例"; string userName; Console.Write("请输入用户名:"); userName = Console.ReadLine(); Console.WriteLine(); return MessageBox(0, userName + "用户,您好!", "消息框", 0); }
(5)按Ctrl+F5组合键编译并运行程序,当出现提示信息时输入用户名,按Enter键后会弹出一个消息框显示欢迎信息。
4.6.10 扩展方法
当方法的第1个形参包含this修饰符时,可以指定其作用于哪种类型。该方法称为“扩展方法”,只能在非泛型及非嵌套静态类中声明。扩展方法的第1个形参不能带有除this之外的其他修饰符,而且形参类型不能是指针类型。扩展方法被定义为静态方法,但其通过实例方法语法调用。仅当使用using指令将命名空间显式导入到源代码中之后,扩展方法才位于范围中。通过创建扩展方法可以在现有类型中添加方法,而不必创建新的派生类型、重新编译或以其他方式修改原始类型。
下面通过一个例子说明如何定义和调用扩展方法。
【例4-24】创建一个C#控制台应用程序,说明如何为String类创建一个扩展方法。通过调用该方法可以获取字符串中包含的单词数目,程序运行结果如图4-36所示。
图4-36 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-24”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,声明一个命名空间ExtensionMethods,并在该命名空间中声明一个静态类MyExtensions。该类包含一个扩展方法,代码如下:
namespace ExtensionMethods // 声明命名空间 { public static class MyExtensions // 用static修饰符声明静态类 { public static int WordCount(this String str) // 声明扩展方法,该方法用于扩展System.String类型 { // 调用String.Split方法方法返回的字符串数组包含此字符串中的子字符串(由指定Unicode字符数组元素分隔) // 第1个参数指定用于分隔此字符串中子字符串的Unicode字符数组 // 第2个参数指定RemoveEmptyEntries以省略返回的数组中的空数组元素 return str.Split(new char[] { ' ','.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length; } } }
(3)在Program类声明上方添加以下using指令:
using ExtensionMethods;
(4)在Program类的Main方法中编写以下代码:
Console.Title = "扩展方法应用示例"; string str = "The quick brown fox jumped over the lazy dog."; // 用实例方法语法来调用扩展方法,但不要指定第1个参数 nt i = str.WordCount(); Console.WriteLine("英文句子为 \"{0}\"。\n", str); onsole.WriteLine("这个句子中的单词个数为 {0}。\n", i);
(5)按Ctrl+F5组合键编译并运行程序。
4.7 嵌套类
在类或结构声明中除了可以声明常量、字段和方法等成员外,也可以声明类型,声明的类型称为“嵌套类型”;在编译单元或命名空间内声明的类型则称为“非嵌套类型”。
4.7.1 声明嵌套类
非嵌套类和嵌套类均使用class关键字来声明,在下面的示例中类Nested是嵌套类型。也称为“内部类型”,这是因为它在类Container内声明;而类Container在编译单元内声明,因此是非嵌套类型,也称为“外部类型”或“包含类型”:
class Container{ class Nested{}}
无论外部类型是类,还是结构,嵌套类型均默认为private,但是可以设置为public、protected internal、protected、internal或private。在上面的示例中,嵌套类Nested默认为private。因此它对外部类型是不可访问的,但可以将其设置为public:
class Container { public class Nested { } }
嵌套类型可以访问包含类型,也可以访问包含类型的私有成员和受保护成员,包括所有继承的私有成员或受保护成员。若要访问包含类型,可将其作为构造函数传递给嵌套类型。例如:
public class Container { public class Nested { private Container m parent; public Nested() { } public Nested(Container parent) { m parent = parent; } } }
若在非嵌套类Container内部声明一个嵌套类Nested,则类Nested的完整名称为“Container.Nested”。当在Container外部创建嵌套类Nested的新实例时,应当使用这个完整名称,例如:
Container.Nested nest = new Container.Nested();
【例4-25】创建一个C#控制台应用程序,用于说明如何声明嵌套类并在其中访问包含类及其成员,程序运行结果如图4-37所示。
图4-37 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-25”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_25中声明一个名为“Container”的类并在其内部声明一个名为“Nested”的嵌套类,代码如下:
public class Container { private string msg = "包含类(外部类)"; public class Nested { private Container m parent; // 在嵌套类中访问包含类 private string msg = "嵌套类(内部类)"; public Nested() { } public Nested(Container parent) // 以包含类作为构造函数形参的类型 { m parent = parent; } public void ShowMsg() { Console.WriteLine(m parent.msg + "\n"); Console.WriteLine(this.msg + "\n"); } } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "嵌套类应用示例"; Container c = new Container(); // 创建包含类的实例 Container.Nested n =new Container.Nested(c); // 创建嵌套类的实例 n.ShowMsg(); // 调用嵌套类的方法 Console.WriteLine(n.GetType() + "\n");
(4)按Ctrl+F5组合键编译并运行程序。
4.7.2 隐藏嵌套类
声明派生类时要隐藏继承的成员,可使用相同名称在派生类中声明并使用new修饰符修饰该成员。隐藏继承的成员时,该成员的派生版本将替换基类版本。虽然可以在不使用new修饰符的情况下隐藏成员,但会生成警告;如果使用new显式隐藏成员,则会取消此警告,并记录要替换为派生版本的这一事实。
对同一成员同时使用new和override是错误的做法,因为这两个修饰符的含义互斥。new修饰符会用同样的名称创建一个新成员并使原始成员变为隐藏的;override修饰符则会扩展继承成员的实现。在不隐藏继承成员的声明中使用new修饰符将会生成警告。
使用new修饰符不仅可以显式隐藏从基类继承的成员,也可以显式隐藏基类中的同名嵌套类,并消除可能生成的警告消息。
【例4-26】创建一个C#控制台应用程序,用于说明如何使用new修饰符来隐藏基类中的同名嵌套类,程序运行结果如图4-38所示。
图4-38 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-26”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_26中声明一个名为“BaseC”的类并在其内部声明一个名为“NestedC”的嵌套类。然后以BaseC作为基类声明一个名为“DerivedC”的派生类并在其内部声明一个名为“NestedC”的嵌套类,为该嵌套类使用new修饰符以隐藏基类中的同名嵌套类,代码如下:
// 声明BaseC类 public class BaseC { public class NestedC { public int x = 200; public int y; } } // 声明类DerivedC,该类从基类BaseC继承 public class DerivedC : BaseC { // 声明嵌套类,使用new修饰符来隐藏基类中的同名嵌套类 new public class NestedC { public int x = 300; public int y; public int z; } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "隐藏基类中的嵌套类示例"; BaseC.NestedC c1 = new BaseC.NestedC(); DerivedC.NestedC c2 = new DerivedC.NestedC(); Console.WriteLine(c1.x+"\n"); Console.WriteLine(c2.x + "\n");
(4)按Ctrl+F5组合键编译并运行程序。
4.8 分部类与分部方法
前面介绍面向对象编程时总是把一个类的定义放置在一个源文件中,实际上在大型应用项目开发中也可以把一个类分布于多个独立的源文件中。每个源文件可以包含类或方法定义的一部分,这样的类称为“分部类”,这样的方法称为“分部方法”。使用分部类的好处是可以让多个开发人员同时处理该类,编译应用程序时将组合所有部分。
4.8.1 分部类
在以下两种情况下,通常需要使用分部类来拆分类定义。
(1)处理大型项目时,使一个类分布于多个独立文件中可以让多个开发人员同时处理该类。
(2)使用自动生成的源时,不必重新创建源文件即可将代码添加到类中,Visual Studio在创建Windows窗体及Web服务包装代码等时都使用此方法。不需要修改Visual Studio创建的文件,即可创建使用这些类的代码。
若要拆分类定义,可使用partial关键字修饰符,该关键字指示可以在命名空间中定义该类、结构或接口的其他部分。所有部分都必须使用partial关键字,在编译时各个部分都必须可以用来形成最终的类型并且必须具有相同的可访问性,例如public及private等。
如果将任意部分声明为抽象的,则整个类型都被视为抽象的;如果将任意部分声明为密封的,则整个类型都被视为密封的;如果任意部分声明基类型,则整个类型都将继承该类。
指定基类的所有部分必须一致,但忽略基类的部分仍继承该基类型。各个部分可以指定不同的基接口,最终类型将实现所有分部声明所列出的全部接口,在某一分部定义中声明的任何类、结构或接口成员可供所有其他部分使用。最终类型是所有部分在编译时的组合,将从所有分部类型定义中合并XML注释、接口、泛型类型参数属性、类属性,以及成员等。
需要注意的是,不能把partial修饰符用于委托或枚举声明中。
在下面的示例中,使用partial修饰符把Student类的定义拆分成两个部分,该类具有studentID和studentName两个字段,以及GoToStudy和GoToLunch两个方法:
public partial class Student { private string studentID; public void GoToStudy() { } } public partial class Student { private string studentName; public void GoToLunch() { } }
编译时将合并分部类型定义的字段和方法成员,上述Student类的定义等效于以下声明:
public class Student { private string studentID; private string studentName; public void GoToStudy() { } public void GoToLunch() { } }
在下面的示例中,嵌套类Nested是分部的,虽然包含类Container本身并不是分部的:
class Container { partial class Nested { void Test() { } } partial class Nested { void Test2() { } } }
编译时将合并分部类定义的属性,例如:
[System.SerializableAttribute] partial class Moon { } [System.ObsoleteAttribute] partial class Moon { }
上述类定义等效于以下声明:
[System.SerializableAttribute] [System.ObsoleteAttribute] class Moon { }
编译时将合并分部类继承的接口,例如:
partial class Earth : Planet, IRotate { } partial class Earth : IRevolve { }
上述类定义等效于以下声明:
class Earth : Planet, IRotate, IRevolve { }
处理分部类定义时,应当遵循以下规则。
(1)作为同一类型的各个部分的所有分部类型定义都必须使用partial修饰,例如,下面的类声明将生成错误:
public partial class A { } public class A { } // 错误,也必须使用partial修饰符
(2)partial修饰符只能出现在紧靠关键字class、struct或interface前面。
(3)分部类型定义中允许使用嵌套的分部类型,例如:
partial class ClassWithNestedClass { partial class NestedClass { } } partial class ClassWithNestedClass { partial class NestedClass { } }
(4)必须在同一程序集和同一模块(.exe或 .dll文件)中定义同一类型的各个部分的所有分部类型,分部定义不能跨越多个模块。
(5)类名和泛型类型参数在所有的分部类型定义中都必须匹配,泛型类型可以是分部的,每个分部声明都必须以相同的顺序使用相同的参数名。
(6)用于分部类型定义中的关键字(public、private、protected、internal、abstract和sealed)、基类、new修饰符(嵌套部分),以及泛型约束都是可选的,但是如果某个关键字出现在一个分部类型定义中,则该关键字不能与在同一类型的其他分部定义中指定的关键字冲突。
【例4-27】创建一个C#控制台应用程序,说明如何把一个类定义拆分到多个源文件中用于计算圆形的周长和面积,程序运行结果如图4-39所示。
图4-39 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-27”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_27中声明一个名为“Circle”的分部类,代码如下:
public partial class Circle { private double radius; public Circle(double r) { radius = r; } public double Perimeter() { return 2 * radius * Math.PI; } }
(3)在当前项目中添加一个类,源文件名为“Circle.cs”,在命名空间ConsoleApplication4_27中声明以下分部类:
namespace ConsoleApplication4 26 { public partial class Circle { public double Area() { return radius * radius * Math.PI; } } }
(4)切换到源文件Program.cs所在代码编辑器窗口,在Main方法中编写以下代码:
Console.Title = "分部类应用示例"; Circle c = new Circle(10); Console.WriteLine("圆周长 = {0}\n",c.Perimeter()); Console.WriteLine("圆面积 = {0}\n", c.Area());
(5)按Ctrl+F5组合键编译并运行程序。
4.8.2 分部方法
分部类或结构可以包含分部方法,类的一个部分包含方法的签名,可以在同一部分或另一个部分中定义可选实现。如果未提供该实现,则会在编译时移除方法及对该方法的所有调用。
分部方法使类的某个部分的实施者能够定义方法(类似于事件),类的另一部分的实施者可以决定是否实现该方法。如果未实现该方法,编译器将移除方法签名及对该方法的所有调用。因此分部类中的任何代码都可以随意地使用分部方法,即使未提供实现也是如此。如果调用了未实现的方法,将不会导致编译时错误或运行时错误。
在自定义生成的代码时,分部方法特别有用。这些方法允许保留方法名称和签名,因此生成的代码可以调用方法,而开发人员可以决定是否实现方法。与分部类非常类似,分部方法使代码生成器创建的代码和开发人员创建的代码能够协同工作,而不会产生运行时开销。
分部方法声明由定义和实现两个部分组成,它们可以位于分部类的不同或同一部分中。该方法在分部类的一个部分中定义其签名,并在该类的另外一个部分中定义其实现。类设计人员可以使用分部方法提供由开发人员决定是否与实现的方法挂钩(类似于事件处理程序)。如果开发人员没有提供实现,则编译器会在编译时移除签名,并优化定义声明和对方法的所有调用。
在下面的示例中,通过两个源文件file1.cs和file2.cs分别提供了分部方法onNameChanged的定义和实现代码:
// 在源文件file1.cs中给出分部方法定义 partial void onNameChanged(); // 在源文件file2.cs中给出分部方法实现 partial void onNameChanged() { // 方法体 }
声明分部方法时,应当注意以下几点。
(1)必须以上下文关键字partial开头,并且方法必须返回void。
(2)两个部分中的签名必须匹配。
(3)可以有ref参数,但不能有out参数。
(4)为隐式private方法,不能为virtual方法。
(5)不能为extern方法,因为主体的存在确定了方法是在定义,还是在实现。
(6)可以有static和unsafe修饰符。
(7)可以为泛型的,约束将放在定义分部方法声明上,也可以选择重复放在实现声明上。参数和类型参数名称在实现声明和定义声明中不必相同。
(8)不能具有访问修饰符或virtual、abstract、override、new及sealed修饰符,也不允许使用属性。
(9)不能将委托转换为分部方法。
【例4-28】创建一个C#控制台应用程序,说明如何在分部类的两个部分中定义一个分部方法,程序运行结果如图4-40所示。
图4-40 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-28”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_28中定义一个名为“Student”的分部类,代码如下:
public partial class Student { private string studentID; private string studentName; public Student(string id, string name) { studentID = id; studentName = name; this.OnCreated(); // 调用分部方法OnCreated } public void ShowMsg() { Console.WriteLine("学号:{0};姓名:{1}\n", studentID, studentName); } partial void OnCreated(); // 分部方法OnCreated的定义 }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "分部方法应用示例"; string id; string name; Console.Write("输入学号:"); id = Console.ReadLine(); Console.Write("输入姓名:"); name = Console.ReadLine(); Student st = new Student(id, name); st.ShowMsg();
(4)在当前项目中中添加一个类,源文件名为“Student.cs”。在命名空间ConsoleApplication4_28中声明分部类Student并提供分部方法OnCreated的实现,代码如下:
namespace ConsoleApplication4 27 { public partial class Student { partial void OnCreated() // 分部方法OnCreated的实现 { Console.WriteLine("Student类实例创建成功!\n"); } } }
(5)按Ctrl+F5组合键编译并运行程序。
4.9 静态类与静态成员
静态类和类成员用于创建不必创建类的实例即访问的数据和函数,静态类成员可用于分离独立于任何对象标识的数据和行为。无论对象发生什么更改,这些数据和函数都不会随之变化。当类中没有依赖对象标识的数据或行为时,即可使用静态类。
4.9.1 静态类
静态类使用static关键字来声明,以指示其仅包含静态成员,不能使用new关键字创建静态类的实例。静态类在加载包含该类的程序或命名空间时由 .NET Framework公共语言运行库(CLR)自动加载。
通常使用静态类来包含不与特定对象关联的方法,如创建一组不操作实例数据并且不与代码中的特定对象关联的方法是很常见的要求,在这种场合可以使用静态类来包含这些方法。
静态类的主要特点如下。
(1)仅包含静态成员。
(2)不能被实例化。
(3)是密封的。
(4)不能包含实例构造函数。
创建静态类与创建仅包含静态成员和私有构造函数的类基本相同,私有构造函数阻止类被实例化。使用静态类的优点在于编译器能够执行检查以确保不致偶然地添加实例成员,并且保证不会创建此类的实例。
静态类是密封的,因此不可被继承,它们不能从除Object外的任何类中继承。静态类不能包含实例构造函数,但是可以具有静态构造函数。
使用静态类作为不与特定对象关联方法的组织单元,此外用其不必创建对象就能调用其方法,从而可以更简单并迅速地实现所需功能。通过一种有意义的方式来组织类内部的方法很有用,如System命名空间中的Math类中包含许多用于数学计算的方法。
【例4-29】创建一个C#控制台应用程序,通过定义一个名为“CompanyInfo”的静态类来提供用于获取公司名称、地址信息和邮政编码的方法,程序运行结果如图4-41所示。
图4-41 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-29”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_29中定义一个名为“CompanyInfo”的静态类,代码如下:
static class CompanyInfo { public static string GetCompanyName() { return "ABC公司"; } public static string GetCompanyAddress() { return "中国北京"; } public static string GetPostCode() { return "100001"; } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "静态类应用示例"; Console.WriteLine("公司名称:{0}", CompanyInfo.GetCompanyName()); Console.WriteLine("公司地址:{0}", CompanyInfo.GetCompanyAddress()); Console.WriteLine("邮政编码:{0}\n", CompanyInfo.GetPostCode());
(4)按Ctrl+F5组合键编译并运行程序。
4.9.2 静态成员
静态成员可以通过在成员的类型之前使用static关键字来声明,如:
public class Employee { public static int employeeCounter; // 静态字段 public static int AddEmployee() // 静态方法 { return ++employeeCounter; } }
静态方法可以被重载,但不能被重写。
即使没有创建类的实例,也可以调用该类中的静态方法、字段、属性或事件。如果创建了该类的任何实例,不能使用实例来访问静态成员。只存在静态字段和事件的一个副本,静态方法和属性只能访问静态字段和静态事件。静态成员通常用于表示不会随对象状态而变化的数据或计算,如数学库中可能包含用于计算正弦和余弦的静态方法。
静态成员在第1次被访问之前并且在任何静态构造函数(若调用的话)之前初始化。若要访问静态类成员,应使用类名,而不是变量名来指定该成员的位置,如:
Employee.AddEmployee(); int i = Employee.employeeCounter;
【例4-30】创建一个C#控制台应用程序,通过定义一个名为“TemperatureConverter”的静态类来实现摄氏温度与华氏温度之间的相互转换,程序运行结果如图4-42所示。
图4-42 程序运行结果
【设计步骤】
(1)在解决方案chapter04中添加一个控制台应用程序项目,项目名称为“ConsoleApplication4-30”。保存在F:\Visual C sharp 2008\chapter04文件夹中,将该项目设置为启动项目。
(2)在代码编辑器中打开源文件Program.cs,在命名空间ConsoleApplication4_30中定义一个名为“TemperatureConverter”的静态类,代码如下:
public static class TemperatureConverter { public static double CelsiusToFahrenheit(string temperatureCelsius) { // 把参数转换为double类型 double celsius =Double.Parse(temperatureCelsius); // 把摄氏温度转换为华氏温度 double fahrenheit = (celsius * 9 / 5) + 32; return fahrenheit; } public static double FahrenheitToCelsius(string temperatureFahrenheit) { // 把参数转换为double类型 double fahrenheit = Double.Parse(temperatureFahrenheit); // 把华氏温度转换为摄氏温度 double celsius = (fahrenheit -32) * 5 / 9; return celsius; } }
(3)在Program类的Main方法中编写以下代码:
Console.Title = "静态成员应用示例"; Console.WriteLine(" ***温度转换实用程序***"); Console.WriteLine("--------------------------"); Console.WriteLine("1. 摄氏温度转换为华氏温度"); Console.WriteLine("2. 华氏温度转换为摄氏温度"); Console.Write("请选择:"); string selection = System.Console.ReadLine(); double F, C = 0; switch (selection) { case "1": Console.Write("请输入摄氏温度: "); F = TemperatureConverter.CelsiusToFahrenheit(Console.ReadLine()); Console.WriteLine("华氏温度为:{0:F2}\n", F); break; case "2": Console.Write("请输入华氏温度: "); C = TemperatureConverter.FahrenheitToCelsius(Console.ReadLine()); Console.WriteLine("摄氏温度为:{0:F2}\n", C); break; default: Console.WriteLine("请选择一种转换方式。"); break; }
(4)按Ctrl+F5组合键编译并运行程序。