CodeGym /课程 /C# SELF /接口的Default方法

接口的Default方法

C# SELF
第 24 级, 课程 0
可用

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之前
void DoSomething();
C# 8及以后
void DoSomething() { Console.WriteLine("我在工作!"); }

语法细节要注意

  • 带实现的方法必须用大括号写方法体。
  • 默认方法不能是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();
    }
}

别滥用!
默认方法能救向后兼容,但用多了会让架构变脏,逻辑分散在接口里。尽量把重要逻辑放类里,接口还是当“契约”用。

2
任务
C# SELF, 第 24 级, 课程 0
已锁定
扩展接口,添加默认方法
扩展接口,添加默认方法
2
任务
C# SELF, 第 24 级, 课程 0
已锁定
接口冲突与实现选择
接口冲突与实现选择
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION