子类
继承的概念
- 继承机制:C#支持单继承,即一个类只能直接继承自一个基类。但基类本身可以继承自另一个类,从而实现继承链。
- 继承关键字:使用冒号(
:
)表示继承关系,子类在声明时指定其基类。
子类的成员
- 继承的成员:子类会继承基类的非私有成员(public、protected、internal、protected internal)。私有成员(private)不会被继承。
- 新增的成员:子类可以定义自己的新成员,这些成员与继承自基类的成员共存。
- 重写的成员:子类可以使用
override
关键字重写继承自基类的虚方法(virtual methods)或抽象方法(abstract methods)。重写允许子类为继承的方法提供特定的实现。
访问修饰符与继承
- 访问修饰符:基类和子类成员的访问级别会影响它们的可访问性和继承性。例如,私有成员在子类中不可见,而受保护的成员(protected)在派生类中可见但在外部不可见。
- 内部(internal)与受保护内部(protected internal):这两种访问修饰符在继承时具有特定的作用域。internal成员在同一程序集中可见,而protected internal成员在同一程序集及其派生类中可见。
构造函数与析构函数
- 构造函数:子类在实例化时会首先调用基类的构造函数(如果有的话)。如果基类有无参构造函数,则子类的无参构造函数会隐式调用它。如果基类只有带参数的构造函数,则子类必须显式调用基类的构造函数,通常是在子类构造函数的初始化列表中完成。
- 析构函数:子类析构函数会先执行子类的清理代码,然后自动调用基类的析构函数(如果存在)。
示例
class Base
{
public Base()
{
Console.WriteLine("Base constructor");
}
~Base()
{
Console.WriteLine("Base destructor");
}
}
class Derived : Base
{
public Derived() : base() // 显式调用基类构造函数(虽然是隐式也可以)
{
Console.WriteLine("Derived constructor");
}
~Derived()
{
Console.WriteLine("Derived destructor");
}
}
class Program
{
static void Main()
{
Derived d = new Derived();
d = null; // 假设这里触发了垃圾回收,但实际上需要GC.Collect()来强制
GC.Collect(); // 强制垃圾回收以触发析构函数
GC.WaitForPendingFinalizers(); // 等待所有析构函数执行完毕
}
}
抽象类与密封类
- 抽象类:抽象类不能被实例化,但可以作为其他类的基类。抽象类可以包含抽象方法(只有声明没有实现的方法),这些方法必须在非抽象派生类中被重写。
- 密封类:密封类(使用
sealed
关键字修饰)不能被其他类继承。这有助于防止类被不恰当地扩展。
你提到的概念是面向对象编程(OOP)中非常基础且重要的部分,特别是在C#、Java等语言中。下面我将详细解释这些概念,并给出一些示例代码。
示例
abstract class Animal
{
public abstract void MakeSound(); // 抽象方法
}
class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Woof");
}
}
sealed class Cat : Animal
{
// 错误:不能从密封类继承
// class Kitten : Cat {}
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
class Program
{
static void Main()
{
// Animal a = new Animal(); // 错误:Animal是抽象类,不能被实例化
Dog d = new Dog();
d.MakeSound(); // 输出: Woof
Cat c = new Cat();
c.MakeSound(); // 输出: Meow
}
}
接口实现
- 接口实现:虽然子类直接继承自基类,但子类也可以实现接口。接口实现与类继承不同,接口定义了一组方法、属性等成员的规范,而不提供实现。实现接口的类必须为接口中的每个成员提供具体实现。
示例
using System;
public class Animal
{
public void Eat()
{
Console.WriteLine("This animal eats food.");
}
}
public class Dog : Animal
{
public override void Eat()
{
Console.WriteLine("Dog eats meat.");
}
public void Bark()
{
Console.WriteLine("Dog barks.");
}
}
class Program
{
static void Main(string[] args)
{
Dog myDog = new Dog();
myDog.Eat(); // 输出: Dog eats meat.
myDog.Bark(); // 输出: Dog barks.
}
}
在这个示例中,Dog
类继承自Animal
类,并重写了Eat
方法。此外,Dog
类还定义了自己的Bark
方法。在Main
方法中,我们创建了Dog
类的一个实例,并调用了它的Eat
和Bark
方法。
接口
接口定义
- 基本定义:接口是一种引用类型,使用
interface
关键字声明。它指定了一组方法、属性、事件和索引器的规范,但不提供这些方法的具体实现。 - 命名约定:按照惯例,接口名称通常以大写字母“I”开头,后跟接口名称的其余部分,如
IExampleInterface
。 - 成员定义:接口中的成员只有声明,没有实现。成员声明使用分号结束,而不是大括号。
- 访问修饰符:接口成员默认为
public
,且不允许使用private
或protected
修饰符。
接口特性
- 不能直接实例化:接口本身不能被实例化,因为它不包含实现。只有实现了接口的类或结构才能被实例化。
- 抽象成员:接口中的所有成员都是抽象的,即它们不包含实现代码。
- 多继承:接口支持多继承,一个接口可以继承自一个或多个其他接口。
- 隐式与显式实现:类可以实现接口,并通过隐式或显式方式提供接口成员的实现。隐式实现允许通过类实例直接访问接口成员,而显式实现则要求通过接口实例来访问。
用途
- 定义契约:接口定义了一组规范,实现接口的类必须遵循这些规范。这有助于确保不同类之间的互操作性。
- 解耦:通过接口,可以实现类之间的解耦,即类的实现细节对彼此是透明的。这有助于降低系统的复杂性和提高可维护性。
- 多态性:接口是实现多态性的重要手段之一。通过接口引用,可以指向实现了该接口的任何类实例,从而允许在运行时动态地替换对象的行为。
- 依赖注入:在依赖注入等设计模式中,接口被广泛用于定义服务的契约,以便在不修改代码的情况下替换服务的实现。
实现
- 实现接口的类:只有类和结构(在C#中)才能实现接口。实现接口的类必须提供接口中声明的所有成员的实现。
- 实现语法:在类定义中,通过冒号(
:
)后跟接口名称来指定该类实现的接口。如果类实现了多个接口,接口名称之间用逗号分隔。 - 接口继承:当类实现了某个接口时,它也可以继承该接口继承的所有其他接口的成员。这意味着实现接口的类必须提供所有这些接口成员的实现。
与类的比较
- 继承关系:类只能单继承(即只能继承自一个基类),但接口支持多继承。
- 实现与继承:类继承不仅是说明继承(即继承基类的声明),也是实现继承(即继承基类的实现)。而接口继承只是说明继承,即派生接口继承了父接口的成员声明,但没有继承实现。
- 成员类型:接口只能包含方法、属性、事件和索引器的声明,不能包含字段、常量、运算符、实例构造函数、析构函数或静态成员。类则可以包含所有这些类型的成员。
示例
public interface IExampleInterface
{
void Method1();
int Property1 { get; set; }
}
public class ExampleClass : IExampleInterface
{
public void Method1()
{
Console.WriteLine("Method1 implementation");
}
public int Property1 { get; set; }
}
在这个示例中,IExampleInterface
接口定义了一个无返回值的方法Method1
和一个读写属性Property1
。ExampleClass
类实现了这个接口,并提供了Method1
方法和Property1
属性的具体实现。