C#程序设计基础 - 面向对象之“继承”
1. 继承概述
1.1 定义与作用
继承是面向对象编程中一个核心的概念,它允许一个类(称为子类或派生类)继承另一个类(称为父类或基类)的属性和方法。在C#中,继承通过关键字:来实现,子类可以继承父类的所有非私有成员,包括字段、属性、方法和索引器等。
继承的主要作用是实现代码的复用,通过继承,子类可以继承父类的已有功能,并且可以对其进行扩展或修改,从而避免重复编写相同的代码。例如,假设有一个Animal类,它包含一些基本的属性和方法,如Name、Age和MakeSound。如果要创建一个Dog类和一个Cat类,它们都具有Animal类的特征,那么就可以让Dog类和Cat类继承Animal类,这样Dog类和Cat类就自动拥有了Name、Age和MakeSound等属性和方法,而无需重新定义这些成员。
继承还可以实现多态,多态是指同一个操作作用于不同的对象时,可以有不同的解释和不同的执行结果。在C#中,通过继承和虚方法(virtual)或抽象方法(abstract),可以实现多态。例如,Animal类中的MakeSound方法可以被声明为虚方法,然后在Dog类和Cat类中分别重写(override)该方法,这样就可以实现多态。当调用MakeSound方法时,根据对象的实际类型,会调用相应类中的MakeSound方法。
2. 基类与派生类
2.1 基类定义
在C#中,基类是被继承的类,它定义了一组通用的属性和方法,这些成员可以被一个或多个派生类继承。基类的设计应该具有通用性和可扩展性,以便能够满足不同派生类的需求。
例如,定义一个Animal基类,它包含所有动物共有的属性和行为。代码如下:
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public Animal(string name, int age)
{
Name = name;
Age = age;
}
public virtual void MakeSound()
{
Console.WriteLine("This animal makes a sound.");
}
}
-
Animal类有两个属性:Name和Age,分别表示动物的名称和年龄。 -
构造函数
Animal(string name, int age)用于初始化这些属性。 -
MakeSound方法被声明为virtual,表示这是一个可以被派生类重写的虚方法,用于实现多态。
2.2 派生类声明
派生类是通过继承基类而创建的类,它继承了基类的所有非私有成员,并且可以添加新的成员或重写继承来的成员。在C#中,派生类的声明使用:关键字,后面跟上基类的名称。
例如,定义一个Dog派生类,它继承自Animal基类,并且添加了一些特定于狗的属性和行为。代码如下:
public class Dog : Animal
{
public string Breed { get; set; } // 狗的品种
public Dog(string name, int age, string breed) : base(name, age)
{
Breed = breed;
}
public override void MakeSound()
{
Console.WriteLine("The dog barks.");
}
}
-
Dog类继承自Animal类,使用:关键字。 -
Dog类添加了一个新的属性Breed,表示狗的品种。 -
构造函数
Dog(string name, int age, string breed)调用了基类的构造函数base(name, age),用于初始化继承来的Name和Age属性。 -
MakeSound方法被重写为override,实现了多态。当调用Dog对象的MakeSound方法时,会输出狗的叫声,而不是基类中的默认实现。
通过这种方式,派生类可以继承基类的通用功能,并且可以根据自己的需求进行扩展和定制。
3. 构造函数与继承
3.1 基类构造函数调用
在C#中,当派生类被实例化时,基类的构造函数会首先被调用,以确保基类的成员被正确初始化。这是通过在派生类的构造函数中使用base关键字来实现的。base关键字用于调用基类的构造函数,确保基类的属性和状态被正确设置。
例如,在前面的Dog类中,构造函数Dog(string name, int age, string breed)通过base(name, age)调用了基类Animal的构造函数,将name和age参数传递给基类的构造函数,从而初始化了继承来的Name和Age属性。
public class Dog : Animal
{
public string Breed { get; set; }
public Dog(string name, int age, string breed) : base(name, age)
{
Breed = breed;
}
public override void MakeSound()
{
Console.WriteLine("The dog barks.");
}
}
-
base(name, age):调用基类Animal的构造函数,初始化Name和Age属性。 -
Breed = breed:初始化派生类特有的属性Breed。
如果没有显式调用基类的构造函数,C#编译器会自动调用基类的无参数构造函数(如果存在)。但如果基类没有无参数构造函数,或者需要传递参数来初始化基类的成员,就必须显式调用基类的构造函数。
3.2 派生类构造函数实现
派生类的构造函数不仅要初始化自己的成员,还需要确保基类的成员被正确初始化。这通常是通过在派生类的构造函数中调用基类的构造函数来实现的。派生类的构造函数可以有自己的参数列表,并且可以添加自己的初始化逻辑。
例如,假设有一个Cat类,它继承自Animal类,并且添加了一个特定于猫的属性Color。Cat类的构造函数需要初始化Color属性,并且调用基类的构造函数来初始化Name和Age属性。
public class Cat : Animal
{
public string Color { get; set; }
public Cat(string name, int age, string color) : base(name, age)
{
Color = color;
}
public override void MakeSound()
{
Console.WriteLine("The cat meows.");
}
}
-
Cat(string name, int age, string color):派生类的构造函数,包含自己的参数列表。 -
base(name, age):调用基类的构造函数,初始化继承来的Name和Age属性。 -
Color = color:初始化派生类特有的属性Color。
派生类的构造函数可以有多个重载版本,以支持不同的初始化需求。例如,可以为Cat类添加一个无参数构造函数,提供默认值:
public class Cat : Animal
{
public string Color { get; set; }
public Cat() : base("Unknown", 0)
{
Color = "Unknown";
}
public Cat(string name, int age, string color) : base(name, age)
{
Color = color;
}
public override void MakeSound()
{
Console.WriteLine("The cat meows.");
}
}
-
Cat():无参数构造函数,调用基类的构造函数时提供默认值"Unknown"和0。 -
Cat(string name, int age, string color):带参数的构造函数,调用基类的构造函数时传递具体的参数。
通过这种方式,派生类的构造函数可以灵活地初始化自己的成员,同时确保基类的成员被正确初始化。
4. 方法重写与多态
4.1 方法重写语法
在C#中,方法重写是继承机制中的一个重要特性,它允许派生类根据自己的需求重新实现从基类继承来的方法。方法重写的关键字是override,它必须与基类中被声明为virtual或abstract的方法相对应。
以下是一个方法重写的示例代码:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("This animal makes a sound.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("The dog barks.");
}
}
-
基类
Animal中的MakeSound方法被声明为virtual,表示这是一个可以被派生类重写的虚方法。 -
派生类
Dog通过override关键字重写了MakeSound方法,提供了自己的实现。
方法重写需要满足以下规则:
-
派生类中的方法必须与基类中被重写的方法具有相同的方法签名,包括方法名、参数列表和返回类型。
-
派生类中的方法必须使用
override关键字来明确表示这是一个重写的方法。 -
基类中的方法必须被声明为
virtual或abstract,才能被派生类重写。 -
如果基类中的方法被声明为
sealed,那么它不能被派生类重写。
4.2 多态实现
多态是指同一个操作作用于不同的对象时,可以有不同的解释和不同的执行结果。在C#中,通过继承和方法重写可以实现多态。
以下是一个多态的示例代码:
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("This animal makes a sound.");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("The dog barks.");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("The cat meows.");
}
}
-
基类
Animal中的MakeSound方法被声明为virtual,表示这是一个可以被派生类重写的虚方法。 -
派生类
Dog和Cat分别重写了MakeSound方法,提供了自己的实现。
在运行时,根据对象的实际类型,会调用相应类中的MakeSound方法。例如:
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.MakeSound(); // 输出:The dog barks.
animal2.MakeSound(); // 输出:The cat meows.
-
animal1和animal2都是Animal类型的引用,但它们实际指向的对象分别是Dog和Cat。 -
当调用
MakeSound方法时,根据对象的实际类型,会调用相应类中的MakeSound方法,这就是多态的实现。
多态的优点包括:
-
提高代码的可扩展性和可维护性,可以通过添加新的派生类来扩展功能,而无需修改现有的代码。
-
提高代码的复用性,可以使用基类的引用指向派生类的对象,从而实现代码的通用性。
-
提高代码的灵活性,可以根据不同的需求动态地选择对象的行为。
5. 继承中的访问修饰符
5.1 public成员
在C#中,public访问修饰符表示成员是公开的,可以在任何地方被访问。当一个成员被声明为public时,它可以在基类和派生类中被自由访问。
例如,基类Animal中的Name和Age属性被声明为public:
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
}
派生类Dog可以访问这些public成员,并且可以对它们进行操作:
public class Dog : Animal
{
public void PrintInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
-
Name和Age属性在派生类Dog中可以被直接访问。 -
派生类可以对这些
public成员进行读取和修改操作。
public成员的优点是访问性高,可以在任何地方被访问和使用。但这也可能导致一些问题,例如外部代码可能会意外地修改这些成员的值,从而影响程序的稳定性和安全性。
5.2 protected成员
protected访问修饰符表示成员只能在基类和派生类中被访问,不能被外部代码访问。这为类的成员提供了一定程度的封装性,同时允许派生类访问基类的成员。
例如,基类Animal中定义了一个protected方法InternalSound:
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
protected void InternalSound()
{
Console.WriteLine("This is an internal sound.");
}
}
派生类Dog可以访问这个protected方法:
public class Dog : Animal
{
public void MakeSound()
{
InternalSound();
Console.WriteLine("The dog barks.");
}
}
-
InternalSound方法在派生类Dog中可以被访问和调用。 -
但这个方法不能被外部代码直接访问,只能在基类和派生类中使用。
protected成员的优点是提供了更好的封装性,防止外部代码直接访问类的内部实现细节,同时允许派生类继承和使用这些成员。这有助于维护代码的结构和安全性,同时保持类的可扩展性。
6. 继承的限制与规则
6.1 单继承限制
在C#中,继承是单继承的,这意味着一个类只能继承自一个基类。例如,不能同时让一个类继承自Animal和Vehicle两个基类。这种单继承的设计简化了语言的复杂性,避免了多重继承可能导致的歧义和冲突,如菱形继承问题。
尽管C#不支持多重继承,但它提供了接口(interface)来弥补这一限制。接口允许一个类实现多个接口,从而实现类似多重继承的功能。接口定义了一组方法和属性的契约,但不提供具体实现。派生类必须实现接口中定义的所有成员。
例如,定义一个IMovable接口:
public interface IMovable
{
void Move();
}
然后让Dog类实现这个接口:
public class Dog : Animal, IMovable
{
public string Breed { get; set; }
public Dog(string name, int age, string breed) : base(name, age)
{
Breed = breed;
}
public override void MakeSound()
{
Console.WriteLine("The dog barks.");
}
public void Move()
{
Console.WriteLine("The dog runs.");
}
}
-
Dog类继承自Animal类,并且实现了IMovable接口。 -
Move方法是IMovable接口的实现,提供了狗的移动行为。
通过接口,C#在单继承的基础上提供了灵活的扩展能力,允许一个类同时具备多个接口定义的行为,而不会引入多重继承的复杂性。
6.2 构造函数与析构函数
构造函数
在继承中,构造函数的调用顺序是先调用基类的构造函数,再调用派生类的构造函数。这是为了确保基类的成员在派生类初始化之前已经被正确初始化。派生类的构造函数通过base关键字显式调用基类的构造函数。
例如:
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public Animal(string name, int age)
{
Console.WriteLine("Animal constructor called.");
Name = name;
Age = age;
}
}
public class Dog : Animal
{
public string Breed { get; set; }
public Dog(string name, int age, string breed) : base(name, age)
{
Console.WriteLine("Dog constructor called.");
Breed = breed;
}
}
-
当创建一个
Dog对象时,首先调用Animal的构造函数,输出Animal constructor called.。 -
然后调用
Dog的构造函数,输出Dog constructor called.。
如果没有显式调用基类的构造函数,C#编译器会尝试调用基类的无参数构造函数。如果基类没有无参数构造函数,编译器会报错。
析构函数
析构函数用于在对象被销毁时释放资源。在继承中,析构函数的调用顺序与构造函数相反,即先调用派生类的析构函数,再调用基类的析构函数。这是因为派生类可能在基类的基础上添加了额外的资源,需要先释放这些资源,再释放基类的资源。
例如:
public class Animal
{
public Animal()
{
Console.WriteLine("Animal constructor called.");
}
~Animal()
{
Console.WriteLine("Animal destructor called.");
}
}
public class Dog : Animal
{
public Dog()
{
Console.WriteLine("Dog constructor called.");
}
~Dog()
{
Console.WriteLine("Dog destructor called.");
}
}
-
当创建一个
Dog对象时,首先调用Animal的构造函数,输出Animal constructor called.。 -
然后调用
Dog的构造函数,输出Dog constructor called.。 -
当
Dog对象被销毁时,首先调用Dog的析构函数,输出Dog destructor called.。 -
然后调用
Animal的析构函数,输出Animal destructor called.。
析构函数的调用顺序确保了派生类和基类的资源都被正确释放,避免了资源泄漏。
7. 继承的实践案例
7.1 场景描述
在面向对象编程中,继承是实现代码复用和扩展功能的重要手段。本节将通过一个实际案例来展示继承在C#程序设计中的应用。假设我们需要开发一个简单的图形绘制程序,程序中包含多种图形,如圆形、矩形和三角形等。每种图形都有自己的属性和行为,但它们也有一些共同的特征,例如都有面积和周长等属性,都可以被绘制和移动等行为。因此,我们可以设计一个基类Shape,它定义了所有图形共有的属性和行为,然后让每种具体的图形类继承自Shape类,并根据自己的特点实现具体的行为。
7.2 代码实现
首先,我们定义一个基类Shape,它包含所有图形共有的属性和行为:
public abstract class Shape
{
public string Color { get; set; }
public Shape(string color)
{
Color = color;
}
public abstract double GetArea();
public abstract double GetPerimeter();
public abstract void Draw();
public abstract void Move();
}
-
Shape类是一个抽象类,使用abstract关键字声明。抽象类不能被实例化,但可以被继承。 -
Color属性表示图形的颜色,是所有图形共有的属性。 -
GetArea、GetPerimeter、Draw和Move方法被声明为抽象方法,使用abstract关键字。抽象方法没有具体的实现,派生类必须提供这些方法的具体实现。
接下来,我们定义一个Circle类,它继承自Shape类,并实现了圆形特有的属性和行为:
public class Circle : Shape
{
public double Radius { get; set; }
public Circle(string color, double radius) : base(color)
{
Radius = radius;
}
public override double GetArea()
{
return Math.PI * Radius * Radius;
}
public override double GetPerimeter()
{
return 2 * Math.PI * Radius;
}
public override void Draw()
{
Console.WriteLine($"Drawing a circle with radius {Radius} and color {Color}.");
}
public override void Move()
{
Console.WriteLine("Moving the circle.");
}
}
-
Circle类继承自Shape类,使用:关键字。 -
Circle类添加了一个新的属性Radius,表示圆的半径。 -
构造函数
Circle(string color, double radius)调用了基类的构造函数base(color),用于初始化继承来的Color属性。 -
GetArea、GetPerimeter、Draw和Move方法被重写为override,提供了圆形的具体实现。
同样地,我们定义一个Rectangle类,它继承自Shape类,并实现了矩形特有的属性和行为:
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(string color, double width, double height) : base(color)
{
Width = width;
Height = height;
}
public override double GetArea()
{
return Width * Height;
}
public override double GetPerimeter()
{
return 2 * (Width + Height);
}
public override void Draw()
{
Console.WriteLine($"Drawing a rectangle with width {Width}, height {Height} and color {Color}.");
}
public override void Move()
{
Console.WriteLine("Moving the rectangle.");
}
}
-
Rectangle类继承自Shape类,使用:关键字。 -
Rectangle类添加了两个新的属性Width和Height,分别表示矩形的宽度和高度。 -
构造函数
Rectangle(string color, double width, double height)调用了基类的构造函数base(color),用于初始化继承来的Color属性。 -
GetArea、GetPerimeter、Draw和Move方法被重写为override,提供了矩形的具体实现。
最后,我们定义一个Triangle类,它继承自Shape类,并实现了三角形特有的属性和行为:
public class Triangle : Shape
{
public double Side1 { get; set; }
public double Side2 { get; set; }
public double Side3 { get; set; }
public Triangle(string color, double side1, double side2, double side3) : base(color)
{
Side1 = side1;
Side2 = side2;
Side3 = side3;
}
public override double GetArea()
{
double s = (Side1 + Side2 + Side3) / 2;
return Math.Sqrt(s * (s - Side1) * (s - Side2) * (s - Side3));
}
public override double GetPerimeter()
{
return Side1 + Side2 + Side3;
}
public override void Draw()
{
Console.WriteLine($"Drawing a triangle with sides {Side1}, {Side2}, {Side3} and color {Color}.");
}
public override void Move()
{
Console.WriteLine("Moving the triangle.");
}
}
-
Triangle类继承自Shape类,使用:关键字。 -
Triangle类添加了三个新的属性Side1、Side2和Side3,分别表示三角形的三条边的长度。 -
构造函数
Triangle(string color, double side1, double side2, double side3)调用了基类的构造函数base(color),用于初始化继承来的Color属性。 -
GetArea、GetPerimeter、Draw和Move方法被重写为override,提供了三角形的具体实现。
通过这个案例,我们可以看到继承在C#程序设计中的强大功能。基类Shape定义了所有图形共有的属性和行为,而派生类Circle、Rectangle和Triangle则根据自己的特点实现了具体的行为。这种设计不仅提高了代码的复用性,还增强了代码的可扩展性和可维护性。
更多推荐



所有评论(0)