抽象类与接口
抽象类与抽象方法
抽象类使用 abstract 修饰,不能被实例化,可包含抽象方法和具体方法。抽象方法没有方法体,必须在派生类中重写。
示例
abstract class Shape
{
public string Name { get; set; }
public abstract double Area();
public void Print()
{
Console.WriteLine($"{Name} 面积: {Area():F2}");
}
}
class Circle : Shape
{
public double Radius { get; set; }
public Circle(double radius)
{
Name = "圆形";
Radius = radius;
}
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public Rectangle(double w, double h)
{
Name = "矩形";
Width = w;
Height = h;
}
public override double Area()
{
return Width * Height;
}
}
Circle c = new Circle(3);
Rectangle r = new Rectangle(4, 5);
c.Print();
r.Print();
圆形 面积: 28.27
矩形 面积: 20.00
💡 抽象类就像一个"半成品模板",规定了子类必须完成的部分(抽象方法),也提供了可以直接使用的部分(具体方法)。
抽象类与普通类的区别
| 特性 | 普通类 | 抽象类 |
|---|---|---|
| 实例化 | 可以 | 不可以 |
| 抽象方法 | 不可以包含 | 可以包含 |
| 具体方法 | 可以 | 可以 |
| 构造函数 | 有 | 有 |
| 字段 | 有 | 有 |
| 被继承 | 可选重写virtual | 必须重写abstract |
| sealed修饰 | 可以 | 不可以(矛盾) |
示例
abstract class Logger
{
protected string prefix;
public Logger(string prefix)
{
this.prefix = prefix;
}
public abstract void Log(string message);
public void LogWithTimestamp(string message)
{
string ts = DateTime.Now.ToString("HH:mm:ss");
Log($"[{ts}] {message}");
}
}
class FileLogger : Logger
{
public FileLogger() : base("FILE") { }
public override void Log(string message)
{
Console.WriteLine($"{prefix}: {message}");
}
}
FileLogger fl = new FileLogger();
fl.Log("启动服务");
fl.LogWithTimestamp("连接数据库");
FILE: 启动服务
FILE: [14:30:00] 连接数据库
⚠️ 抽象类可以有构造函数,但构造函数不能是 abstract 的。构造函数在子类实例化时通过 base() 调用。
接口定义与实现
接口使用 interface 关键字定义,是一组成员的契约。实现接口的类必须提供所有成员的实现。接口成员默认为 public,不能有访问修饰符。
示例
interface IMovable
{
void Move(double dx, double dy);
double Speed { get; set; }
}
interface IDrawable
{
void Draw();
}
class Player : IMovable, IDrawable
{
public double X { get; set; }
public double Y { get; set; }
public double Speed { get; set; }
public Player(double x, double y, double speed)
{
X = x;
Y = y;
Speed = speed;
}
public void Move(double dx, double dy)
{
X += dx * Speed;
Y += dy * Speed;
Console.WriteLine($"移动到 ({X:F1}, {Y:F1})");
}
public void Draw()
{
Console.WriteLine($"绘制玩家 @ ({X:F1}, {Y:F1})");
}
}
Player p = new Player(0, 0, 2.0);
p.Draw();
p.Move(3, 4);
p.Draw();
绘制玩家 @ (0.0, 0.0)
移动到 (6.0, 8.0)
绘制玩家 @ (6.0, 8.0)
📌 接口名称推荐以大写字母 I 开头,如 IComparable、IDisposable,这是 C# 的命名约定。
多接口实现
一个类可以实现多个接口,用逗号分隔。这是 C# 实现多态的方式之一,弥补了单继承的限制。
示例
interface IWalkable
{
void Walk();
}
interface ISwimmable
{
void Swim();
}
interface IFlyable
{
void Fly();
}
class Duck : IWalkable, ISwimmable, IFlyable
{
public string Name { get; set; }
public Duck(string name)
{
Name = name;
}
public void Walk()
{
Console.WriteLine($"{Name} 摇摇摆摆地走");
}
public void Swim()
{
Console.WriteLine($"{Name} 在水里游");
}
public void Fly()
{
Console.WriteLine($"{Name} 扑腾翅膀飞起来");
}
}
Duck d = new Duck("唐老鸭");
d.Walk();
d.Swim();
d.Fly();
IWalkable walker = d;
walker.Walk();
ISwimmable swimmer = d;
swimmer.Swim();
唐老鸭 摇摇摆摆地走
唐老鸭 在水里游
唐老鸭 扑腾翅膀飞起来
唐老鸭 摇摇摆摆地走
唐老鸭 在水里游
💡 不同接口类型的变量只能调用该接口定义的成员,这是接口隔离原则的体现。
接口默认方法(C# 8)
从 C# 8 开始,接口可以包含默认实现的方法。这允许在不破坏现有实现类的情况下向接口添加新成员。
示例
interface ILogger
{
void Log(string message);
void LogWarning(string message)
{
Log($"[WARN] {message}");
}
void LogError(string message)
{
Log($"[ERROR] {message}");
}
}
class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
class SimpleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($">> {message}");
}
}
ConsoleLogger cl = new ConsoleLogger();
cl.Log("系统启动");
cl.LogWarning("内存不足");
cl.LogError("连接失败");
SimpleLogger sl = new SimpleLogger();
sl.Log("系统启动");
sl.LogWarning("内存不足");
系统启动
[WARN] 内存不足
[ERROR] 连接失败
>> 系统启动
>> [WARN] 内存不足
⚠️ 默认方法仅在通过接口类型调用时可用,通过实现类类型的变量无法调用默认方法(除非实现类也重新实现该方法)。
抽象类与接口选择指南
| 对比项 | 抽象类 | 接口 |
|---|---|---|
| 继承/实现 | 单继承 | 多实现 |
| 成员类型 | 任意 | 方法、属性、事件、索引器 |
| 字段 | 可以 | 不可以(C# 8前) |
| 构造函数 | 可以 | 不可以 |
| 访问修饰符 | 任意 | 默认public |
| 默认实现 | 可以 | C# 8起可以 |
| 版本兼容 | 加新成员安全 | 加新成员破坏实现类(C# 8前) |
| 设计意图 | "是什么"(IS-A) | "能做什么"(CAN-DO) |
选择建议:
- 表示有层次关系的"是什么"关系 → 抽象类
- 表示跨类型的能力或行为 → 接口
- 需要共享代码和状态 → 抽象类
- 需要多继承能力 → 接口
- 需要为已有接口添加新方法而不破坏旧代码 → 接口默认方法
🔥 简单口诀:共享身份用抽象,共享能力用接口。
显式接口实现
当多个接口有同名成员,或需要隐藏接口成员时,使用显式接口实现。语法为 返回类型 接口名.成员名,显式实现的成员不能加访问修饰符,且只能通过接口类型调用。
示例
interface IEnglish
{
void Speak();
}
interface IFrench
{
void Speak();
}
class BilingualPerson : IEnglish, IFrench
{
void IEnglish.Speak()
{
Console.WriteLine("Hello!");
}
void IFrench.Speak()
{
Console.WriteLine("Bonjour!");
}
public void Speak()
{
Console.WriteLine("Hi!");
}
}
BilingualPerson p = new BilingualPerson();
p.Speak();
IEnglish eng = p;
eng.Speak();
IFrench fr = p;
fr.Speak();
Hi!
Hello!
Bonjour!
📌 显式接口实现的成员在类实例上不可见,必须转型为对应接口类型才能调用。这既是限制,也是一种封装手段。
IComparable<T> 实战
IComparable<T> 是 .NET 中最常用的接口之一,用于定义类型的自然排序顺序。Array.Sort 和 List<T>.Sort 都依赖此接口。
示例
class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public Student(string name, int score)
{
Name = name;
Score = score;
}
public int CompareTo(Student other)
{
if (other == null) return 1;
return Score.CompareTo(other.Score);
}
public override string ToString()
{
return $"{Name}({Score})";
}
}
Student[] students = new Student[]
{
new Student("张三", 85),
new Student("李四", 92),
new Student("王五", 78),
new Student("赵六", 95),
new Student("钱七", 88)
};
Console.WriteLine("排序前:");
foreach (var s in students)
Console.WriteLine(s);
Array.Sort(students);
Console.WriteLine("排序后(按分数升序):");
foreach (var s in students)
Console.WriteLine(s);
排序前:
张三(85)
李四(92)
王五(78)
赵六(95)
钱七(88)
排序后(按分数升序):
王五(78)
张三(85)
钱七(88)
李四(92)
赵六(95)
💻 CompareTo 返回值约定:负数表示当前对象排在前面,零表示相等,正数表示当前对象排在后面。借助 IComparable<T>,自定义类型可以直接使用 Array.Sort、List<T>.Sort 等排序功能。
❓ 常见问题
📖 小节
abstract类不可实例化,可包含抽象方法和具体方法- 抽象方法没有方法体,派生类必须用
override重写 - 接口定义契约,实现类必须提供所有成员的实现
- 一个类可实现多个接口,弥补单继承限制
- C# 8 接口支持默认方法,便于向后兼容
- 抽象类表示"是什么",接口表示"能做什么"
- 显式接口实现解决同名冲突,只能通过接口类型调用
IComparable<T>定义自然排序,配合Sort使用
📝 作业
- 定义抽象类
Vehicle,包含抽象方法Start()和具体方法Stop(),然后创建Car和Bike子类实现 - 定义接口
ISerializable含方法string Serialize(),定义接口IDeserializable含方法void Deserialize(string data),创建一个类同时实现两个接口 - 定义两个接口
IPlayable和IRecordable,都有void Pause()方法,用显式接口实现区分两者 - 创建
Product类实现IComparable<Product>,按价格排序,并测试Array.Sort - 为已有接口
IPrintable添加一个带默认实现的void PrintHeader(string title)方法,验证旧实现类不受影响



