C#类和继承(十)
类继承
通过继承可以定义一个新类,新类纳入一个已经声明的类并进行扩展。已存在的类称为基类(base class),新类称为派生类(derived class)。
- 派生类扩展它的基类,因为它包含了基类的成员,还有派生类本身声明中的新增功能。
- 派生类不能删除它所继承的任何成员。
1 | class OtherClass : SomeClass |
访问继承的成员
1 | class SomeClass //基类 |
1 | Base class -- Method1: base class field |
所有类都派生自object类
除了特殊的类object,所有的类都是派生类,即使它们没有基类规格说明。类object是唯一的非派生类,因为它是继承层次结构的基础。
没有基类规格说明的类隐式地直接派生自类 object。不加基类规格说明只是指定 object 为基类的简写。这两种形式是语义等价的。class SomeClass { ... } 与 class SomeClass : object { ... } 语义等价。
- 一个类声明的基类规格说明中只能有一个单独的类。这称为单继承。
- 基类和派生类是相对的术语。所有的类都是派生类,要么派生自
object,要么派生自其他的类。所以,通常称一个类为派生类时,我们的意思是它直接派生自某类而不是object。
1 | class SomeClass |
屏蔽基类的成员
虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽(mask)基类成员。这是继承的主要功能之一,非常实用。
- 要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。
- 通过在派生类中声明新的带有相同签名的函数成员,可以屏蔽继承的函数成员。请记住,签名由名称和参数列表组成,不包括返回类型。
- 要让编译器知道你在故意屏蔽继承的成员,可使用
new修饰符。否则,程序可以成功编译,但编译器会警告你隐藏了一个继承的成员。 - 也可以屏蔽静态成员。
1 | class SomeClass //基类 |
1 | OtherClass.Method1: OtherClass Field1 |
基类的访问
如果派生类必须访问被隐藏的继承成员,可以使用基类访问(base access)表达式。基类访问表达式由关键字 base 后面跟着一个点和成员的名称组成。Console.WriteLine("{0}", base.Field1);。
1 | class SomeClass { |
1 | Field1 -- In the derived class |
使用基类的引用
派生类的实例由基类的实例和派生类新增的成员组成。派生类的引用指向整个类对象,包括基类部分。
如果有一个派生类对象的引用,就可以获取该对象基类部分的引用(使用类型转换运算符把该引用转换为基类类型)。
类型转换运算符放置在对象引用的前面,由圆括号括起的要被转换成的类名组成。
将派生类对象强制转换为基类对象的作用是产生的变量只能访问基类的成员。
1 | class MyBaseClass |
1 | This is the derived class. |
虚方法和覆写方法
虚方法可以使基类的引用访问“升至”派生类内。
可以使用基类引用调用派生类的方法,只需满足下面的条件:
- 派生类的方法和基类的方法有相同的签名和返回类型。
- 基类的方法使用
virtual标注。 - 派生类的方法使用
override标注。
1 | class MyBaseClass |
1 | This is the derived class. |
- 覆写和被覆写的方法必须有相同的可访问性。如果被覆写的方法是
private的,而覆写方法是public的,是不可以的。 - 不能覆写
static方法或非虚方法。 - 方法、属性和索引器,以及另一种成员类型——事件,都可以被声明为
virtual和override。
覆写标记为override的方法
覆写方法可以在继承的任何层次出现。
- 当使用对象基类部分的引用调用一个被覆写的方法时,方法的调用被沿派生层次上溯执行,一直到标记为
override的方法的最高派生(most-derived)版本。 - 如果在更高的派生级别有该方法的其他声明,但没有被标记为
override,那么它们不会被调用。
公共部分(基类与派生类定义)
1 | class MyBaseClass // 基类 |
使用 override 的 SecondDerived时:
无论Print是通过派生类调用还是通过基类调用的,都会调用最高派生类中的方法。当通过基类调用时,调用沿着继承层次向上传递。
1 | class SecondDerived : MyDerivedClass |
1 | This is the second derived class. |
使用 new 的 SecondDerived时:
当通过SecondDerived的引用调用方法Print时,SecondDerived中的方法被执行。
当通过MyBaseClass的引用调用Print方法时,方法调用只向上传递了一级,到达类MyDerived。
1 | class SecondDerived : MyDerivedClass |
1 | This is the second derived class. |
覆盖其他成员类型
在属性、事件以及索引器上virtual/override用法也是一样的。
1 | class MyBaseClass |
1 | 10 |
构造函数的执行
- 要创建对象的基类部分,需要隐式调用基类的某个构造函数。
- 继承层次链中的每个类在执行它自己的构造函数体之前执行它的基类构造函数。
创建一个实例的过程中,完成的第一件事是初始化对象的所有实例成员。在此之后,调用基类的构造函数,然后才执行该类自己的构造函数体。
1 | class MyDerivedClass : MyBaseClass |
构造函数初始化语句
默认情况下,在构造对象时,将调用基类的无参数构造函数。
但构造函数可以重载,所以基类可能有一个以上的构造函数。如果希望派生类使用一个指定的基类构造函数而不是无参数构造函数,必须在构造函数初始化语句中指定它。
有两种形式的构造函数初始化语句:
- 第一种形式使用关键字
base并指明使用哪个基类构造函数。 - 第二种形式使用关键字
this并指明应该使用当前类的哪一个构造函数。
1 | public MyDerivedClass( int x, string s ) : base( s, x ) |
1 | public MyClass(int x) : this(x, "Using Default String") |
1 | class MyClass |
类访问修饰符
类可以被系统中其他类看到并访问。可访问(accessible)有时也称为可见(visible)。类的可访问性有两个级别:public和internal。
- 标记为
public的类可以被系统内任何程序集中的代码访问。public class MyBaseClass { ... } - 标记为
internal的类只能被它自己所在的程序集内的类看到。- 这是默认的可访问级别,所以,除非在类的声明中显式地指定修饰符
public,否则程序集外部的代码不能访问该类。 - 可以使用
internal访问修饰符显式地声明一个类为内部的。internal class MyBaseClass {...}
- 这是默认的可访问级别,所以,除非在类的声明中显式地指定修饰符
程序集间的继承
C#允许从一个在不同的程序集内定义的基类来派生类。
- 基类必须被声明为
public,这样才能从它所在的程序集外部访问它。 - 必须在Visual Studio工程中的References节点中添加对包含该基类的程序集的引用。可以在Solution Explorer中找到该标题。
要使引用其他程序集中的类和类型更容易,不使用它们的完全限定名称,可以在源文件的顶部放置一个using指令,并带上将要访问的类或类型所在的命名空间。
1 | // Assembly1.cs |
1 | // Assembly2.cs |
1 | I am MyBaseClass |
成员访问修饰符
- 所有显式声明在类声明中的成员都是互相可见的,无论它们的访问性如何。
- 继承的成员不在类的声明中显式声明,所以,继承的成员对派生类的成员可以是可见的,也可以是不可见的。
- 以下是5个成员访问级别的名称:
- public
- private
- protected
- internal
- protected internal
- 必须对每个成员指定成员访问级别。如果不指定某个成员的访问级别,它的隐式访问级别为
private。 - 成员的可访问性不能比它的类高。也就是说,如果一个类的可访问性限于它所在的程序集,那么类的成员在程序集的外部也不可见,无论它们的访问修饰符是什么,
public也不例外。
访问成员的区域
1 | public class MyClass |
另一个类(如类B)能否访问这些成员取决于该类的两个特征:
- 类B是否派生自
MyClass类。 - 类B是否和
MyClass类在同一程序集。
这两个特征划分出4个集合: - 在同一程序集且继承
MyClass(右下)。 - 在同一程序集但不继承
MyClass(左下)。 - 在不同的程序集且继承
MyClass(右上)。 - 在不同的程序集且不继承
MyClass(左上)。
这些特征用于定义 5 种访问级别。

![[csharp-10-oop-accessible.png]]
公有成员的可访问性
public 访问级别是限制最少的。
所有的类,包括程序集内部的类和外部的类都可以自由地访问成员。
要声明一个公有成员,使用 public 访问修饰符。
1 | public int Member1; |

![[csharp-10-oop-accessible1.png]]
私有成员的可访问性
private 访问级别是限制最严格的。
private类成员只能被它自己的类的成员访问。它不能被其他的类访问,包括继承它的类。- 然而,
private成员能被嵌套在它的类中的类成员访问(嵌套类)。

![[csharp-10-oop-accessible2.png]]
受保护成员的可访问性
protected 访问级别如同 private 访问级别,但它允许派生自该类的类访问该成员。即使程序集外部继承该类的类也能访问该成员。

![[csharp-10-oop-accessible3.png]]
内部成员的可访问性
标记为 internal 的成员对程序集内部的所有类可见,但对程序集外部的类不可见。

![[csharp-10-oop-accessible4.png]]
受保护内部成员的可访问性
标记为 protected internal 的成员对所有继承该类的类以及程序集内部的所有类可见。注意,允许访问的集合是 protected 修饰符允许访问的类的集合加上 internal 修饰符允许访问的类的集合。注意,这是 protected 和 internal 的并集,不是交集。

![[csharp-10-oop-accessible5.png]]
成员访问修饰符小结
成员的访问修饰符:
| 修饰符 | 含义 |
|---|---|
| private | 只在类的内部可访问 |
| internal | 对该程序集内所有类可访问 |
| protected | 对所有继承该类的类可访问 |
| protected internal | 对所有继承该类或在该程序集内声明的类可访问 |
| public | 对任何类可访问 |
成员的可访问性总结:
| 修饰符 | 同一程序集内的类(非派生) | 同一程序集内的类(派生) | 不同程序集内的类(非派生) | 不同程序集内的类(派生) |
|---|---|---|---|---|
private |
||||
internal |
✓ | ✓ | ||
protected |
✓ | ✓ | ||
protected internal |
✓ | ✓ | ✓ | |
public |
✓ | ✓ | ✓ | ✓ |
抽象成员
抽象成员是指设计为被覆写的函数成员。
- 必须是一个函数成员。也就是说,字段和常量不能为抽象成员。
- 必须用
abstract修饰符标记。 - 不能有实现代码块。抽象成员的代码用分号表示。
1 | abstract public void PrintStuff(string s); //分号替换实现 |
一共有4种类型的成员可以声明为抽象的:
- 方法
- 属性
- 事件
- 索引器
尽管抽象成员必须在派生类中用相应的成员覆写,但不能把 virtual 修饰符附加到 abstract 修饰符。
类似于虚成员,派生类中抽象成员的实现必须指定 override 修饰符。
| 对比项 | 虚成员 | 抽象成员 |
|---|---|---|
| 关键字 | virtual |
abstract |
| 实现体 | 有实现体 | 没有实现体,被分号取代 |
| 在派生类中被覆写 | 能被覆写,使用 override |
必须被覆写,使用 override |
| 成员的类型 | 方法 属性 事件 索引器 |
方法 属性 事件 索引器 |
抽象类
抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。
- 不能创建抽象类的实例。
- 抽象类使用
abstract修饰符声明。
1 | abstract class MyClass { ... } |
- 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带实现的成员的任意组合。
- 抽象类自己可以派生自另一个抽象类。
- 任何派生自抽象类的类必须使用
override关键字实现该类所有的抽象成员,除非派生类自己也是抽象类。
1 | abstract class AbClass //抽象类 |
抽象类和抽象方法的示例
1 | abstract class AbClass //抽象类 |
1 | I am AbClass |
1 | abstract class MyBase //抽象和非抽象成员的组合 |
1 | This is a string. |
密封类
抽象类必须用作基类,它不能像独立的类对象那样被实例化。密封类与它相反。
- 密封类只能被用作独立的类,它不能被用作基类。将它用作其他类的基类会产生编译错误。
- 密封类使用
sealed修饰符标注。
1 | sealed class MyClass |
静态类
静态类中所有成员都是静态的。静态类用于存放不受实例数据影响的数据和函数。
静态类的一个常见用途可能是创建一个包含一组数学方法和值的数学库。
- 类本身必须标记为
static。 - 类的所有成员必须是静态的。
- 类可以有一个静态构造函数,但不能有实例构造函数,因为不能创建该类的实例。
- 静态类是隐式密封的,也就是说,不能继承静态类。
- 可以使用类名和成员名,像访问其他静态成员那样访问静态类的成员。从C# 6.0开始,也可以通过使用
using static指令来访问静态类的成员,而不必使用类名。
1 | static public class MyMath { |
1 | 3 is odd is True. |
扩展方法
目前为止的每个方法都和声明它的类关联。扩展方法特性扩展了这个边界,允许编写的方法和声明它的类之外的类关联。
在现实世界的开发中,扩展方法是一个特别有用的工具。事实上,几乎整个LINQ库都是通过扩展方法来实现的。
- 声明扩展方法的类必须声明为
static。 - 扩展方法本身必须声明为
static。 - 扩展方法必须包含关键字
this作为它的第一个参数类型,并在后面跟着它扩展的类的名称。
![[csharp-10-oop-kuozhan-func.png]]
1 | namespace ExtensionMethods |
1 | Sum: 12 |
命名约定
| 风格名称 | 描述 | 推荐使用 | 示例 |
|---|---|---|---|
| Pascal 大小写 | 标识符中每个单词的首字母大写 | 用于类型名称和类中对外可见成员的名称。涉及的名称包括:类、方法、命名空间、属性和公有字段 | CardDeck, Dealershand |
| Camel 大小写 | 标识符中每个单词的首字母大写,第一个单词除外 | 用于局部变量的名称和方法声明的形参名称 | totalCycleCount, randomSeedParam |
| 下划线加 Camel 大小写 | 以下划线开头的 Camel 大小写的标识符 | 用于私有和受保护的字段 | _cycleCount, _selectedIndex |