1. 从“纯契约”到灵活架构
在C# 8之前,接口就像死板的契约:你想实现接口——那就得把所有东西都实现了,哪怕差一个逗号都不行。如果接口里加了新成员,所有现有实现都得马上加上,不然编译器就不让你过。
但现实更复杂。想象下,你维护一个被几百个项目用的库,突然你想在接口里加个新方法。你肯定不想破坏向后兼容吧?这时候,默认方法(Default Interface Methods, DIM)就登场了!
核心是什么?
默认方法允许你直接在接口里写实现。这样契约就更灵活了:如果类没实现“新东西”,就会用默认实现。就像电影里的替身:演员不想跳桥,就让替身上。
2. Default Interface Methods的语法
怎么在接口里声明带实现的方法?
和普通方法很像,只不过现在你可以(也必须)直接在接口里写方法体:
public interface ILogger
{
void Log(string message);
// 新方法,带默认实现!
void LogWarning(string message)
{
Log("[WARNING] " + message);
}
}
这里LogWarning已经有实现了!任何实现ILogger的类,只需要实现Log,LogWarning会自动用默认实现(除非你自己写了)。
对比:传统 vs. 现代签名
| 版本 | 接口声明 |
|---|---|
| 在C# 8之前 | |
| C# 8及以后 | |
语法细节要注意
- 带实现的方法必须用大括号写方法体。
- 默认方法不能是abstract。
- 接口里的所有方法还是默认public。
- 也可以声明带默认get/set的属性(见下文)。
3. 实用例子
例子1. 保证向后兼容
假设你应用里有个保存数据的接口:
public interface ISaveable
{
void Save(string filePath);
}
后来你想加个云保存。不想改一百个类?加个默认方法就行!
public interface ISaveable
{
void Save(string filePath);
// 新方法,带“默认”实现!
void SaveToCloud(string cloudService)
{
Console.WriteLine($"保存到云 {cloudService}(默认啥也不做)");
}
}
现在所有老类都“会”云保存了(虽然现在只是输出一句话)。
例子2. 扩展日志接口
之前我们有个简单的日志接口:
public interface ILogger
{
void Log(string message);
}
加个默认的错误日志方法:
public interface ILogger
{
void Log(string message);
void LogError(string message)
{
Log("[ERROR] " + message);
}
}
实现ILogger的类可以不用实现LogError——会用默认版本:
public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
// LogError不实现——会用默认实现!
}
ILogger logger = new ConsoleLogger();
logger.Log("一切OK!");
logger.LogError("哎呀,出错啦!"); // 会调用默认实现
例子3. 默认方法+可扩展应用
你的应用支持多种导出:到文件、数据库、网络。接口如下:
public interface IExporter
{
void Export(string data, string destination);
// 新功能——导出到压缩包
void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("默认不支持压缩。");
}
}
其他开发者写的插件还能继续用,哪怕他们不知道新方法的存在。
4. 接口默认方法的调用机制?
场景:“老类—新接口”
如果类没实现接口的默认方法,通过接口引用调用时会用接口的实现。如果实现了——就用自己的。
public class FileExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("保存到文件...");
}
// ExportToArchive不实现——会用默认输出
}
IExporter exporter = new FileExporter();
exporter.Export("数据", "file.txt"); // 用FileExporter的实现
exporter.ExportToArchive("数据", "file.zip"); // 用默认实现!
场景:“类覆盖默认方法”
public class AdvancedExporter : IExporter
{
public void Export(string data, string destination)
{
Console.WriteLine("高级模式保存...");
}
public void ExportToArchive(string data, string archivePath)
{
Console.WriteLine("支持压缩!");
}
}
IExporter exporter = new AdvancedExporter();
exporter.ExportToArchive("数据", "file.zip"); // 现在会调用类自己的实现!
5. Default Interface Methods还能干啥?
默认属性和事件
可以声明带默认实现的属性,只要有get或set方法体:
public interface IHasId
{
// 没重写前自动返回42
int Id => 42;
}
public class Person : IHasId {}
Console.WriteLine(new Person().Id); // 42
接口代码里调用默认方法
在接口内部,默认方法和其他成员可以互相调用:
public interface IDemo
{
void Foo() => Bar();
void Bar() => Console.WriteLine("BAR");
}
6. Default Interface Methods的限制和特点
能声明字段、构造函数吗?
不能。即使有默认方法,接口还是不是类。不能有字段、构造函数、析构函数。
能对接口用base吗?
可以,但有点特殊。在接口的默认方法里,可以通过显式指定调用父接口的方法:
public interface IBase
{
void Greet() => Console.WriteLine("IBase打招呼");
}
public interface IDerived : IBase
{
void IBase.Greet()
{
Console.WriteLine("IDerived打招呼!");
IBase.Greet(this); // 显式调用父接口方法
}
}
但一般场景很少用到。
默认实现冲突会怎样?
public interface IA { void Foo() { Console.WriteLine("A"); } }
public interface IB { void Foo() { Console.WriteLine("B"); } }
// 类没显式实现Foo:
public class C : IA, IB { }
// 编译错误:不知道选哪个实现!
7. 常见错误、限制和特点
有趣的常见错误:
有些人学了DIM后想在接口里声明字段——但还是不行。另外,如果你想实现默认的静态方法——C# 8之前不行。(8以后接口可以有静态方法,但默认实现静态方法是另一回事。)
特点:Diamond Problem(菱形问题)
如果你的类实现了两个接口,这两个接口有同名的默认方法,你必须显式实现这个方法:
public class ConflictClass : IA, IB
{
public void Foo() // 你得自己选一个!
{
// 显式通过接口调用想要的实现(如果需要)
((IA)this).Foo();
// 或者
((IB)this).Foo();
}
}
别滥用!
默认方法能救向后兼容,但用多了会让架构变脏,逻辑分散在接口里。尽量把重要逻辑放类里,接口还是当“契约”用。
GO TO FULL VERSION