通过继承,可以创建重用、扩展和修改在其他类中定义的行为的新类。继承主要实现重用代码,节省开发时间。C#中的继承符合下列规则:
- 继承是可传递的。如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object类作为所有类的基类。
- 派生类应当是对基类的扩展。派生类可以添加新的成员,但不能除去已经继承的成员的定义。
- 构造函数和析构函数不能被继承。除此之外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。
- 派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖已继承的成员。但这并不因为这派生类删除了这些成员,只是不能再访问这些成员。
- 类可以定义虚文法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而实现类可以展示出多态性。
derived 派生
派生类只能有一个直接基类。 但是,继承是可传递的。 如果 ClassC 派生自 ClassB,并且 ClassB 派生自 ClassA,则 ClassC 会继承在 ClassB 和 ClassA 中声明的成员。
从概念上讲,派生类是基类的专门化。例如,如果有一个基类 Animal
,则可以有一个名为 Mammal
的派生类,以及另一个名为 Reptile
的派生类。Mammal
是 Animal
,Reptile
也是 Animal
,但每个派生类表示基类的不同专门化。
定义要从其他类派生的类时,派生类会隐式获得基类的所有成员(除了其构造函数和终结器)。派生类因而可以重用基类中的代码,而无需重新实现。在派生类中,可以添加更多成员。通过这种方法,派生类可扩展基类的功能。
从一个类派生出另一个类
PdaItem类派生出Contact类
(一个类继承自另一个类Contact
类继承自PdaItem
类)
1 | public class PdaItem |
使用继承的属性
1 | public class Program |
Contact
类自身没有定义Name
属性,但仍然可以使用继承自 PdaItem
类的Name
属性,并把它作为 Contact自身的一部分使用。除此之外,从 Contact 派生的其他任何类也会继承 PdaItem 类(以及 PdaItem的父类)的成员。这个继承链条
基类和派生类之间的转换
派生类型的值可以直接赋值给基类型的值,不需要添加转型操作符,不会引发异常。
反之则不成立。从基类型转换为派生类型,要求执行显式转换,并且在运行时可能会失败。
1 | public class Program |
private访问修饰符
派生类继承了除构造器和析构器之外的所有基类成员。但是,继承并不意味着一定能够访问。私有成员只能在声明他们的那个类中才能访问,派生类不能访问基类的private成员(除了一个例外,派生类同时是基类的一个嵌套类)。
1 | public class PdaItem { |
protected访问修饰符
受保护成员在其所在的类中可由派生类实例访问。
只有在通过派生类类型进行访问时,基类的受保护成员在派生类中才是可访问的。A protected member of a base class is accessible in a derived class only if the access occurs through the derived class type.
1 | class A |
语句 a.x = 10
生成错误,因为它是在静态方法 Main 中生成的,而不是类 B 的实例。The statement a.x = 10
generates an error because it is made within the static method Main, and not an instance of class B.
无法保护结构成员,因为无法继承结构。
在此示例中,DerivedPoint
类是从 Point
派生的。In this example, the class DerivedPoint
is derived from Point
. 因此,可以从派生类直接访问基类的受保护成员。Therefore, you can access the protected members of the base class directly from the derived class.
1 | class Point |
如果将 x
和 y
的访问级别更改为 private,编译器将发出错误消息:
'Point.y' is inaccessible due to its protection level.
'Point.x' is inaccessible due to its protection level.
单继承 多继承 聚合(Aggregation)
C++支持多继承,C#只支持单继承。在极少数需要多继承类结构的时候,一般的解决方案是使用聚合(Aggregation);换言之,不是一个类从另外一个类继承,而是一个类包含另一个类的实例。
用聚合解决单继承问题
1 | public class PdaItem { |
方法有点怪异,不够合理,有点多余。 ヾ(゚∀゚ゞ)ヾ(。 ̄□ ̄)ツ゜゜゜
密封类
sealed
修饰符可阻止其他类继承自该类。在下面的示例中,类 B
继承自类 A
,但没有类可以继承自类 B
。
1 | class A {} |
System.String
类型就是用sealed
修饰符禁止了派生。下面的代码编译不通过:
1 | using System; |
override 替代 覆盖 重写
overload
重载:指的是同一个类中有两个或多个名字相同但是参数不同的方法,(注:返回值不能区别函数是否重载),重载没有关键字。
override
替代 覆盖 重写:指子类对父类中虚函数或抽象函数的“覆盖”(这也就是有些书将过载翻译为覆盖的原因),但是这种“覆盖”和用new关键字来覆盖是有区别的。另外,override
需要和 virtual
对应(配合、配对)使用。override
似乎提高了派生类成员的优先级,例如:用基类声明类型而用派生类实例化的对象,具有override
修饰符的派生类成员会强制替代基类成员,而 ‘new’ 修饰符则无次优先级,还会执行基类成员。
new
替代:显式隐藏从基类继承的成员。隐藏继承的成员时,该成员的派生版本将替换基类版本。如果基类和派生类中有同名成员,而不使用 new
,则会产生编译警告,好像没有更多的差异了。
基类除了构造器和析构器之外,所有成员都可以在派生类中继承。
virtual
virtual
关键字用于修改方法、属性、索引器或事件声明,并使它们可以在派生类中被重写。例如,此方法可被任何继承它的类替代:
1 | public virtual double Area() |
虚拟成员的实现可由派生类中的替代成员更改。有关如何使用 virtual
关键字的详细信息,请参阅使用 Override 和 New 关键字进行版本控制和了解何时使用 Override 和 New 关键字。
以下示例显示了虚拟属性:
1 | using System; |
下面示例中,Shape
类包含 x
、y
两个坐标和 Area()
虚拟方法。不同的形状类(如 Circle
、Cylinder
和 Sphere
)继承 Shape
类,并为每个图形计算表面积。每个派生类都有各自的 Area()
替代实现。
请注意,继承的类 Circle
、Sphere
和 Cylinder
均使用初始化基类的构造函数(参考 构造器链),base
用法参考 base,如下面的声明中所示。
1 | public Cylinder(double r, double h): base(r, h) {} |
根据与方法关联的对象,下面的程序通过调用 Area()
方法的相应实现来计算并显示每个对象的相应区域。
1 | using System; |
对成员进行重载,会造成“运行时”调用最深的或者说派生的最远的实现,示例如下:
1 | using System; |
上例代码中,调用 item.Name
,而item
被声明为一个PdaItem
。但是,contact
的 FirstName
和 LastName
还是会被处理。这里的规则是:“运行时”遇到虚方法时,它会调用虚成员派生得最远的重写。在上例中,代码实例化一个Contact
并调用Contact.Name
,因为Contact
包含了Name
派生的最远的实现。
虚方法不应包含关键代码,因为如果派生类重写了它,那些代码就永远得不到调用。创建类时,必须谨慎选择是否允许重写方法,因为控制不了派生的实现。
new
前面例子中,如果重写子类方法却没有使用override关键字,编译器会报告警告信息,如下所示:
warning CS0114: “Contact.Name”隐藏继承的成员“PdaItem.Name”。若要使当前成员重写该实现,请添加关键字 override。否则,添加关键字 new。
override
修饰符用于扩展基类方法,而 new
修饰符则用于隐藏该方法。
在用作声明修饰符时,new
关键字可以显式隐藏从基类继承的成员。隐藏继承的成员时,该成员的派生版本将替换基类版本。虽然可以不使用 new
修饰符来隐藏成员,但将收到编译器警告。 如果使用 new
来显式隐藏成员,将禁止此警告。如果不使用 new
,则执行基类的成员,而忽略派生类的成员。
在此示例中,基类 BaseClass
和派生类 DerivedClass
使用相同的字段名 x
,从而隐藏了继承字段的值。另外还演示了如何使用完全限定名访问基类的隐藏成员。
1 | using System; |
使用new
关键字后,具有 BaseClass
类型的变量继续访问 BaseClass
的成员,而具有 DerivedClass
类型的变量首先继续访问 DerivedClass
中的成员,然后再考虑从 BaseClass
继承的成员。
示例:无修饰符 无同名成员
1 | using System; |
示例:无修饰符 有同名成员
1 | using System; |
你将看到在 BaseClass
中添加 Method2
方法将引发警告。警告显示 DerivedClass
中的 Method2
方法隐藏了 BaseClass
中的 Method2
方法。如果希望获得该结果,则建议使用 Method2
定义中的 new
关键字。或者,可重命名 Method2
方法之一来消除警告。
示例:new 修饰符 有同名成员
1 | using System; |
输出结果与上例不使用 new
修饰符是一样的,只是不再有警告。就CIL来说,new修饰符对编译器生成的代码没有任何影响。从 C# 的角度看,它唯一的作用就是移除编译器警告。
示例:virtual override 修饰符
1 | using System; |
使用 override
修饰符可使 bcdc
访问 DerivedClass
中定义的 Method1
方法。通常,这是继承层次结构中所需的行为。让具有从派生类创建的值的对象使用派生类中定义的方法。可使用 override
扩展基类方法实现该行为。
sealed 修饰符
类使用 sealed 修饰符是禁止从该类继承。类似的,虚成员也可以密封。一般很少这样用,除非迫切需要这种限制。
1 | class A { |
base 成员
base
关键字用于从派生类中访问基类的成员:
调用基类上已被其他方法重写的方法。Call a method on the base class that has been overridden by another method.
指定创建派生类实例时应调用的基类构造函数。
在本例中,基类 Person
和派生类 Employee
都有一个名为 Getinfo
的方法通过使用 base
关键字,可以从派生类中调用基类的 Getinfo
方法。
1 | using System; |
本示例显示如何指定在创建派生类实例时调用的基类构造函数。
1 | using System; |
abstract 抽象
使用 abstract 关键字可以创建不完整且必须在派生类中实现的类和 class 成员。
通过在类定义前面放置关键字 abstract
,可以将类声明为抽象类。
- 抽象类是仅供派生的类。
- 抽象类无法实例化,只能实例化从它派生的类。
- 抽象成员应当被重写,所以自动成为虚成员(但不能用
virtual
关键字显示声明)。 - 抽象成员不能声明为私有,私有的话派生类看不到它们。
理解:
- abstract 抽象:显式
⬇
强制要求所有派生类来提供实现。 - override 替代:显式
⬆
强制重写基类成员,。
1 | public abstract class A |
抽象类不能实例化。抽象类的用途是提供一个可供多个派生类共享的通用基类定义。例如,类库可以定义一个抽象类,将其用作多个类库函数的参数,并要求使用该库的程序员通过创建派生类来提供自己的类实现。
抽象类也可以定义抽象方法。方法是将关键字 abstract
添加到方法的返回类型的前面。
1 | public abstract class A |
例子:抽象类不能实例化
1 | public abstract class PdaItem { |
例子:抽象成员。抽象成员没有实现的方法或者属性,其作用是强制所有派生类提供实现。
1 | using System; |
抽象方法没有实现,所以方法定义后面是分号,而不是常规的方法块。抽象类的派生类必须实现所有抽象方法。当抽象类从基类继承虚方法时,抽象类可以使用抽象方法(abstract method
)重写该虚方法。例如:
1 | // compile with: /target:library |
如果将 virtual
方法声明为 abstract
,则该方法对于从抽象类继承的所有类而言仍然是虚方法。继承抽象方法的类无法访问方法的原始实现,因此在上一示例中,类 F 上的 DoWork
无法调用类 D 上的 DoWork
。通过这种方式,抽象类可强制派生类向虚拟方法提供新的方法实现。
多态性 Polymorphism
多态性常被视为自封装和继承之后,面向对象的编程的第三个支柱。Polymorphism(多态性)是一个希腊词,指“多种形态”,多态性具有两个截然不同的方面:
在运行时,在方法参数和集合或数组等位置,派生类的对象可以作为基类的对象处理。At run time, objects of a derived class may be treated as objects of a base class in places such as method parameters and collections or arrays. 发生此情况时,该对象的声明类型不再与运行时类型相同。When this occurs, the object’s declared type is no longer identical to its run-time type.
基类可以定义并实现虚方法,派生类可以重写这些方法,即派生类提供自己的定义和实现。在运行时,客户端代码调用该方法,CLR 查找对象的运行时类型,并调用虚方法的重写方法。因此,你可以在源代码中调用基类的方法,但执行该方法的派生类版本。
虚方法允许你以统一方式处理多组相关的对象。例如,假定你有一个绘图应用程序,允许用户在绘图图面上创建各种形状。你在编译时不知道用户将创建哪些特定类型的形状。但应用程序必须跟踪创建的所有类型的形状,并且必须更新这些形状以响应用户鼠标操作。你可以使用多态性通过两个基本步骤解决这一问题:
- 创建一个类层次结构,其中每个特定形状类均派生自一个公共基类。
- 使用虚方法通过对基类方法的单个调用来调用任何派生类上的相应方法。
首先,创建一个名为 Rectangle Shape
的基类,并创建一些派生类,例如 Triangle Circle
、 和 。为 Draw Shape
类提供一个名为 的虚方法,并在每个派生类中重写该方法以绘制该类表示的特定形状。创建一个 List<Shape>
对象,并向该对象添加 Circle、Triangle 和 Rectangle。若要更新绘图图面,请使用 foreach 循环对该列表进行循环访问,并对其中的每个 Shape
对象调用 Draw
方法。虽然列表中的每个对象都具有声明类型 Shape
,但调用的将是运行时类型(该方法在每个派生类中的重写版本)。
1 | using System; |
在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object。
虚成员 Virtual Members
当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。派生类的设计器可以选择是否:
重写基类中的虚拟成员
继承最接近的基类方法而不重写它
定义隐藏基类实现的成员的新非虚实现
仅当基类成员声明为 virtual 或 abstract 时,派生类才能重写基类成员。派生成员必须使用 override 关键字显式指示该方法将参与虚调用。以下代码提供了一个示例:The following code provides an example:
1 | public class BaseClass |
字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的。当派生类重写某个虚拟成员时,即使该派生类的实例被当作基类的实例访问,也会调用该成员。以下代码提供了一个示例:
1 | DerivedClass B = new DerivedClass(); |
虚方法和属性允许派生类扩展基类,而无需使用方法的基类实现。有关详细信息,请参阅使用 Override 和 New 关键字进行版本控制。接口提供另一种方式来定义将实现留给派生类的方法或方法集。有关详细信息,请参阅接口。
使用新成员隐藏基类成员 Hiding Base Class Members with New Members
如果希望派生成员具有与基类中的成员相同的名称,但又不希望派生成员参与虚调用,则可以使用 new 关键字。new
关键字放置在要替换的类成员的返回类型之前。以下代码提供了一个示例:
1 | public class BaseClass |
通过将派生类的实例强制转换为基类的实例,仍然可以从客户端代码访问隐藏的基类成员。例如:
1 | DerivedClass B = new DerivedClass(); |
阻止派生类重写虚拟成员 Preventing Derived Classes from Overriding Virtual Members
无论在虚拟成员和最初声明虚拟成员的类之间已声明了多少个类,虚拟成员永远都是虚拟的。 如果类 A 声明了一个虚拟成员,类 B 从 A 派生,类 C 从类 B 派生,则类 C 继承该虚拟成员,并且可以选择重写它,而不管类 B 是否为该成员声明了重写。以下代码提供了一个示例:
1 | public class A |
派生类可以通过将重写声明为 sealed 来停止虚拟继承。这需要在类成员声明中的 override
sealed 关键字前面放置 关键字。以下代码提供了一个示例:
1 | public class C : B |
在上一示例中,方法 DoWork
对从 C 派生的任何类都不再是虚方法。它对 C 的实例仍是虚拟的,即使它们转换为类型 B 或类型 A。使用 new
关键字可以将密封方法替换为派生类,如下方示例所示:
1 | public class D : C |
在此情况下,如果在 D 中使用类型为 D 的变量调用 DoWork
,被调用的将是新的 。如果使用类型为 C、B 或 A 的变量访问 D 的实例,对 DoWork
的调用将遵循虚拟继承的规则,即把这些调用传送到类 C 的实现。
从派生类访问基类虚拟成员 Accessing Base Class Virtual Members from Derived Classes
已替换或重写某个方法或属性的派生类仍然可以使用基关键字访问基类的该方法或属性。以下代码提供了一个示例:
1 | public class Base |
有关详细信息,请参阅 base。
System.Object 基类
System.Object在.Net中是所有类型的基类,任何类型都直接或间接地继承自System.Object。没有指定基类的类型都默认继承于System.Object。
由于所有的类型都继承于System.Object。因此,所有的类型都具有下面这些特性:
方法名 | 说明 |
---|---|
GetType() | 获取对象的类型. |
ToString() | 获取对象的字符串信息,默认返回对象带命名空间的全名。 |
public virtual bool Equals(Object obj); | 确定指定的对象是否等于当前对象。 |
public static bool Equals(Object objA,Object objB); | 确定指定的对象实例是否被视为相等。 |
public static bool ReferenceEquals(Object objA,Object objB); | 确定指定的 Object 实例是否是相同的实例。 |
GetHashCode() | 获取对象的值的散列码。 |
Finalize() | 在垃圾回收时,进行资源管理。 |
MemberwiseClone() | 对象实例的浅拷贝。 |
is
检查对象是否与给定类型兼容,或(从 C# 7 开始)针对某个模式测试表达式。
类型兼容性测试Testing for type compatibility
is
关键字在运行时评估类型兼容性。它确定对象实例或表达式结果是否可转换为指定类型。语法如下:
1 | expr is type |
其中 expr 是计算结果为某个类型的实例的表达式,而 type 是 expr 结果要转换到的类型的名称。如果 expr 非空,并且通过计算表达式得出的对象可转换为 type,则 is
语句为 true
;否则返回 false
。
例如,以下代码确定 obj
是否可转换为 Person
类型的实例:
1 | if (obj is Person) { |
如果满足以下条件,则 is
语句为 true:
expr 是与 type 具有相同类型的一个实例。
expr 是派生自 type 的类型的一个实例。换言之,expr 结果可以向上转换为 type 的一个实例。
expr 具有属于 type 的一个基类的编译时类型,expr 还具有属于 type 或派生自 type 的运行时类型。变量的编译时类型是其声明中定义的变量类型。变量的运行时类型是分配给该变量的实例类型。
expr 是实现 type 接口的类型的一个实例。
下例表明,对于所有这些转换,is
表达式的计算结果都为 true
。
1 | using System; |
从 C# 7 开始,可以使用类型模式的模式匹配来编写代码,代码使用 is
语句更为简洁。
利用 is
的模式匹配 Pattern matching with is
从 C# 7 开始,is
和 switch 语句支持模式匹配。is
关键字支持以下模式:
类型模式,用于测试表达式是否可转换为指定类型,如果可以,则将其转换为该类型的一个变量。
常量模式,用于测试表达式计算结果是否为指定的常数值。[Constant pattern]
var 模式,始终成功的匹配,可将表达式的值绑定到新局部变量。[var pattern]
类型模式 Type pattern
使用类型模式执行模式匹配时,is
会测试表达式是否可转换为指定类型,如果可以,则将其转换为该类型的一个变量。它是 is
语句的直接扩展,可执行简单的类型计算和转换。is
类型模式的一般形式为:
1 | expr is type varname |
其中 expr 是计算结果为某个类型的实例的表达式,type 是 expr 结果要转换到的类型的名称,varname 是 expr 结果要转换到的对象(如果 is
测试为 true
)。
如果以下任一条件成立,则 is
表达式为 true
:
expr 是与 type 具有相同类型的一个实例。
expr 是派生自 type 的类型的一个实例。换言之,expr 结果可以向上转换为 type 的一个实例。
expr 具有属于 type 的一个基类的编译时类型,expr 还具有属于 type 或派生自 type 的运行时类型。变量的编译时类型是其声明中定义的变量类型。变量的运行时类型是分配给该变量的实例类型。
expr 是实现 type 接口的类型的一个实例。
如果 exp 为 true
,且 is
与 if
语句一起使用,则会分配 varname,并且其仅在 if
语句中具有局部范围。
下列示例使用 is
类型模式为类型的 IComparable.CompareTo(Object) 方法提供实现。
1 | using System; |
如果没有模式匹配,则可能按以下方式编写此代码。使用类型模式匹配无需测试转换结果是否为 null
,从而生成更紧凑易读的代码。
1 | using System; |
常量模式 Constant pattern
使用常量模式执行模式匹配时,is
会测试表达式结果是否等于指定常量。在 C# 6 和更低版本中,switch 语句支持常量模式从。C# 7 开始,is
语句也支持常量模式。语法为:
1 | expr is constant |
其中 expr 是要计算的表达式,constant 是要测试的值。 constant 可以是以下任何常数表达式:
一个文本值。A literal value.
已声明
const
变量的名称。The name of a declaredconst
variable.一个枚举常量。An enumeration constant.
常数表达式的计算方式如下:
如果 expr 和 constant 均为整型类型,则 C# 相等运算符确定表示式是否返回
true
(即,是否为expr == constant
)。否则,由对静态 Object.Equals(expr, constant) 方法的调用来确定表达式的值。
下例同时使用了类型模式和常量模式来测试对象是否为 Dice
实例,如果是,则确定骰子的值是否为 6。
1 | using System; |
var 模式 var pattern
具有 var 模式的模式匹配始终成功。
1 | expr is var varname |
其中,expr 的值始终分配给名为 varname 的局部变量。varname 是一个与 expr 具有相同类型的静态变量。下例使用 var 模式向名为 obj
的变量分配表达式。然后,显示 obj
的值和类型。
1 | using System; |
请注意,如果 expr 为 null
,则 is
表达式仍为 true 并向 varname 分配 null
。
as
可以使用 as
运算符在符合的引用类型或可以为 null 的类型之间执行某些类型的转换。以下代码显示一个示例。
1 | using System; |
as
运算符类似于转换运算。但是,如果无法进行转换,则 as
会返回 null
,而不是引发异常。请看下面的示例:
1 | expression as type |
该代码等效于以下表达式,但 expression
变量仅进行一次计算。
1 | expression is type ? (type)expression : (type)null |
请注意,as
运算符仅执行引用转换、可以为 null 的转换和装箱转换。as
运算符无法执行其他转换,例如用户定义的转换,应使用转换表达式执行此转换。
1 | using System; |