1. 业务逻辑中的接口:按钮和动作
这是接口的最后一节课,所以这里会有超多实用例子,让你更好地理解接口在实际开发中的用法。
接口在大型应用里特别有价值。所以下面举的都是比较复杂的场景,有时候甚至超前于本课程进度。感兴趣就看看,不感兴趣直接跳到下一级 :P
最简单的例子——按钮和点击
我们继续扩展我们的学习用控制台小程序——假设我们有一个简单的用户控件系统(比如按钮、文本框和单选框)在控制台菜单里。
我们希望有些控件能响应点击(按钮),有些不能(比如静态标题)。接口可以优雅地实现这种需求。
第1步:创建接口
// 可以“点击”的组件:
public interface IClickable
{
void Click();
}
第2步:把接口用在不同类型的控件上
// 所有菜单项的基类
public class MenuItem
{
public string Title { get; set; }
public MenuItem(string title)
{
Title = title;
}
public virtual void Display()
{
Console.WriteLine(Title);
}
}
// 按钮:可以“点击”
public class Button : MenuItem, IClickable
{
public Button(string title) : base(title) {}
public void Click()
{
Console.WriteLine($"[按钮] {Title} 被点击了!");
}
}
// 只是个标签:不能点击
public class Label : MenuItem
{
public Label(string title) : base(title) {}
}
第3步:用接口多态
IClickable[] clickableItems = new IClickable[]
{
new Button("保存"),
new Button("退出")
// Label 不能加进来——它不是 IClickable!
};
foreach (var item in clickableItems)
{
item.Click();
}
看到魔法了吗?这个列表只接受“会”Click的对象,所以你不会遇到运行时错误,比如点了个不该点的对象。
2. 接口和“策略”模式:运行时选择算法
假如你写了个应用,可以用不同方式保存报告:存文件、存数据库、存“云”,甚至发到你老板的Telegram(别问,啥都有可能)。
第1步:明确需求
需要让ReportGenerator类能用任何保存方式,而不用关心具体实现细节。
第2步:定义策略接口
public interface IDataSaver
{
void Save(string reportData);
}
第3步:实现不同的保存方式
public class FileDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[FileDataSaver] 保存到文件中...\n" + reportData);
// 这里可以写 File.WriteAllText(...)
}
}
public class DatabaseDataSaver : IDataSaver
{
public void Save(string reportData)
{
Console.WriteLine("[DatabaseDataSaver] 保存到数据库...\n" + reportData);
// 这里可以写数据库保存代码
}
}
第4步:用接口切换保存策略
public class ReportGenerator
{
private readonly IDataSaver _dataSaver;
public ReportGenerator(IDataSaver dataSaver)
{
_dataSaver = dataSaver;
}
public void GenerateReport()
{
string report = "这是重要报告!";
Console.WriteLine("正在生成报告...");
_dataSaver.Save(report);
}
}
灵活性演示
// 可以随便切换保存方式,ReportGenerator不用改一行代码!
IDataSaver fileSaver = new FileDataSaver();
ReportGenerator fileReport = new ReportGenerator(fileSaver);
fileReport.GenerateReport();
IDataSaver dbSaver = new DatabaseDataSaver();
ReportGenerator dbReport = new ReportGenerator(dbSaver);
dbReport.GenerateReport();
实际意义:如果哪天你想把报告写到一个新潮的服务,只要写个新类实现接口,直接塞进架构里,老代码一行不用动。
3. .NET里的接口:IDisposable 和 using
.NET里最常见的接口之一就是IDisposable。所有操作非托管资源的类都会实现它:文件、流、网络连接啥的。
为什么要有IDisposable?
当你用到必须手动释放的资源(比如关闭文件),你就实现IDisposable,写个Dispose方法。这样就能把对象交给using语句,保证离开代码块时一定会调用Dispose。
例子:模拟文件操作
public class FakeFile : IDisposable
{
public string FileName { get; }
public FakeFile(string fileName)
{
FileName = fileName;
Console.WriteLine($"打开文件: {fileName}");
}
public void Dispose()
{
Console.WriteLine($"关闭文件: {FileName}");
}
}
// 主程序里:
using (var file = new FakeFile("report.txt"))
{
Console.WriteLine("写入文件中...");
// using后文件会自动“关闭”
}
输出:
打开文件: report.txt
写入文件中...
关闭文件: report.txt
真的很有用:你不会忘记关文件——接口和语言机制会帮你兜底。
4. 集合和LINQ里的接口
你用列表、数组、字典的时候,其实已经在用接口了,只是你没注意到。
List<int> list = new List<int> { 1, 2, 3 };
IEnumerable<int> enumerable = list; // 没问题!
// 现在你可以这样遍历元素:
foreach(var x in enumerable)
{
Console.WriteLine(x);
}
大多数LINQ方法都是通过IEnumerable<T>接口操作集合的。这样你写的代码就不用关心具体集合类型。
为啥要这样?
你可以把List<T>换成T[]、HashSet<T>甚至你自己写的集合——代码都能跑!
5. 测试用接口(Mock对象)
测试时很重要的一点是让代码和外部依赖隔离开:别去真数据库,别碰真文件。接口让你可以用假对象(mock对象)测试代码,不用动真家伙。
public class FakeDataSaver : IDataSaver
{
public bool WasCalled { get; private set; } = false;
public void Save(string reportData)
{
WasCalled = true;
Console.WriteLine("数据保存到mock对象里了!");
}
}
// 在测试里
FakeDataSaver saver = new FakeDataSaver();
ReportGenerator generator = new ReportGenerator(saver);
generator.GenerateReport();
Console.WriteLine($"Save被调用了吗? {saver.WasCalled}");
结果:测试不依赖真实环境,但能验证方法确实被调用了!
6. 显式接口实现:解决冲突
有时候一个类要实现两个接口,这俩接口有同名方法,但逻辑不一样。
public interface IFlyable
{
void Move();
}
public interface ISwimmable
{
void Move();
}
public class Duck : IFlyable, ISwimmable
{
// 显式实现
void IFlyable.Move()
{
Console.WriteLine("鸭子飞起来了!");
}
void ISwimmable.Move()
{
Console.WriteLine("鸭子游泳了!");
}
}
Duck duck = new Duck();
// duck.Move(); // 报错:没有这个方法!
((IFlyable)duck).Move(); // "鸭子飞起来了!"
((ISwimmable)duck).Move(); // "鸭子游泳了!"
看起来有点硬核,但有时候规范就是这么要求的!
7. 事件模型里的接口:INotifyPropertyChanged
.NET有标准接口支持事件——比如数据模型有变化,UI要知道(WPF、MAUI等GUI里超常见)。
using System.ComponentModel;
public class Person : INotifyPropertyChanged
{
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
}
意义:任何支持数据绑定的框架都希望你的类实现这个接口——这样UI属性变了会自动刷新。
8. “工厂”模式里的接口(Factory)
接口特别适合做工厂——专门创建不同但可互换对象的类。
public interface ITransport
{
void Move();
}
public class Bicycle : ITransport
{
public void Move() => Console.WriteLine("骑自行车出发!");
}
public class Car : ITransport
{
public void Move() => Console.WriteLine("开车出发!");
}
public class TransportFactory
{
public static ITransport Create(string type)
{
return type switch
{
"bike" => new Bicycle(),
"car" => new Car(),
_ => throw new ArgumentException("未知的交通工具类型")
};
}
}
ITransport transport = TransportFactory.Create("bike");
transport.Move(); // "骑自行车出发!"
9. 事件接口:自定义事件例子
来定义一个“事件监听器”接口:
public interface ILoginListener
{
void OnLogin(string userName);
}
// 触发事件的类
public class LoginManager
{
private List<ILoginListener> listeners = new();
public void Subscribe(ILoginListener listener) => listeners.Add(listener);
public void Login(string userName)
{
Console.WriteLine($"用户 {userName} 登录了。");
foreach (var listener in listeners)
listener.OnLogin(userName);
}
}
// 实现接口的类
public class WelcomeMessage : ILoginListener
{
public void OnLogin(string userName)
{
Console.WriteLine($"欢迎, {userName}!");
}
}
LoginManager manager = new();
manager.Subscribe(new WelcomeMessage());
manager.Login("瓦夏");
// 用户 瓦夏 登录了。
// 欢迎, 瓦夏!
面试时:如果被问“你怎么实现自己的事件系统?”,直接说接口监听器就行!
10. 插件接口(应用扩展性)
很多大型应用都支持插件。靠接口,你的应用可以“动态加载”新模块,完全不用知道它们的内部细节。
public interface IPlugin
{
string Name { get; }
void Run();
}
// 你的应用:
public class PluginLoader
{
public void LoadAndRun(IEnumerable<IPlugin> plugins)
{
foreach (var plugin in plugins)
{
Console.WriteLine($"启动插件: {plugin.Name}");
plugin.Run();
}
}
}
插件可以由第三方开发——只要实现接口就行。你的应用就能无限扩展!
GO TO FULL VERSION