继承

在 C# 中,继承是面向对象编程的一个核心概念,它允许一个类(子类或派生类)继承另一个类(父类或基类)的属性和方法,从而实现代码的复用和扩展。以下从多个方面详细介绍 C# 中的继承:

基本概念

继承建立了类与类之间的一种 “is - a” 关系,即子类是父类的一种特殊类型。通过继承,子类可以拥有父类的成员(字段、属性、方法等),并且可以添加自己特有的成员或重写父类的成员以实现不同的行为。

语法示例

以下是一个简单的继承示例,展示了如何定义父类和子类:

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
// 定义一个父类 Animal
class Animal
{
public string Name { get; set; }

public Animal(string name)
{
Name = name;
}

public void Eat()
{
Console.WriteLine($"{Name} is eating.");
}
}

// 定义一个子类 Dog,继承自 Animal
class Dog : Animal
{
public Dog(string name) : base(name)
{
}

public void Bark()
{
Console.WriteLine($"{Name} is barking.");
}
}

class Program
{
static void Main()
{
Dog dog = new Dog("Buddy");
dog.Eat();
dog.Bark();
}
}

在上述代码中:

Animal 是父类,包含一个属性 Name 和一个方法 Eat。
Dog 是子类,使用 : 符号表示继承自 Animal 类。子类 Dog 不仅继承了父类的 Name 属性和 Eat 方法,还添加了自己特有的方法 Bark。
在 Dog 类的构造函数中,使用 base(name) 调用父类的构造函数来初始化从父类继承的属性。

继承的特性

单继承

C# 只支持单继承,即一个类只能直接继承自一个父类。但可以通过接口实现多继承的效果,一个类可以实现多个接口。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 接口定义
interface ICanFly
{
void Fly();
}

interface ICanSwim
{
void Swim();
}

// 类实现多个接口
class Duck : ICanFly, ICanSwim
{
public void Fly()
{
Console.WriteLine("The duck is flying.");
}

public void Swim()
{
Console.WriteLine("The duck is swimming.");
}
}

访问修饰符与继承

父类的 public 成员可以在子类中直接访问。
父类的 protected 成员只能在父类本身、子类和同一个程序集内的其他类中访问。例如:

1
2
3
4
5
6
7
8
9
10
11
12
class Parent
{
protected int protectedField = 10;
}

class Child : Parent
{
public void PrintProtectedField()
{
Console.WriteLine(protectedField);
}
}

父类的 private 成员不能被子类直接访问。

方法重写(Override)

子类可以重写父类的虚方法(使用 virtual 关键字修饰的方法),以实现不同的行为。重写时需要使用 override 关键字。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing a shape.");
}
}

class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("Drawing a circle.");
}
}

继承的优点

代码复用:避免了重复编写相同的代码,提高了开发效率。例如,多个不同类型的动物类都可以继承自 Animal 类,复用 Eat 方法。

可维护性:如果需要修改父类的某个通用功能,只需要在父类中修改一次,所有子类都会受到影响,减少了代码的维护成本。

扩展性:子类可以在继承父类的基础上添加新的功能,实现对现有代码的扩展。


base关键字

调用父类的构造函数

在子类的构造函数中,可以使用 base 关键字来调用父类的构造函数,以完成父类成员的初始化。这在子类需要继承父类的某些初始化逻辑时非常有用。

示例代码

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
// 定义父类
class Person
{
protected string name;
protected int age;

// 父类的构造函数
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
}

// 定义子类
class Student : Person
{
private string studentId;

// 子类的构造函数,使用 base 调用父类的构造函数
public Student(string name, int age, string studentId) : base(name, age)
{
this.studentId = studentId;
}
}

在 Student 类的构造函数中,base(name, age) 调用了父类 Person 的构造函数,将 name 和 age 参数传递给父类构造函数进行初始化。这样可以避免在子类中重复编写父类成员的初始化代码。

访问父类的方法和属性

当子类中重写了父类的方法或属性时,可以使用 base 关键字来访问父类的原始实现。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义父类
class Shape
{
public virtual void Draw()
{
Console.WriteLine("Drawing a shape.");
}
}

// 定义子类
class Circle : Shape
{
public override void Draw()
{
// 使用 base 调用父类的 Draw 方法
base.Draw();
Console.WriteLine("Drawing a circle.");
}
}

在 Circle 类中重写了 Draw 方法。通过 base.Draw(),可以先调用父类 Shape 的 Draw 方法,然后再执行子类特有的绘制逻辑。这使得子类可以在父类的基础上进行功能扩展。

注意事项

访问权限:base 只能访问父类中可访问的成员。即父类的 public 和 protected 成员可以通过 base 访问,而 private 成员不能通过 base 访问。
调用时机:在子类构造函数中使用 base 调用父类构造函数时,base 语句必须是构造函数中的第一条语句。

总结

base 关键字为子类提供了一种方便的方式来与父类进行交互。它可以帮助我们在子类中复用父类的构造逻辑,并且在重写方法时能够访问父类的原始实现,从而实现代码的复用和功能的扩展。