1. 经典区别简述
如果有人说:“接口就是一堆签名”,你就问:“你写的是哪个版本的C#?”从C# 8开始,接口已经强大多了。现在是时候把它们和抽象类好好对比下——不光是老的那些属性,还要看.NET平台的新花样。
如果回到几年前——C# 7那会儿——一切都很简单。抽象类可以有字段和部分实现的方法,接口只能有签名(方法、属性、事件、索引器)。
抽象类的继承是"是"关系(Is-a),接口实现的是多重行为继承("能做",can-do)。
| 特性 | 抽象类 | 接口(C# 8之前) |
|---|---|---|
| 关系 | is-a | can-do |
| 继承 | 只能一个 | 可以多个 |
| 字段 | 可以有 | 不能有 |
| 方法实现 | 可以 | 不行 |
| 构造函数 | 可以 | 不行 |
| 访问修饰符 | 各种(public, protected, ...) | 只能隐式public |
你看,以前抽象类就是接口的“老大哥”——更强更灵活。但一切都在变!
2. 带默认实现的接口
自从C# 8(更别说C# 14和.NET 9)出来后,接口有了新超能力——默认实现方法,英文叫"Default Interface Methods"(DIM)。
长啥样?
public interface IAnimal
{
void SayHello();
// 带默认实现的方法!
void Walk()
{
Console.WriteLine("我在走路...");
}
}
哇哦! 现在接口可以有方法实现了。而且不限一个,想加几个都行。但有个点:这些方法必须有方法体,其他的(字段、私有方法、构造函数)还是不行。
3. 现代接口的新能力
现在.NET开发者必须知道的新花样:
- 带默认实现的方法。
- 接口里的私有方法(只能给接口里的其他方法用,外面用不了)。
- 静态方法(C# 8起)。
- 带默认实现的属性。
- 静态字段(C# 14起——"static interface members")。
- 抽象静态成员("abstract static members"——现在接口可以要求实现类必须有某些静态方法!)。
现代接口完整例子:
public interface ILogger
{
static int LoggerCount { get; set; } // C# 14
void Log(string message); // 签名(契约)
// 默认实现
void LogWarning(string warning)
{
Log("[WARNING]: " + warning);
}
// 接口里的私有辅助方法(C# 8+)
private void FormatAndLog(string level, string msg)
{
Log($"{level}: {msg}");
}
// 接口里的静态方法(C# 8+)
static void PrintLoggerInfo()
{
Console.WriteLine("接口ILogger——你的最佳助手!");
}
}
想象下——以前这根本不可能,就像让猫去当服务器保安一样离谱。
4. 抽象类:这些年有啥新变化?
抽象类嘛……说实话,这十年没啥大变化。它们还是可以有:
- 字段(包括私有、受保护、静态的)。
- 实现的方法和抽象方法。
- 构造函数(对,可以写初始化逻辑)。
- 属性、事件、索引器。
- 静态和实例成员。
抽象类例子:
public abstract class Animal
{
public string Name { get; set; }
public abstract void Speak();
public virtual void Walk()
{
Console.WriteLine($"{Name} 用爪子走路!");
}
protected void Eat()
{
Console.WriteLine($"{Name} 在吃猫粮。");
}
}
抽象类依然是存放通用逻辑、状态和行为的好地方,适合一整个类族用。
5. 现代对比:新能力下的表格
| 特性 | 抽象类 | 接口(C# 14+,.NET 9) |
|---|---|---|
| 关系 | is-a(是) | can-do(能做) |
| 继承 | 只能一个 | 可以多个 |
| 字段 | 可以,随便 | 只能静态*(C# 14+) |
| 构造函数 | 有 | 没有 |
| 方法实现 | 有(virtual/abstract) | 有(default, static, abstract static) |
| 带实现的属性 | 有 | 有(default implementation) |
| 私有成员 | 有 | 有(只限方法,C# 8+) |
| 静态成员 | 有 | 有(C# 8+,有限制) |
| 静态字段 | 有 | 有*(C# 14+) |
| 访问修饰符 | 随便 | 默认public或private |
*——接口里的静态字段一般只在特殊场景用,这功能很新鲜。
6. 什么时候用接口,什么时候用抽象类:现代建议
接口(现在还能有默认实现)——用来做契约,让组件之间协作。最大特点:多用。你的类可以实现十个接口,妥妥的多面手。
抽象类适合这些场景:
- 需要通用状态(字段)、逻辑和行为,被其他类继承。
- 需要标准但可重写的逻辑(用virtual)。
- 想统一初始化(用构造函数)。
实际项目里常见套路:纯契约用接口,要是有通用代码或基础设施就搞个抽象基类。
. ┌────────────────────────┐
│ 接口 │
│ (契约:能做啥) │
└─────────┬──────────────┘
│
┌──────────────┼──────────────┐
│ │ │
实现1 实现2 ... 实现N
MyLogger CloudLogger FileLogger
(可以和抽象类继承一起用)
7. 场景对比——谁更适合
多重实现:
比如你有个接口IDrivable和抽象类Vehicle。现在Car类可以继承Vehicle,同时实现好几个接口(IDrivable、IRepairable、IInsurable)。要是你有个抽象类Repairable,那只能选Vehicle还是Repairable!接口这时候就赢了。
通用逻辑和状态:
比如所有“交通工具”都有个“车牌号”字段。这就得放抽象类里。接口里没法放字段(除了静态的)。
API进化:
Default Interface Methods的革命性——现在接口能进化,不怕把老代码搞崩。
比如你给接口加了个带默认实现的新方法——一切正常,老实现都能用!以前这事儿要么很痛苦,要么根本做不到。
8. 实战例子
我们这个学习项目慢慢加上了日志功能。来写个带默认实现的ILogger接口:
public interface ILogger
{
void Log(string message);
// 默认实现所有实现类都能用!
void LogInfo(string info)
{
Log("[INFO] " + info);
}
// 接口静态方法
static void PrintHelp()
{
Console.WriteLine("用ILogger来记录事件");
}
}
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
// 某处代码:
ILogger logger = new ConsoleLogger();
logger.LogInfo("系统已启动!"); // 多亏了default实现
// 调用接口静态方法
ILogger.PrintHelp();
如果我们给接口加了个带默认实现的新方法,所有现有实现(比如ConsoleLogger)自动就有了——不用慌,代码不会崩。
9. 错误和细节:实战坑点
要知道,事情没想象的那么美好。比如接口有default实现,但你用类类型访问对象,不用接口类型,那default实现只能通过接口用。
ConsoleLogger log = new ConsoleLogger();
log.LogInfo("Hello"); // 编译不过:LogInfo类里没定义!
ILogger log2 = log;
log2.LogInfo("Hello"); // 没问题!
这有点像接口的显式实现。有时候这样能隐藏“多余”的API,有时候新手会觉得很奇怪。
GO TO FULL VERSION