C#本质论笔记 第6章 继承

通过继承,可以创建重用、扩展和修改在其他类中定义的行为的新类。继承主要实现重用代码,节省开发时间。C#中的继承符合下列规则:

  • 继承是可传递的。如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object类作为所有类的基类。
  • 派生类应当是对基类的扩展。派生类可以添加新的成员,但不能除去已经继承的成员的定义。
  • 构造函数和析构函数不能被继承。除此之外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。
  • 派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖已继承的成员。但这并不因为这派生类删除了这些成员,只是不能再访问这些成员。
  • 类可以定义虚文法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而实现类可以展示出多态性。

derived 派生

派生类只能有一个直接基类。 但是,继承是可传递的。 如果 ClassC 派生自 ClassB,并且 ClassB 派生自 ClassA,则 ClassC 会继承在 ClassB 和 ClassA 中声明的成员。

从概念上讲,派生类是基类的专门化。例如,如果有一个基类 Animal,则可以有一个名为 Mammal 的派生类,以及另一个名为 Reptile 的派生类。MammalAnimalReptile 也是 Animal,但每个派生类表示基类的不同专门化。

定义要从其他类派生的类时,派生类会隐式获得基类的所有成员(除了其构造函数和终结器)。派生类因而可以重用基类中的代码,而无需重新实现。在派生类中,可以添加更多成员。通过这种方法,派生类可扩展基类的功能。

从一个类派生出另一个类 PdaItem类派生出Contact类(一个类继承自另一个类 Contact类继承自PdaItem类)

1
2
3
4
5
6
7
8
9
10
11
12
public class PdaItem
{
public string Name { get; set; }
public DateTime LastUpdated { get; set; }
}

// Define the Contact class as inheriting the PdaItem class
public class Contact : PdaItem
{
public string Address { get; set; }
public string Phone { get; set; }
}

使用继承的属性

1
2
3
4
5
6
7
8
9
public class Program
{
public static void Main()
{
Contact contact = new Contact();
contact.Name = "Inigo Montoya";
// ...
}
}

Contact类自身没有定义Name属性,但仍然可以使用继承自 PdaItem 类的Name属性,并把它作为 Contact自身的一部分使用。除此之外,从 Contact 派生的其他任何类也会继承 PdaItem 类(以及 PdaItem的父类)的成员。这个继承链条

基类和派生类之间的转换

  • 派生类型的值可以直接赋值给基类型的值,不需要添加转型操作符,不会引发异常。

  • 反之则不成立。从基类型转换为派生类型,要求执行显式转换,并且在运行时可能会失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Program
{
public static void Main()
{
// Derived types can be implicitly converted to
// base types
Contact contact = new Contact();
PdaItem item = contact;
// ...

// Base types must be cast explicitly to derived types
contact = (Contact)item;
// ...
}
}

private访问修饰符

派生类继承了除构造器和析构器之外的所有基类成员。但是,继承并不意味着一定能够访问。私有成员只能在声明他们的那个类中才能访问,派生类不能访问基类的private成员(除了一个例外,派生类同时是基类的一个嵌套类)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class PdaItem {
private string _Name;
// ...
}

public class Contact : PdaItem {
// ...
}

public class Program {
public static void Main () {
Contact contact = new Contact ();

// ERROR: 'PdaItem. _Name' is inaccessible
// due to its protection level
//contact._Name = "Inigo Montoya"; //uncomment this line and it will not compile
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A
{
protected int x = 123;
}

class B : A
{
static void Main()
{
A a = new A();
B b = new B();

// Error CS1540, because x can only be accessed by
// classes derived from A.
// a.x = 10;

// OK, because this class derives from A.
b.x = 10;
}
}

语句 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point
{
protected int x;
protected int y;
}

class DerivedPoint: Point
{
static void Main()
{
DerivedPoint dpoint = new DerivedPoint();

// Direct access to protected members:
dpoint.x = 10;
dpoint.y = 15;
Console.WriteLine("x = {0}, y = {1}", dpoint.x, dpoint.y);
}
}
// Output: x = 10, y = 15

如果将 xy 的访问级别更改为 private,编译器将发出错误消息:

'Point.y' is inaccessible due to its protection level.

'Point.x' is inaccessible due to its protection level.

单继承 多继承 聚合(Aggregation)

C++支持多继承,C#只支持单继承。在极少数需要多继承类结构的时候,一般的解决方案是使用聚合(Aggregation);换言之,不是一个类从另外一个类继承,而是一个类包含另一个类的实例。

用聚合解决单继承问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class PdaItem {
// ...
}

public class Person {
public string FirstName { get; set; }
public string LastName { get; set; }
// ...
}

public class Contact : PdaItem {
private Person InternalPerson { get; set; }

public string FirstName {
get { return InternalPerson.FirstName; }
set { InternalPerson.FirstName = value; }
}

public string LastName {
get { return InternalPerson.LastName; }
set { InternalPerson.LastName = value; }
}
// ...
}

方法有点怪异,不够合理,有点多余。 ヾ(゚∀゚ゞ)ヾ(。 ̄□ ̄)ツ゜゜゜

密封类

sealed 修饰符可阻止其他类继承自该类。在下面的示例中,类 B 继承自类 A,但没有类可以继承自类 B

1
2
class A {}
sealed class B : A {}

System.String 类型就是用sealed修饰符禁止了派生。下面的代码编译不通过:

1
2
3
4
5
6
7
8
9
10
11
using System;

public class Program {
public static void Main () {
}
}

public class MyClass : String { }
/* 输出
错误 CS0509 “MyClass”: 无法从密封类型“string”派生
*/

override 替代 覆盖 重写

overload 重载:指的是同一个类中有两个或多个名字相同但是参数不同的方法,(注:返回值不能区别函数是否重载),重载没有关键字

override 替代 覆盖 重写:指子类对父类中虚函数或抽象函数的“覆盖”(这也就是有些书将过载翻译为覆盖的原因),但是这种“覆盖”和用new关键字来覆盖是有区别的。另外,override 需要和 virtual 对应(配合、配对)使用。override 似乎提高了派生类成员的优先级,例如:用基类声明类型而用派生类实例化的对象,具有override 修饰符的派生类成员会强制替代基类成员,而 ‘new’ 修饰符则无次优先级,还会执行基类成员。

new 替代:显式隐藏从基类继承的成员。隐藏继承的成员时,该成员的派生版本将替换基类版本。如果基类和派生类中有同名成员,而不使用 new ,则会产生编译警告,好像没有更多的差异了。

基类除了构造器和析构器之外,所有成员都可以在派生类中继承。

virtual

virtual 关键字用于修改方法、属性、索引器或事件声明,并使它们可以在派生类中被重写。例如,此方法可被任何继承它的类替代:

1
2
3
4
public virtual double Area()
{
return x * y;
}

虚拟成员的实现可由派生类中的替代成员更改。有关如何使用 virtual 关键字的详细信息,请参阅使用 Override 和 New 关键字进行版本控制了解何时使用 Override 和 New 关键字

以下示例显示了虚拟属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System;

class MyBaseClass {
// virtual auto-implemented property. Overrides can only
// provide specialized behavior if they implement get and set accessors.
public virtual string Name { get; set; }

// ordinary virtual property with backing field
private int num;
public virtual int Number {
get { return num; }
set { num = value; }
}
}

class MyDerivedClass : MyBaseClass {
private string name;

// Override auto-implemented property with ordinary property
// to provide specialized accessor behavior.
public override string Name {
get {
return name;
}
set {
if (value != String.Empty) {
name = value;
} else {
name = "Unknown";
}
}
}

}

下面示例中,Shape 类包含 xy 两个坐标和 Area() 虚拟方法。不同的形状类(如 CircleCylinderSphere)继承 Shape 类,并为每个图形计算表面积。每个派生类都有各自的 Area() 替代实现。

请注意,继承的类 CircleSphereCylinder 均使用初始化基类的构造函数(参考 构造器链),base 用法参考 base,如下面的声明中所示。

1
public Cylinder(double r, double h): base(r, h) {}

根据与方法关联的对象,下面的程序通过调用 Area() 方法的相应实现来计算并显示每个对象的相应区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
using System;

class TestClass {
public class Shape {
public const double PI = Math.PI;
protected double x, y;
public Shape () { }
public Shape (double x, double y) {
this.x = x;
this.y = y;
}

public virtual double Area () {
return x * y;
}
}

public class Circle : Shape {
public Circle (double r) : base (r, 0) { }

public override double Area () {
return PI * x * x;
}
}

class Sphere : Shape {
public Sphere (double r) : base (r, 0) { }

public override double Area () {
return 4 * PI * x * x;
}
}

class Cylinder : Shape {
public Cylinder (double r, double h) : base (r, h) { }

public override double Area () {
return 2 * PI * x * x + 2 * PI * x * y;
}
}

static void Main () {
double r = 3.0, h = 5.0;
Shape c = new Circle (r);
Shape s = new Sphere (r);
Shape l = new Cylinder (r, h);
// Display results:
Console.WriteLine ("Area of Circle = {0:F2}", c.Area ());
Console.WriteLine ("Area of Sphere = {0:F2}", s.Area ());
Console.WriteLine ("Area of Cylinder = {0:F2}", l.Area ());
}
}
/*
Output:
Area of Circle = 28.27
Area of Sphere = 113.10
Area of Cylinder = 150.80
*/

对成员进行重载,会造成“运行时”调用最深的或者说派生的最远的实现,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;

public class Program {
public static void Main () {
Contact contact;
PdaItem item;

contact = new Contact ();
item = contact;

// Set the name via PdaItem variable
item.Name = "Inigo Montoya";

// Display that FirstName & LastName
// properties were set.
Console.WriteLine ("{0} {1}",
contact.FirstName, contact.LastName);
}
}

public class PdaItem {
public virtual string Name { get; set; }
// ...
}

public class Contact : PdaItem {
public override string Name {
get {
return FirstName + " " + LastName;
}

set {
string[] names = value.Split (' ');
// Error handling not shown.
FirstName = names[0];
LastName = names[1];
}
}

public string FirstName { get; set; }
public string LastName { get; set; }

// ...
}

上例代码中,调用 item.Name,而item被声明为一个PdaItem。但是,contactFirstNameLastName 还是会被处理。这里的规则是:“运行时”遇到虚方法时,它会调用虚成员派生得最远的重写。在上例中,代码实例化一个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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;

public class BaseClass {
public static int x = 55;
public static int y = 22;
}

public class DerivedClass : BaseClass {
// Hide field 'x'.
new public static int x = 100;

static void Main () {
// Display the new value of x:
Console.WriteLine (x);

// Display the hidden value of x:
Console.WriteLine (BaseClass.x);

// Display the unhidden member y:
Console.WriteLine (y);
}
}
/*
Output:
100
55
22
*/

使用new关键字后,具有 BaseClass 类型的变量继续访问 BaseClass 的成员,而具有 DerivedClass 类型的变量首先继续访问 DerivedClass 中的成员,然后再考虑从 BaseClass 继承的成员。

示例:无修饰符 无同名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;

class BaseClass {
public void Method1 () {
Console.WriteLine ("Base - Method1");
}
}

class DerivedClass : BaseClass {
public void Method2 () {
Console.WriteLine ("Derived - Method2");
}
}

class Program {
static void Main (string[] args) {
BaseClass bc = new BaseClass ();
DerivedClass dc = new DerivedClass ();
BaseClass bcdc = new DerivedClass ();

bc.Method1 ();
dc.Method1 ();
dc.Method2 ();
bcdc.Method1 ();
}
// Output:
// Base - Method1
// Base - Method1
// Derived - Method2
// Base - Method1
}

示例:无修饰符 有同名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;

class BaseClass {
public void Method1 () {
Console.WriteLine ("Base - Method1");
}
public void Method2 () {
Console.WriteLine ("Base - Method2");
}
}

class DerivedClass : BaseClass {
public void Method2 () {
Console.WriteLine ("Derived - Method2");
}
}

class Program {
static void Main (string[] args) {
BaseClass bc = new BaseClass ();
DerivedClass dc = new DerivedClass ();
BaseClass bcdc = new DerivedClass ();

bc.Method1 ();
bc.Method2 ();
dc.Method1 ();
dc.Method2 ();
bcdc.Method1 ();
bcdc.Method2 ();
}
// Output:
// Base - Method1
// Base - Method2
// Base - Method1
// Derived - Method2
// Base - Method1
// Base - Method2
}

你将看到在 BaseClass 中添加 Method2 方法将引发警告。警告显示 DerivedClass 中的 Method2 方法隐藏了 BaseClass 中的 Method2 方法。如果希望获得该结果,则建议使用 Method2 定义中的 new 关键字。或者,可重命名 Method2 方法之一来消除警告。

示例:new 修饰符 有同名成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;

class BaseClass {
public void Method1 () {
Console.WriteLine ("Base - Method1");
}
public void Method2 () {
Console.WriteLine ("Base - Method2");
}
}

class DerivedClass : BaseClass {
public new void Method2 () {
Console.WriteLine ("Derived - Method2");
}
}

class Program {
static void Main (string[] args) {
BaseClass bc = new BaseClass ();
DerivedClass dc = new DerivedClass ();
BaseClass bcdc = new DerivedClass ();

bc.Method1 ();
bc.Method2 ();
dc.Method1 ();
dc.Method2 ();
bcdc.Method1 ();
bcdc.Method2 ();
}
// Output:
// Base - Method1
// Base - Method2
// Base - Method1
// Derived - Method2
// Base - Method1
// Base - Method2
}

输出结果与上例不使用 new 修饰符是一样的,只是不再有警告。就CIL来说,new修饰符对编译器生成的代码没有任何影响。从 C# 的角度看,它唯一的作用就是移除编译器警告。

示例:virtual override 修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using System;

class BaseClass {
public virtual void Method1 () {
Console.WriteLine ("Base - Method1");
}
public void Method2 () {
Console.WriteLine ("Base - Method2");
}
}

class DerivedClass : BaseClass {
public override void Method1 () {
Console.WriteLine ("Derived - Method1");
}
public new void Method2 () {
Console.WriteLine ("Derived - Method2");
}
}

class Program {
static void Main (string[] args) {
BaseClass bc = new BaseClass ();
DerivedClass dc = new DerivedClass ();
BaseClass bcdc = new DerivedClass ();

bc.Method1 ();
bc.Method2 ();
dc.Method1 ();
dc.Method2 ();
bcdc.Method1 ();
bcdc.Method2 ();
}
// Output:
// Base - Method1
// Base - Method2
// Derived - Method1
// Derived - Method2
// Derived - Method1
// Base - Method2
}

使用 override 修饰符可使 bcdc 访问 DerivedClass 中定义的 Method1 方法。通常,这是继承层次结构中所需的行为。让具有从派生类创建的值的对象使用派生类中定义的方法。可使用 override 扩展基类方法实现该行为。

sealed 修饰符

类使用 sealed 修饰符是禁止从该类继承。类似的,虚成员也可以密封。一般很少这样用,除非迫切需要这种限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A {
public virtual void Method () { }
}

class B : A {
public override sealed void Method () { }
}

class C : B {
// ERROR: Cannot override sealed members
//public override void Method()
//{
//}
}

base 成员

base 关键字用于从派生类中访问基类的成员:

  • 调用基类上已被其他方法重写的方法。Call a method on the base class that has been overridden by another method.

  • 指定创建派生类实例时应调用的基类构造函数。

在本例中,基类 Person 和派生类 Employee 都有一个名为 Getinfo 的方法通过使用 base 关键字,可以从派生类中调用基类的 Getinfo 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
using System;

public class Person {
protected string ssn = "444-55-6666";
protected string name = "John L. Malgraine";

public virtual void GetInfo () {
Console.WriteLine ("Name: {0}", name);
Console.WriteLine ("SSN: {0}", ssn);
}
}
class Employee : Person {
public string id = "ABC567EFG";
public override void GetInfo () {
// Calling the base class GetInfo method:
base.GetInfo ();
Console.WriteLine ("Employee ID: {0}", id);
}
}

class TestClass {
static void Main () {
Employee E = new Employee ();
E.GetInfo ();
}
}
/*
Output
Name: John L. Malgraine
SSN: 444-55-6666
Employee ID: ABC567EFG
*/

本示例显示如何指定在创建派生类实例时调用的基类构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;

public class BaseClass {
int num;

public BaseClass () {
Console.WriteLine ("in BaseClass()");
}

public BaseClass (int i) {
num = i;
Console.WriteLine ("in BaseClass(int i)");
}

public int GetNum () {
return num;
}
}

public class DerivedClass : BaseClass {
// This constructor will call BaseClass.BaseClass()
public DerivedClass () : base () { }

// This constructor will call BaseClass.BaseClass(int i)
public DerivedClass (int i) : base (i) { }

static void Main () {
DerivedClass md = new DerivedClass ();
DerivedClass md1 = new DerivedClass (1);
}
}
/*
Output:
in BaseClass()
in BaseClass(int i)
*/

abstract 抽象

使用 abstract 关键字可以创建不完整且必须在派生类中实现的类和 class 成员。

通过在类定义前面放置关键字 abstract,可以将类声明为抽象类。

  • 抽象类是仅供派生的类。
  • 抽象类无法实例化,只能实例化从它派生的类。
  • 抽象成员应当被重写,所以自动成为虚成员(但不能用virtual关键字显示声明)。
  • 抽象成员不能声明为私有,私有的话派生类看不到它们。

理解:

  • abstract 抽象:显式 强制要求所有派生类来提供实现。
  • override 替代:显式 强制重写基类成员,。
1
2
3
4
public abstract class A
{
// Class members here.
}

抽象类不能实例化。抽象类的用途是提供一个可供多个派生类共享的通用基类定义。例如,类库可以定义一个抽象类,将其用作多个类库函数的参数,并要求使用该库的程序员通过创建派生类来提供自己的类实现。

抽象类也可以定义抽象方法。方法是将关键字 abstract 添加到方法的返回类型的前面。

1
2
3
4
public abstract class A
{
public abstract void DoWork(int i);
}

例子:抽象类不能实例化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class PdaItem {
public PdaItem (string name) {
Name = name;
}

public virtual string Name { get; set; }
}

public class Program {
public static void Main () {
PdaItem item;
// ERROR: Cannot create an instance of the abstract class
//item = new PdaItem("Inigo Montoya"); //uncomment this line and it will not compile
}
}

例子:抽象成员。抽象成员没有实现的方法或者属性,其作用是强制所有派生类提供实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
using System;

// Define an abstract class
public abstract class PdaItem {
public PdaItem (string name) {
Name = name;
}

public virtual string Name { get; set; }
public abstract string GetSummary ();
}
public class Contact : PdaItem {
public Contact (string name) : base (name) { }

public override string Name {
get {
return FirstName + " " + LastName;
}

set {
string[] names = value.Split (' ');
// Error handling not shown.
FirstName = names[0];
LastName = names[1];
}
}

public string FirstName { get; set; }
public string LastName { get; set; }
public string Address { get; set; }

public override string GetSummary () {
return string.Format (
"FirstName: {0}\n" +
"LastName: {1}\n" +
"Address: {2}", FirstName, LastName, Address);
}

// ...
}

public class Appointment : PdaItem {
public Appointment (string name):
base (name) {
Name = name;
}

public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
public string Location { get; set; }

// ...
public override string GetSummary () {
return string.Format (
"Subject: {0}" + Environment.NewLine +
"Start: {1}" + Environment.NewLine +
"End: {2}" + Environment.NewLine +
"Location: {3}",
Name, StartDateTime, EndDateTime, Location);
}
}

抽象方法没有实现,所以方法定义后面是分号,而不是常规的方法块。抽象类的派生类必须实现所有抽象方法。当抽象类从基类继承虚方法时,抽象类可以使用抽象方法(abstract method )重写该虚方法。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// compile with: /target:library
public class D
{
public virtual void DoWork(int i)
{
// Original implementation.
}
}

public abstract class E : D
{
public abstract override void DoWork(int i);
}

public class F : E
{
public override void DoWork(int i)
{
// New implementation.
}
}

如果将 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 查找对象的运行时类型,并调用虚方法的重写方法。因此,你可以在源代码中调用基类的方法,但执行该方法的派生类版本。

虚方法允许你以统一方式处理多组相关的对象。例如,假定你有一个绘图应用程序,允许用户在绘图图面上创建各种形状。你在编译时不知道用户将创建哪些特定类型的形状。但应用程序必须跟踪创建的所有类型的形状,并且必须更新这些形状以响应用户鼠标操作。你可以使用多态性通过两个基本步骤解决这一问题:

  1. 创建一个类层次结构,其中每个特定形状类均派生自一个公共基类。
  2. 使用虚方法通过对基类方法的单个调用来调用任何派生类上的相应方法。

首先,创建一个名为 Rectangle Shape 的基类,并创建一些派生类,例如 Triangle Circle、 和 。为 Draw Shape 类提供一个名为 的虚方法,并在每个派生类中重写该方法以绘制该类表示的特定形状。创建一个 List<Shape> 对象,并向该对象添加 Circle、Triangle 和 Rectangle。若要更新绘图图面,请使用 foreach 循环对该列表进行循环访问,并对其中的每个 Shape 对象调用 Draw 方法。虽然列表中的每个对象都具有声明类型 Shape ,但调用的将是运行时类型(该方法在每个派生类中的重写版本)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using System;
using System.Collections.Generic;

public class Shape {
// A few example members
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }

// Virtual method
public virtual void Draw () {
Console.WriteLine ("Performing base class drawing tasks");
}
}

class Circle : Shape {
public override void Draw () {
// Code to draw a circle...
Console.WriteLine ("Drawing a circle");
base.Draw ();
}
}
class Rectangle : Shape {
public override void Draw () {
// Code to draw a rectangle...
Console.WriteLine ("Drawing a rectangle");
base.Draw ();
}
}
class Triangle : Shape {
public override void Draw () {
// Code to draw a triangle...
Console.WriteLine ("Drawing a triangle");
base.Draw ();
}
}

class Program {
static void Main (string[] args) {
// Polymorphism at work #1: a Rectangle, Triangle and Circle
// can all be used whereever a Shape is expected. No cast is
// required because an implicit conversion exists from a derived
// class to its base class.
var shapes = new List<Shape> {
new Rectangle (),
new Triangle (),
new Circle ()
};

// Polymorphism at work #2: the virtual method Draw is
// invoked on each of the derived classes, not the base class.
foreach (var shape in shapes) {
shape.Draw ();
}

// Keep the console open in debug mode.
Console.WriteLine ("Press any key to exit.");
Console.ReadKey ();
}

}

/* Output:
Drawing a rectangle
Performing base class drawing tasks
Drawing a triangle
Performing base class drawing tasks
Drawing a circle
Performing base class drawing tasks
*/

在 C# 中,每个类型都是多态的,因为包括用户定义类型在内的所有类型都继承自 Object

虚成员 Virtual Members

当派生类从基类继承时,它会获得基类的所有方法、字段、属性和事件。派生类的设计器可以选择是否:

  • 重写基类中的虚拟成员

  • 继承最接近的基类方法而不重写它

  • 定义隐藏基类实现的成员的新非虚实现

仅当基类成员声明为 virtualabstract 时,派生类才能重写基类成员。派生成员必须使用 override 关键字显式指示该方法将参与虚调用。以下代码提供了一个示例:The following code provides an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BaseClass
{
public virtual void DoWork() { }
public virtual int WorkProperty
{
get { return 0; }
}
}
public class DerivedClass : BaseClass
{
public override void DoWork() { }
public override int WorkProperty
{
get { return 0; }
}
}

字段不能是虚拟的,只有方法、属性、事件和索引器才可以是虚拟的。当派生类重写某个虚拟成员时,即使该派生类的实例被当作基类的实例访问,也会调用该成员。以下代码提供了一个示例:

1
2
3
4
5
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork(); // Also calls the new method.

虚方法和属性允许派生类扩展基类,而无需使用方法的基类实现。有关详细信息,请参阅使用 Override 和 New 关键字进行版本控制。接口提供另一种方式来定义将实现留给派生类的方法或方法集。有关详细信息,请参阅接口

使用新成员隐藏基类成员 Hiding Base Class Members with New Members

如果希望派生成员具有与基类中的成员相同的名称,但又不希望派生成员参与虚调用,则可以使用 new 关键字。new 关键字放置在要替换的类成员的返回类型之前。以下代码提供了一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BaseClass
{
public void DoWork() { WorkField++; }
public int WorkField;
public int WorkProperty
{
get { return 0; }
}
}

public class DerivedClass : BaseClass
{
public new void DoWork() { WorkField++; }
public new int WorkField;
public new int WorkProperty
{
get { return 0; }
}
}

通过将派生类的实例强制转换为基类的实例,仍然可以从客户端代码访问隐藏的基类成员。例如:

1
2
3
4
5
DerivedClass B = new DerivedClass();
B.DoWork(); // Calls the new method.

BaseClass A = (BaseClass)B;
A.DoWork(); // Calls the old method.

阻止派生类重写虚拟成员 Preventing Derived Classes from Overriding Virtual Members

无论在虚拟成员和最初声明虚拟成员的类之间已声明了多少个类,虚拟成员永远都是虚拟的。 如果类 A 声明了一个虚拟成员,类 B 从 A 派生,类 C 从类 B 派生,则类 C 继承该虚拟成员,并且可以选择重写它,而不管类 B 是否为该成员声明了重写。以下代码提供了一个示例:

1
2
3
4
5
6
7
8
public class A
{
public virtual void DoWork() { }
}
public class B : A
{
public override void DoWork() { }
}

派生类可以通过将重写声明为 sealed 来停止虚拟继承。这需要在类成员声明中的 overridesealed 关键字前面放置 关键字。以下代码提供了一个示例:

1
2
3
4
public class C : B
{
public sealed override void DoWork() { }
}

在上一示例中,方法 DoWork 对从 C 派生的任何类都不再是虚方法。它对 C 的实例仍是虚拟的,即使它们转换为类型 B 或类型 A。使用 new 关键字可以将密封方法替换为派生类,如下方示例所示:

1
2
3
4
public class D : C
{
public new void DoWork() { }
}

在此情况下,如果在 D 中使用类型为 D 的变量调用 DoWork,被调用的将是新的 。如果使用类型为 C、B 或 A 的变量访问 D 的实例,对 DoWork 的调用将遵循虚拟继承的规则,即把这些调用传送到类 C 的实现。

从派生类访问基类虚拟成员 Accessing Base Class Virtual Members from Derived Classes

已替换或重写某个方法或属性的派生类仍然可以使用基关键字访问基类的该方法或属性。以下代码提供了一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Base
{
public virtual void DoWork() {/*...*/ }
}
public class Derived : Base
{
public override void DoWork()
{
//Perform Derived's work here
//...
// Call DoWork on base class
base.DoWork();
}
}

有关详细信息,请参阅 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 是计算结果为某个类型的实例的表达式,而 typeexpr 结果要转换到的类型的名称。如果 expr 非空,并且通过计算表达式得出的对象可转换为 type,则 is 语句为 true;否则返回 false

例如,以下代码确定 obj 是否可转换为 Person 类型的实例:

1
2
3
if (obj is Person) {
// Do something if obj is a Person.
}

如果满足以下条件,则 is 语句为 true:

  • expr 是与 type 具有相同类型的一个实例。

  • expr 是派生自 type 的类型的一个实例。换言之,expr 结果可以向上转换为 type 的一个实例。

  • expr 具有属于 type 的一个基类的编译时类型,expr 还具有属于 type 或派生自 type 的运行时类型。变量的编译时类型是其声明中定义的变量类型。变量的运行时类型是分配给该变量的实例类型。

  • expr 是实现 type 接口的类型的一个实例。

下例表明,对于所有这些转换,is 表达式的计算结果都为 true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;

public class Class1 : IFormatProvider
{
public object GetFormat(Type t)
{
if (t.Equals(this.GetType()))
return this;
return null;
}
}

public class Class2 : Class1
{
public int Value { get; set; }
}

public class Example
{
public static void Main()
{
var cl1 = new Class1();
Console.WriteLine(cl1 is IFormatProvider);
Console.WriteLine(cl1 is Object);
Console.WriteLine(cl1 is Class1);
Console.WriteLine(cl1 is Class2);
Console.WriteLine();

var cl2 = new Class2();
Console.WriteLine(cl2 is IFormatProvider);
Console.WriteLine(cl2 is Class2);
Console.WriteLine(cl2 is Class1);
Console.WriteLine();

Class1 cl = cl2;
Console.WriteLine(cl is Class1);
Console.WriteLine(cl is Class2);
}
}
// The example displays the following output:
// True
// True
// True
// False
//
// True
// True
// True
//
// True
// True

从 C# 7 开始,可以使用类型模式的模式匹配来编写代码,代码使用 is 语句更为简洁。

利用 is 的模式匹配 Pattern matching with is

从 C# 7 开始,isswitch 语句支持模式匹配。is 关键字支持以下模式:

  • 类型模式,用于测试表达式是否可转换为指定类型,如果可以,则将其转换为该类型的一个变量。

  • 常量模式,用于测试表达式计算结果是否为指定的常数值。[Constant pattern]

  • var 模式,始终成功的匹配,可将表达式的值绑定到新局部变量。[var pattern]

类型模式 Type pattern

使用类型模式执行模式匹配时,is 会测试表达式是否可转换为指定类型,如果可以,则将其转换为该类型的一个变量。它是 is 语句的直接扩展,可执行简单的类型计算和转换。is 类型模式的一般形式为:

1
expr is type varname

其中 expr 是计算结果为某个类型的实例的表达式,typeexpr 结果要转换到的类型的名称,varnameexpr 结果要转换到的对象(如果 is 测试为 true)。

如果以下任一条件成立,则 is 表达式为 true

  • expr 是与 type 具有相同类型的一个实例。

  • expr 是派生自 type 的类型的一个实例。换言之,expr 结果可以向上转换为 type 的一个实例。

  • expr 具有属于 type 的一个基类的编译时类型,expr 还具有属于 type 或派生自 type 的运行时类型。变量的编译时类型是其声明中定义的变量类型。变量的运行时类型是分配给该变量的实例类型。

  • expr 是实现 type 接口的类型的一个实例。

如果 exptrue,且 isif 语句一起使用,则会分配 varname,并且其仅在 if 语句中具有局部范围。

下列示例使用 is 类型模式为类型的 IComparable.CompareTo(Object) 方法提供实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;

public class Employee : IComparable
{
public String Name { get; set; }
public int Id { get; set; }

public int CompareTo(Object o)
{
if (o is Employee e)
{
return Name.CompareTo(e.Name);
}
throw new ArgumentException("o is not an Employee object.");
}
}

如果没有模式匹配,则可能按以下方式编写此代码。使用类型模式匹配无需测试转换结果是否为 null,从而生成更紧凑易读的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System;

public class Employee : IComparable
{
public String Name { get; set; }
public int Id { get; set; }

public int CompareTo(Object o)
{
var e = o as Employee;
if (e == null)
{
throw new ArgumentException("o is not an Employee object.");
}
return Name.CompareTo(e.Name);
}
}

常量模式 Constant pattern

使用常量模式执行模式匹配时,is 会测试表达式结果是否等于指定常量。在 C# 6 和更低版本中,switch 语句支持常量模式从。C# 7 开始,is 语句也支持常量模式。语法为:

1
expr is constant

其中 expr 是要计算的表达式,constant 是要测试的值。 constant 可以是以下任何常数表达式:

  • 一个文本值。A literal value.

  • 已声明 const 变量的名称。The name of a declared const variable.

  • 一个枚举常量。An enumeration constant.

常数表达式的计算方式如下:

  • 如果 exprconstant 均为整型类型,则 C# 相等运算符确定表示式是否返回 true(即,是否为 expr == constant)。

  • 否则,由对静态 Object.Equals(expr, constant) 方法的调用来确定表达式的值。

下例同时使用了类型模式和常量模式来测试对象是否为 Dice 实例,如果是,则确定骰子的值是否为 6。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

public class Dice
{
Random rnd = new Random();
public Dice()
{

}
public int Roll()
{
return rnd.Next(1, 7);
}
}

class Program
{
static void Main(string[] args)
{
var d1 = new Dice();
ShowValue(d1);
}

private static void ShowValue(object o)
{
const int HIGH_ROLL = 6;

if (o is Dice d && d.Roll() is HIGH_ROLL)
Console.WriteLine($"The value is {HIGH_ROLL}!");
else
Console.WriteLine($"The dice roll is not a {HIGH_ROLL}!");
}
}
// The example displays output like the following:
// The value is 6!

var 模式 var pattern

具有 var 模式的模式匹配始终成功。

1
expr is var varname

其中,expr 的值始终分配给名为 varname 的局部变量。varname 是一个与 expr 具有相同类型的静态变量。下例使用 var 模式向名为 obj 的变量分配表达式。然后,显示 obj 的值和类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;

class Program
{
static void Main()
{
object[] items = { new Book("The Tempest"), new Person("John") };
foreach (var item in items) {
if (item is var obj)
Console.WriteLine($"Type: {obj.GetType().Name}, Value: {obj}");
}
}
}

class Book
{
public Book(string title)
{
Title = title;
}

public string Title { get; set; }

public override string ToString()
{
return Title;
}
}

class Person
{
public Person(string name)
{
Name = name;
}

public string Name
{ get; set; }

public override string ToString()
{
return Name;
}
}
// The example displays the following output:
// Type: Book, Value: The Tempest
// Type: Person, Value: John

请注意,如果 exprnull,则 is 表达式仍为 true 并向 varname 分配 null

as

可以使用 as 运算符在符合的引用类型或可以为 null 的类型之间执行某些类型的转换。以下代码显示一个示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

class csrefKeywordsOperators {
class Base {
public override string ToString () {
return "Base";
}
}
class Derived : Base { }

class Program {
static void Main () {

Derived d = new Derived ();

Base b = d as Base;
if (b != null) {
Console.WriteLine (b.ToString ());
}

}
}
}

as 运算符类似于转换运算。但是,如果无法进行转换,则 as 会返回 null,而不是引发异常。请看下面的示例:

1
expression as type

该代码等效于以下表达式,但 expression 变量仅进行一次计算。

1
expression is type ? (type)expression : (type)null

请注意,as 运算符仅执行引用转换、可以为 null 的转换和装箱转换。as 运算符无法执行其他转换,例如用户定义的转换,应使用转换表达式执行此转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

class ClassA { }
class ClassB { }

class MainClass {
static void Main () {
object[] objArray = new object[6];
objArray[0] = new ClassA ();
objArray[1] = new ClassB ();
objArray[2] = "hello";
objArray[3] = 123;
objArray[4] = 123.4;
objArray[5] = null;

for (int i = 0; i < objArray.Length; ++i) {
string s = objArray[i] as string;
Console.Write ("{0}:", i);
if (s != null) {
Console.WriteLine ("'" + s + "'");
} else {
Console.WriteLine ("not a string");
}
}
}
}
/*
Output:
0:not a string
1:not a string
2:'hello'
3:not a string
4:not a string
5:not a string
*/

结尾

0%