多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

接口的运用

接口的运用 1. 为什么需要接口假设程序中有这些类型EmailMessage电子邮件Report报表Invoice发票。它们不一定适合继承同一个业务基类但都可以被打印。我们真正关心的是它们具有“可打印”能力而不是它们是不是同一种事物。接口Interface就是用来描述这种能力或契约的接口规定实现者对外提供哪些成员但调用者不必知道实现细节。可以把接口类比为插座标准标准规定插头形状和电气要求不同厂家可以采用不同内部结构来满足标准。这个类比强调的是“遵守共同约定”不代表接口只能用于硬件式功能。2. 接口的基本语法使用interface定义接口。C# 中常用大写字母I作为接口名称前缀public interface IPrintable { void Print(); }类在冒号后写出要实现的接口public class Report : IPrintable { public string Title { get; set; } 未命名报表; ​ public void Print() { Console.WriteLine($正在打印报表{Title}); } }使用Report report new Report { Title 年度报表 }; report.Print();3. 接口是一份契约实现接口的非抽象类必须提供接口要求的成员public interface ISwitchable { void TurnOn(); void TurnOff(); } ​ public class Lamp : ISwitchable { public void TurnOn() { Console.WriteLine(台灯已打开。); } ​ public void TurnOff() { Console.WriteLine(台灯已关闭。); } }如果Lamp漏掉TurnOff()编译器会报错。接口让“必须提供哪些功能”成为可以由编译器检查的约定。4. 接口变量与接口多态接口不能像普通类那样直接创建对象// IPrintable item new IPrintable(); // 编译错误但是接口类型的变量可以引用实现该接口的对象IPrintable item new Report { Title 月度报表 }; item.Print();多个没有共同业务父类的类型可以通过同一个接口统一处理public class Invoice : IPrintable { public string Number { get; set; } 未知编号; ​ public void Print() { Console.WriteLine($正在打印发票{Number}); } } ​ IPrintable[] printableItems { new Report { Title 月度报表 }, new Invoice { Number FP-001 } }; ​ foreach (IPrintable printable in printableItems) { printable.Print(); }循环只关心对象是否能够Print()不关心它是报表还是发票。这也是运行时多态的一种常见形式。5. 接口可以声明哪些成员初学阶段最常见的是方法和属性public interface IUserService { // 接口属性 int UserCount { get; } ​ // 接口方法 bool AddUser(string name); string? FindUser(int id); }实现类必须提供兼容的成员public class MemoryUserService : IUserService { private readonly Liststring _users new(); ​ public int UserCount _users.Count; ​ public bool AddUser(string name) { if (string.IsNullOrWhiteSpace(name)) { return false; } ​ _users.Add(name); return true; } ​ public string? FindUser(int id) { if (id 0 || id _users.Count) { return null; } ​ return _users[id]; } }现代 C# 接口还支持更多高级成员形式例如默认实现、静态成员和静态抽象成员。初学时先牢固掌握“实例方法、属性和契约”即可。6. 接口成员的访问性没有显式实现代码的普通接口成员默认是公开契约。实现类通常需要使用publicpublic interface IRunner { void Run(); } ​ public class Athlete : IRunner { public void Run() { Console.WriteLine(运动员开始跑步。); } }如果把实现写成private void Run()它就不能作为普通的公共接口实现。后面介绍的“显式接口实现”语法是另一种特殊情况。7. 一个类可以实现多个接口C# 类只能直接继承一个基类但可以实现多个接口public interface IPrintable { void Print(); } ​ public interface ISavable { void Save(string filePath); } ​ public class Document : IPrintable, ISavable { public string Content { get; set; } string.Empty; ​ public void Print() { Console.WriteLine(Content); } ​ public void Save(string filePath) { Console.WriteLine($文档已保存到{filePath}); } }Document同时具有“可打印”和“可保存”两种身份Document document new Document { Content 学习笔记 }; ​ IPrintable printable document; ISavable savable document; ​ printable.Print(); savable.Save(note.txt);这比为了每种能力建立复杂的多层继承关系更灵活。8. 同时继承基类和实现接口一个类可以继承一个基类同时实现多个接口。基类必须写在接口前面public abstract class Device { public string Name { get; } ​ protected Device(string name) { Name name; } } ​ public interface IConnectable { void Connect(); } ​ public interface IRechargeable { void Charge(); } ​ public class SmartPhone : Device, IConnectable, IRechargeable { public SmartPhone(string name) : base(name) { } ​ public void Connect() { Console.WriteLine(${Name}已连接网络。); } ​ public void Charge() { Console.WriteLine(${Name}正在充电。); } }这里的设计含义是SmartPhone是一种DeviceSmartPhone具有IConnectable和IRechargeable所描述的能力。9. 显式接口实现如果两个接口包含签名相同但含义不同的成员或者不希望接口成员直接出现在类的公共表面可以使用显式接口实现public interface IChineseSpeaker { void SayHello(); } ​ public interface IEnglishSpeaker { void SayHello(); } ​ public class BilingualPerson : IChineseSpeaker, IEnglishSpeaker { void IChineseSpeaker.SayHello() { Console.WriteLine(你好); } ​ void IEnglishSpeaker.SayHello() { Console.WriteLine(Hello!); } }显式实现不能直接通过类变量调用BilingualPerson person new BilingualPerson(); ​ // person.SayHello(); // 编译错误类上没有普通公共 SayHello() ​ IChineseSpeaker chineseSpeaker person; IEnglishSpeaker englishSpeaker person; ​ chineseSpeaker.SayHello(); // 你好 englishSpeaker.SayHello(); // Hello!显式接口实现的特点成员名称写成接口名.成员名前面不写public只能通过相应的接口引用访问能解决接口成员重名但语义不同的问题也可以把较少使用的接口功能从类的常用公共成员中隐藏起来。不要为了“显得高级”而滥用显式实现。大多数简单接口使用普通公共实现更直观。10. 接口可以继承接口接口可以在另一个接口的基础上增加契约public interface IReadable { string Read(); } ​ public interface IEditable : IReadable { void Write(string content); }实现IEditable的非抽象类必须满足它自己和父接口的要求public class TextDocument : IEditable { private string _content string.Empty; ​ public string Read() { return _content; } ​ public void Write(string content) { _content content; } }接口还可以继承多个接口用来组合多种能力。不过接口层次过深会增加理解成本应保持契约小而清晰。11. 默认接口实现从 C# 8 开始接口成员可以提供默认实现public interface ILogger { void Log(string message); ​ void LogWarning(string message) { Log($警告{message}); } } ​ public class ConsoleLogger : ILogger { public void Log(string message) { Console.WriteLine(message); } }调用默认接口成员时通常通过接口引用ILogger logger new ConsoleLogger(); logger.LogWarning(磁盘空间不足);默认接口实现主要用于在不立即破坏已有实现类的前提下扩展接口。对初学者而言要注意接口的核心用途仍然是表达契约不要把大量业务状态和实现都塞进接口需要共享字段、构造逻辑和大量实现时抽象类往往更自然实际项目还要考虑目标框架和团队规范是否支持或允许这种写法。12. 接口不能保存每个对象的实例状态普通类和抽象类可以声明实例字段public class Counter { private int _count; }接口不用于保存每个实现对象自己的实例字段。对象状态应由实现类或结构体保存接口只规定外部可使用的成员。现代 C# 允许接口出现某些静态成员但这不改变接口没有普通实例字段、不能承担每个对象状态存储的基本理解。13. 结构体也可以实现接口接口不仅能由类实现结构体也可以实现public interface IFormattableValue { string Format(); } ​ public readonly struct Temperature : IFormattableValue { public double Celsius { get; } ​ public Temperature(double celsius) { Celsius celsius; } ​ public string Format() { return ${Celsius:F1} ℃; } }把结构体转换为接口引用时可能发生装箱。先根据类型语义选择类或结构体再在确有性能要求时测量装箱影响。14. 接口如何降低耦合如果一个通知服务直接依赖具体邮件类它就很难改用短信public interface IMessageSender { void Send(string message); } ​ public class EmailSender : IMessageSender { public void Send(string message) { Console.WriteLine($发送邮件{message}); } } ​ public class SmsSender : IMessageSender { public void Send(string message) { Console.WriteLine($发送短信{message}); } } ​ public class NotificationService { private readonly IMessageSender _sender; ​ public NotificationService(IMessageSender sender) { _sender sender; } ​ public void Notify(string message) { _sender.Send(message); } }使用时选择具体实现NotificationService emailService new NotificationService(new EmailSender()); ​ NotificationService smsService new NotificationService(new SmsSender()); ​ emailService.Notify(欢迎注册); smsService.Notify(验证码是 123456);NotificationService依赖的是IMessageSender契约不依赖某一种发送方式。以后增加新的实现时它的核心代码通常不需要修改。这就是接口常用于解耦和依赖注入的原因。15. 什么时候适合使用接口以下情况适合优先考虑接口多种没有共同父类的类型需要表现同一种能力一个类需要同时具备多种独立能力调用者只需要知道“能做什么”不需要知道内部状态和实现希望同一功能可以替换不同实现例如文件存储、数据库存储希望便于测试用简单替代实现代替真实网络、文件或数据库组件需要建立稳定的小型公共契约。接口应尽量职责单一。例如IPrintable只描述打印不要同时塞入保存、联网、登录和统计等互不相关的功能。16. 抽象类和接口可以一起使用二者不是非此即彼。常见设计是接口定义对外契约抽象类为一组相关实现提供公共代码具体类继承抽象类并完成剩余实现。public interface IExporter { void Export(string content); } ​ public abstract class FileExporter : IExporter { protected string OutputDirectory { get; } ​ protected FileExporter(string outputDirectory) { OutputDirectory outputDirectory; } ​ public abstract void Export(string content); ​ protected void ShowTarget(string fileName) { Console.WriteLine($输出到{OutputDirectory}/{fileName}); } } ​ public class TextExporter : FileExporter { public TextExporter(string outputDirectory) : base(outputDirectory) { } ​ public override void Export(string content) { ShowTarget(result.txt); Console.WriteLine(content); } }调用者可以只依赖IExporter而相关的文件导出实现可以通过FileExporter复用公共逻辑。
返回列表