类的继承

可以继承基类和接口

  1. class MyDerivedClass: MyBaseClass
  2. {
  3. // members
  4. }

如果类(或结构)也派生自接口,则用逗号分隔列表中的基类和接口:

  1. public class MyDerivedClass: MyBaseClass, IInterface1, IInterface2
  2. {
  3. // members
  4. }

结构的继承

只能用于接口继承

  1. public struct MyDerivedStruct: IInterface1, IInterface2
  2. {
  3. // members
  4. }

案例

定义了基类Shape。
无论是矩形还是椭圆,形状都有一些共同点:形状都有位置和大小。
定义相应的类时,位置和大小应包含在Shape类中。
Shape类定义了只读属性Position和Shape,它们使用自动属性初始化器来初始化。

  1. public class Position
  2. {
  3. public int X { get; set; }
  4. public int Y { get; set; }
  5. }
  6. public class Size
  7. {
  8. public int Width { get; set; }
  9. public int Height { get; set; }
  10. }
  11. public class Shape
  12. {
  13. public Position Position { get; } = new Position();
  14. public Size Size { get; } = new Size();
  15. }

虚方法

把一个基类方法声明为virtual,就可以在任何派生类中重写该方法:

普通方法的虚方法

  1. public class Shape
  2. {
  3. public virtual void Draw()
  4. {
  5. WriteLine($"Shape with {Position} and {Size}");
  6. }
  7. }

代码只有一行时

如果实现代码只有一行,在C# 6中,也可以把virtual关键字和表达式体的方法(使用lambda运算符)一起使用。

这个语法可以独立于修饰符,单独使用:

  1. public class Shape
  2. {
  3. public virtual void Draw() => WriteLine($"Shape with {Position} and {Size}");
  4. }

属性的虚方法

  1. private Size _size;
  2. public virtual Size Size
  3. {
  4. get
  5. {
  6. return _size;
  7. }
  8. set
  9. {
  10. _size = value;
  11. }
  12. }

方法的重写

C#要求在派生类的函数重写另一个函数时,
使用override关键字显式声明

  1. public class Rectangle : Shape
  2. {
  3. public override void Draw() =>WriteLine($"Rectangle with {Position} and {Size}");
  4. }

多态性

使用多态性,可以动态地定义调用的方法,而不是在编译期间定义。
编译器创建一个虚拟方法表(vtable),其中列出了可以在运行期间调用的方法,它根据运行期间的类型调用方法。

  1. //DrawShape()方法接收一个Shape参数,并调用Shape类的Draw()方法
  2. public static void DrawShape(Shape shape)
  3. {
  4. shape.Draw();
  5. }

使用之前创建的矩形调用方法。
尽管方法声明为接收一个Shape对象,但任何派生Shape的类型(包括Rectangle)都可以传递给这个方法。

隐藏方法

如果签名相同的方法在基类和派生类中都进行了声明,但该方法没有分别声明为virtual和override,派生类方法就会隐藏基类方法。

在大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会造成对于给定类的实例调用错误方法的危险。

  1. public class Shape//基类
  2. {
  3. // various members
  4. }
  1. public class Ellipse: Shape//派生类
  2. {
  3. public void MoveBy(int x, int y)
  4. {
  5. Position.X += x;
  6. Position.Y += y;
  7. }
  8. }

当派生类定义一个基类中不存在的方法时
基类在后面也写了一个与派生类相同的方法,但没有定义是虚方法。

编译Ellipse类会生成一个编译警告,提醒使用new关键词隐藏方法。在实践中,不使用new关键字会得到相同的编译结果,但避免出现编译器警告:

  1. public class Ellipse: Shape
  2. {
  3. new public void Move(Position newPosition)
  4. {
  5. Position.X = newPosition.X;
  6. Position.Y = newPosition.Y;
  7. }
  8. //. . . other members
  9. }

不使用new关键字,也可以重命名方法,或者,如果基类的方法声明为virtual,且用作相同的目的,就重写它。
然而,如果其他方法已经调用了此方法,简单的重命名会破坏其他代码。

调用方法的基类版本

C#有一种特殊的语法用于从派生类中调用方法的基类版本

例如,派生类Shape声明了Move()方法,想要在派生类Rectangle中调用它,以使用基类的实现代码。

:::danger base.<MethodName>() :::

  1. public class Shape
  2. {
  3. public virtual void Move(Position newPosition)
  4. {
  5. Position.X = newPosition.X;
  6. Position.Y = newPosition.Y;
  7. WriteLine($"moves to {Position}");
  8. }
  9. //. . . other members
  10. }

Move()方法在Rectangle类中重写,把Rectangle一词添加到控制台。

  1. public class Rectangle: Shape
  2. {
  3. public override void Move(Position newPosition)
  4. {
  5. Write("Rectangle ");
  6. base.Move(newPosition);
  7. }
  8. //. . . other members
  9. }

抽象类和抽象方法

C#允许把类和方法声明为abstract。

:::info 抽象类不能实例化,而抽象方法不能直接实现,必须在非抽象的派生类中重写。 :::

  1. public abstract class Shape
  2. {
  3. public abstract void Resize(int width, int height); // abstract method
  4. }

从抽象基类中派生类型时,需要实现所有抽象成员。否则,编译器会报错:

  1. public class Ellipse : Shape
  2. {
  3. public override void Resize(int width, int height)
  4. {
  5. Size.Width = width;
  6. Size.Height = height;
  7. }
  8. }

密封类和密封方法

如果不应创建派生自某个自定义类的类,该自定义类就应密封。

  • 给类添加sealed修饰符,就不允许创建该类的子类。
  • 密封一个方法,表示不能重写该方法。
  1. sealed class FinalClass
  2. {
  3. // etc
  4. }
  5. class DerivedClass: FinalClass // wrong. Cannot derive from sealed class.
  6. {
  7. // etc
  8. }

派生类的构造函数

  • 创建派生类的实例时,实际上会有多个构造函数起作用。
  • 要实例化的类的构造函数本身不能初始化类,还必须调用基类中的构造函数。
  1. public class Shape
  2. {
  3. public Position Position { get; } = new Position();
  4. public Size Size { get; } = new Size();
  5. }

在幕后,编译器会给类创建一个默认的构造函数,把属性初始化器放在这个构造函数中:

  1. public class Shape
  2. {
  3. public Shape()
  4. {
  5. Position = new Position();
  6. Size = new Size();
  7. }
  8. public Position Position { get; };
  9. public Size Size { get; };
  10. }

构造函数总是按照层次结构的顺序调用:

  1. 先调用System.Object类的构造函数
  2. 再按照层次结构由上向下进行
  3. 直到到达编译器要实例化的类为止。
  1. public abstract class Shape
  2. {
  3. public Shape(int width, int height, int x, int y)
  4. {
  5. Size = new Size { Width = width, Height = height };
  6. Position = new Position { X = x, Y = y };
  7. }
  8. public Position Position { get; }
  9. public Size Size { get; }
  10. }

当删除默认构造函数,重新编译程序时,不能编译Ellipse和Rectangle类,因为编译器不知道应该把什么值传递给基类唯一的非默认值构造函数。

  1. public Rectangle(int width, int height, int x, int y)
  2. : base(width, height, x, y)
  3. {
  4. }

把初始化代码放在构造函数块内太迟了,因为基类的构造函数在派生类的构造函数之前调用。这就是为什么在构造函数块之前声明了一个构造函数初始化器。