2026/5/21 18:10:15
网站建设
项目流程
只做早餐的网站,济南网站建设咨 询小七,如何绑定域名wordpress,南宁网站建设在第9章中学习了面向对象编程的核心概念之一#xff1a;类。C#是一种面向对象的语言#xff0c;从本章开始#xff0c;我们将逐个学习面向对象编程的基本特征#xff0c;本章要从继承性开始讨论。继承机制可以提高软件模块的可复用性和可扩展性#xff0c;以提高软件开发效…在第9章中学习了面向对象编程的核心概念之一类。C#是一种面向对象的语言从本章开始我们将逐个学习面向对象编程的基本特征本章要从继承性开始讨论。继承机制可以提高软件模块的可复用性和可扩展性以提高软件开发效率。10.1 类的继承C#和C不同可以从一个类继承或实现多个接口但不可以从多个类继承。新定义的派生类的实例可以继承已有的基类的特征和能力。如果从其他的类继承语法也很简单只需在声明类时在类名后加一个冒号然后在冒号后指定要继承的类即可如代码清单10-1所示。代码清单10-1 类的继承语法下面我们解释一下与继承性相关的两个概念❑基类被继承的类又称做父类。实际上基类和派生类也只是一个相对的概念Automobile汽车在此处是基类但它同时也是一个派生类因为任何类都继承自Object类都是Object类型的派生类型。❑派生类继承自基类的类又称做子类。派生类不但有自身的成员还包含了基类的成员。我们还是以奔驰和汽车的关系举例首先奔驰汽车是汽车的一种如果不把汽车作为基类抽象出来那么势必会造成奔驰汽车、宝马汽车、奇瑞汽车等具有重复的性质既造成无谓的冗余又不利于数据复用及统一管理。因此可以先定义一个代表汽车的基类AutomobileAutomobile类作为基类体现了“汽车”这个实体具有的公共性质这里只拿鸣笛和行驶举例对应两个方法Beep和Run然后定义代表奔驰汽车的派生类Benz它派生自汽车类如代码清单10-2所示。class Automobile { //鸣喇叭 public void Beep{} //行驶 public void Run{} } class BenzAutomobile{}基类和派生类的关系可以使用UML进行表示如图10-1所示。这里虽然我们并没有为Benz类定义任何成员方法但实际上Benz类已经继承了它的基类Automobile类的所有非私有成员私有成员只有Automobile类自身可以访问这点可以通过图10-2进行验证。由图10-2可见我们虽然没有为Benz类定义任何成员但仍然可以使用其基类的非私有成员如Beep方法和Run方法除了这两个从Automobile类继承过来的方法以外还有从Object类型继承的方法成员下面对IntelliSense列出的方法逐个进行说明此处仅为说明故忽略了各方法的参数如表10-1所示。10.2 访问继承的成员派生类内部可以访问基类的所有非私有成员私有成员只有类自身才可访问就像这些成员是派生类自身的一样。下面以“人-学生”为例进行说明。当然这里“人”为基类“学生”派生自“人”是派生类。在“人”类型中我们定义了所有人所共有的一些数据成员如姓名、年龄而“学生”类型定义了自己独有的学校数据在派生类“学生”中可以自由访问基类的姓名、年龄等成员如代码清单10-3所示。代码清单10-3 从派生类访问基类的非私有成员using System; namespace ProgrammingCSharp4 { class Person { public string Name; public int Age; public Person(string name, int age) { Name name; Age age; Console.WriteLine({0}{1}, Name, Age); } } class Student : Person { public string School; public Student(string name, int age, string school) : base(name, age) { School school; Console.WriteLine({0}{1}{2}, Name, Age, School); } } class ClassExample { public static void Main() { Student student new Student(大雄, 12, 一中); Console.WriteLine(student.Name); Console.WriteLine(student.Age); Console.WriteLine(student.School); } } }程序运行结果如下大雄12大雄12一中大雄12一中对代码清单10-14的说明如表10-2所示。10.3 使用new修饰符隐藏基类的成员我们在第6章学习过new运算符这里讲的是它的另外一个用途在这里new不再是运算符而是修饰符。new修饰符的作用是显式地隐藏从基类继承的成员。你并非一定这样做不用它也可以达到目的但会引发一个编译器警告同样地如果你没有隐藏任何基类成员时就不应使用new修饰符那样也会引发一个编译器警告。那么如何隐藏基类的成员呢❑若要隐藏继承的数据成员例如字段需要在派生类中声明一个相同名称的成员并使用new修饰符修饰该成员注意这里只需名称相同即可而不管类型是否相同❑若要隐藏继承的方法成员需要在派生类中声明一个具有相同签名的方法注意签名不包括返回值并使用new修饰符修饰该方法❑基类的静态成员也可以被隐藏方法同上。下面我们看一个例子如代码清单10-4所示。代码清单10-4 隐藏基类的成员using System; namespace ProgrammingCSharp4 { class BaseClass { public string FieldA 我是基类; public static string StaticFieldB 我是基类的静态字段; public void SayHi() { Console.WriteLine(Hi我是基类); } } class ChildClass : BaseClass { new public int FieldA 100; new public static string StaticFieldB 我是派生类的静态字段; new public string SayHi() { Console.WriteLine(Hi我是派生类); return null; } } class ClassExample { public static void Main() { ChildClass child new ChildClass(); Console.WriteLine(child.FieldA); Console.WriteLine(ChildClass.StaticFieldB); child.SayHi(); } } }对代码清单10-14的说明如表10-3所示。程序运行结果如下100我是派生类的静态字段Hi我是派生类如果我们去掉代码清单10-4中第17行的new修饰符程序仍然可以编译并能正确运行但编译器会引发一个警告信息如下ProgrammingCSharp4.ChildClass.FieldA隐藏了继承的成员Programming CSharp4.BaseClass.FieldA如果是有意隐藏请使用关键字new10.4 访问基类的成员访问类的当前实例成员使用的是this关键字访问基类的成员则使用base关键字。base的用法类似于this在base后使用点运算符即可访问基类成员。base关键字还有另外一个用途就是可以调用基类的构造函数。下面我们看一个例子如代码清单10-5所示。代码清单10-5 访问基类的成员using System; namespace ProgrammingCSharp4 { class BaseClass { public string FieldA 我在基类中; } class ChildClass : BaseClass { //隐藏了基类中的FieldA成员 new public string FieldA 我在派生类中; public void Print() { Console.WriteLine(FieldA); Console.WriteLine(base.FieldA); } } class ClassExample { public static void Main() { ChildClass child new ChildClass(); child.Print(); } } }上述代码输出如下我在派生类中我在基类中对代码清单10-14的说明如表10-4所示。关于base关键字引用基类的构造函数的知识我们将在10.5节进行介绍。10.5 类的初始化顺序类的初始化顺序是1首先初始化类的实例字段2其次调用基类的构造函数没有明确的基类则调用System.Object的构造函数3最后调用类自己的构造函数。如图10-3所示。下面我们通过一个实例来了解这种初始化顺序如代码清单10-6所示。代码清单10-6 类的初始化顺序using System; namespace ProgrammingCSharp4 { class BaseClass { public BaseClass() { Console.WriteLine(基类构造函数被调用); } } class ChildClass : BaseClass { public bool FieldA true; public bool FieldB; public ChildClass() { Console.WriteLine(自己的构造函数被调用); } public void Print() { Console.WriteLine(FieldA); Console.WriteLine(FieldB); } } class ClassExample { public static void Main() { ChildClass child new ChildClass(); child.Print(); } } }我们在第33行初始化了类ChildClass它的基类是BaseClass那么它的初始化顺序如表10-5所示。代码清单10-6 的运行结果为基类构造函数被调用自己的构造函数被调用TrueFalse10.6 在派生类中指定基类构造函数我们刚讲过了类的初始化顺序在类初始化的第二个阶段就是调用基类的构造函数的阶段默认调用的是基类的无参数的构造函数因为构造函数可以重载基类除了具有无参数的构造函数外可能还有其他带参数的构造函数。因此如果我们要指定某一个基类的构造函数而不是无参数的构造函数那么就必须在构造函数初始化语句中进行显式指定。方法有两种❑使用base关键字指定基类的某个构造函数❑使用this关键字指定使用当前类的某个构造函数。我们先来看看如何在派生类中显式指定其基类的某个构造函数语法如下class SomeClass : BaseClass { public SomeClass() : base() { } public SomeClass(string str) : base(str) { } }我们通过一个实例来看看如何通过base关键字指定基类的某个构造函数。如代码清单10-7所示。using System; namespace ProgrammingCSharp4 { class BaseClass { public string FieldA 我在基类中; public BaseClass() { Console.WriteLine(基类的构造函数); } public BaseClass(string str) { Console.WriteLine(基类的构造函数参数{0}, str); } } class ChildClass : BaseClass { // 隐藏了基类中的FieldA成员 new public string FieldA 我在派生类中; public ChildClass() : base() { Console.WriteLine(派生类的构造函数); } public ChildClass(string str) : base(str) { Console.WriteLine(派生类的构造函数参数{0}, str); } public void Print() { Console.WriteLine(FieldA); Console.WriteLine(base.FieldA); } } class ClassExample { public static void Main() { ChildClass child new ChildClass(a); child.Print(); } } }上述代码运行结果如下基类的构造函数参数a派生类的构造函数参数a我在派生类中我在基类中如果我们将第30行注释掉亦即不指定基类带参数的构造函数BaseClassstring str那么运行结果如下基类的构造函数派生类的构造函数参数a我在派生类中我在基类中可见虽然我们使用的派生类带参数的构造函数对派生类进行实例化但默认情况下调用的仍是基类不带参数的构造函数。接下来我们使用第二种方法看看如何显式指定使用当前类的某个构造函数语法如下class SomeClassBaseClass { public SomeClassthissomething{} public SomeClassstring str{} }我们继续看看如何使用上述方式来显式指定当前类的某个构造函数如代码清单10-8所示。代码清单10-8 使用this关键字指定当前类构造函数using System; namespace ProgrammingCSharp4 { class BaseClass { public BaseClass() { Console.WriteLine(基类的构造函数); } public BaseClass(string str) { Console.WriteLine(基类的构造函数参数{0}, str); } } class SomeClass : BaseClass { public SomeClass() : this(something) { Console.WriteLine(调用SomeClass()构造函数); } public SomeClass(string str) : this(str, 10) { Console.WriteLine(调用SomeClass(string str)构造函数); } public SomeClass(string str, int val) { Console.WriteLine(调用SomeClass(string str, int val)构造函数); } } class ClassExample { public static void Main() { SomeClass someClass new SomeClass(something); } } }上述代码运行结果如下基类的构造函数调用SomeClassstring strint val构造函数调用SomeClassstring str构造函数对代码清单10-8的说明如表10-6所示。从上述运行结果来看虽然我们初始化类的时候使用的是SomeClassstring str构造函数因此会先调用SomeClassstring strint val构造函数其次才是SomeClassstring str构造函数。10.7 类的访问修饰符没有嵌套在其他类中的类可以是公共的也可以是内部的。声明为公共的类型可由任何其他类型访问不管是否在当前程序集内。而声明为内部的类型只能由同一程序集中的类型访问。默认情况下即不加任何访问修饰符的情况下类的声明为内部的除非向类定义添加了关键字public。类定义可以添加internal关键字使其访问级别成为内部的。要注意的是访问修饰符不影响类自身。10.8 跨程序集的继承程序集可以简单理解为类的物理组织形式而命名空间则可认为是类的逻辑组织形式。只有指定了类所在的程序集以及命名空间编译器才能“找到”该类型。一个程序集明确地说就是一个EXE或DLL。可能有人会有疑问为什么我们从来都没有向编译器指定某个特定的程序集那是因为截至目前为止我们所有的类都定义在同一个程序集中。跨程序集继承其实并不复杂只是相对于在同一个程序集中继承需要另外添加对那个程序集的引用而已。下面我们新建一个类库工程MyLibrary然后在其中新增一个类BaseClass如图10-4所示。这里的BaseClass是基类我们将在另一个程序集ConsoleApplication1中新建一个继承自它的派生类ChildClass首先要添加对MyLibrary程序集的引用如图10-5所示。然后编写ChildClass的代码如代码清单10-9所示。代码清单10-9 ChildClass的代码class ChildClassBaseClass { }此时Visual Studio还不知道这个BaseClass定义在哪里需要我们提供进一步的信息比如在哪个程序集哪个命名空间等。此时单击BaseClass左下角的代码提示可以快捷地列出目标类可能位于哪个程序集或命名空间注意如果此前没有指定BaseClass所在的程序集Visual Studio将无法获取元数据信息即不能提供被选类供你选择只能选择新建类如图10-6所示。我们虽然没有为派生类ChildClass定义任何新的成员它仍然具有基类所具有的一些非私有成员。下面我们调用它继承的基类方法如代码清单10-10所示。代码清单10-10 调用ChildClass的基类方法class ClassExample { public static void Main { ChildClass childnew ChildClass child.DoSomething } }上述代码的运行结果如下我在基类中……10.9 密封类当我们把一个类声明为密封类的时候就意味着该类无法作为基类也就是说它不可以被继承了这和第11章要讲的“抽象类必须被继承”相反。使用sealed关键字可以把类声明为密封类如代码清单10-11所示。代码清单10-11 密封类sealed class CanNotBeInheritedClass {}如果试图继承一个密封类如代码清单10-12所示。代码清单10-12 试图从密封类继承class ChildClassCanNotBeInheritedClass {}将产生一条错误信息ChildClass无法从密封类型CanNotBeInheritedClass派生意思是无法从密封类CanNotBeInheritedClass继承。10.10 静态类迄今为止我们学习了静态方法、静态字段当我们在声明类时使用static修饰符所声明的类就称做静态类。静态类具有如下特征❑静态类不能使用sealed或abstract修饰符❑静态类必须直接继承自System.Object类型不能是任何其他类的派生类❑静态类不能实现任何接口❑静态类不能包含任何操作符❑静态类不能包含使用protected或者protected internal修饰的静态成员❑静态类只能包含静态成员❑静态类可以包含静态构造函数但不能包含实例构造函数❑静态类不能被实例化❑静态类是密封的不能被继承静态类仅包含静态成员不能使用new关键字创建静态类的实例。静态类在加载包含该类的程序集时由CLR自动加载。正是因为静态类不包括实例字段因此它不可以包含与特定实例相关联的数据。在实际工作中创建一组不操作实例数据并且不与代码中的特定对象关联的方法是很常见的需求。.NET FRAMEWORK类库中的System.Math就是一个静态类其提供了三角函数、对数函数和其他通用数学函数的常数如π和静态方法。下面来看一个实例这是一个摄氏度/华氏度转换的工具类功能就是提供华氏度到摄氏度的转换以及相反过程。如代码清单10-13所示。代码清单10-13 摄氏度/华氏度转换工具类/// summary /// 温度转换 /// /summary public static class TemperatureConverterUtils { /// summary /// 摄氏度转成华氏度 /// /summary /// param nametemperatureCelsius/param /// returns/returns public static double CelsiusToFahrenheit(string temperatureCelsius) { double celsius Double.Parse(temperatureCelsius); //转换摄氏度到华氏度 double fahrenheit (celsius * 9 / 5) 32; return fahrenheit; } /// summary /// 华氏度转摄氏度 /// /summary /// param nametemperatureFahrenheit/param /// returns/returns public static double FahrenheitToCelsius(string temperatureFahrenheit) { double fahrenheit Double.Parse(temperatureFahrenheit); //华氏度到摄氏度转换 double celsius (fahrenheit - 32) * 5 / 9; return celsius; } }我们再从IL的角度观察这段代码编译后的结果可以看出编译器将类标记为abstract和sealed并且没有实例构造函数即IL中没有.ctor方法如图10-7所示。10.11 扩展方法当我们需要对已有的类添加新的功能时当然可以选择从已有的类继承然后在派生类中加入新的方法我们还有另外一个选择那就是使用扩展方法扩展方法是这样一种方法❑扩展方法是一种特殊的静态方法它必须定义于静态类中❑扩展方法的第一个参数以this修饰符为前缀后跟要扩展的目标类型及其形参❑扩展方法所在的类必须在使用它的类可见范围内否则需使用using指令将命名空间显式导入到当前源代码中❑扩展方法只能针对实例调用也就是说目标类不能为静态类❑如果扩展方法和被扩展类中某个方法签名相同则扩展方法永远都不会被调用❑其他命名空间下的扩展方法的优先级低于当前命名空间下的扩展方法的优先级优先级最高为实例方法。通过扩展方法我们就可以在实现不修改类型的情况下对一个类型进行功能上的扩展而且新的扩展方法可以在其他类的对象上像调用实例方法那样进行调用。下面我们看一个示例我们为string类型添加了计算英文单词数的方法WordCount使得为string类型扩展功能比较容易并且让计算一个字符串值中的英文单词数更加直观如代码清单10-14所示。代码清单10-14 扩展方法using System; namespace ProgrammingCSharp4 { public static class MyExt { /// summary /// 扩展方法增加计算一个字符串中包含单词数的功能 /// /summary public static int WordCount(this string str) { return str.Split(new char[]{ , ., ?}, StringSplitOptions.RemoveEmptyEntries).Length; } /// summary /// 此扩展方法不会被执行因为它和string类型的ToString方法签名相同 /// /summary public static string ToString(this string str) { return My ToString(); } } class ClassExample { public static void Main() { // 声明一个string类型的变量word并初始化 string word The quick brown fox jumps over a lazy dog.; // 应该输出单词数9 Console.WriteLine(word.WordCount()); // 注意观察此处的输出 Console.WriteLine(word.ToString()); } } }上述代码的运行结果为9 The quick brown fox jumps over a lazy dog.对代码清单10-14的说明如表10-7所示。10.12 派生类型的向上转型派生类实际上是由基类的实例加上派生类新增的成员组成将派生类向上转型就转换到它的基类类型这种转换会让派生类中新增的成员变得“不可见”。我们仍使用“人-学生”为例如代码清单10-15所示。代码清单10-15 派生类型的向上转型using System; namespace ProgrammingCSharp4 { class Person { public string Name; public int Age; public Person(string name, int age) { Name name; Age age; } } class Student : Person { public string School; public Student(string name, int age, string school) : base(name, age) { School school; } } class ClassExample { public static void Main() { Student student new Student(大雄, 12, 一中); Person person student; Console.WriteLine(person.Name); Console.WriteLine(person.Age); // Console.WriteLine(person.School); // 该行会报错因为Person类没有School属性 } } }上述代码运行结果如下大雄12对代码清单10-15的说明如表10-8所示。Student类型的变量向上转型到Person类型如图10-8所示。