目 录CONTENT

文章目录

CSharp(二十九) 接口(Interface)详解

CSharp(二十九) 接口(Interface)详解


目录

  1. 什么是接口?——用生活例子理解
  2. 为什么需要接口?——接口解决什么问题
  3. 接口的定义语法
  4. 实现接口
  5. 实现多个接口
  6. 接口的多态
  7. 显式接口实现
  8. 接口的继承
  9. C# 中常用的内置接口
  10. C# 8.0 新特性:接口默认实现
  11. 接口 vs 抽象类 —— 一张表说清楚
  12. 最佳实践与命名规范
  13. 总结:记住这5句话

1. 什么是接口?——用生活例子理解

生活类比:USB 接口

想象你电脑上的 USB 接口

  • USB 接口定义了一个标准规范(形状、电压、通信协议)
  • 只要符合这个规范,任何设备都能插上去使用:
    • U盘可以插
    • 鼠标可以插
    • 键盘可以插
    • 打印机可以插
  • 电脑不在乎你插的是什么,只在乎你遵循了 USB 的规范

在 C# 中,接口就是这样一个"规范/契约"

// 接口 = USB 接口的标准规范
public interface IUSB
{
    void Connect();    // 连接
    void Transfer();   // 传输数据
}

// 实现了 IUSB 的类 = 符合 USB 规范的设备
public class UDisk : IUSB { /* ... */ }     // U盘
public class Mouse : IUSB { /* ... */ }     // 鼠标
public class Keyboard : IUSB { /* ... */ }  // 键盘

一句话理解:接口定义了"能做什么",但不规定"怎么做"。每个类可以按自己的方式去实现。

另一个类比:餐厅菜单

// 接口就像餐厅菜单 —— 告诉你有什么菜可以点
// 但每道菜怎么做,每个厨师有自己的方式
public interface IMenu
{
    void MakeRice();      // 做米饭
    void MakeSoup();      // 做汤
    void MakeMainDish();  // 做主菜
}

// 中餐厨师实现菜单(用中餐的方式做)
public class ChineseChef : IMenu
{
    public void MakeRice() { Console.WriteLine("用电饭煲蒸米饭"); }
    public void MakeSoup() { Console.WriteLine("煲老火靓汤"); }
    public void MakeMainDish() { Console.WriteLine("炒宫保鸡丁"); }
}

// 西餐厨师实现菜单(用西餐的方式做)
public class WesternChef : IMenu
{
    public void MakeRice() { Console.WriteLine("做意大利烩饭"); }
    public void MakeSoup() { Console.WriteLine("做奶油蘑菇汤"); }
    public void MakeMainDish() { Console.WriteLine("煎牛排"); }
}

同样的菜单(接口),不同的厨师(类)有不同的实现方式。这就是接口的核心思想。


2. 为什么需要接口?——接口解决什么问题

问题场景:没有接口的世界

假设你在开发一个游戏,有战士、法师、弓箭手三种角色,他们都应该能"攻击":

// ========== 没有接口的做法(不好!)==========
public class Warrior
{
    public void SwingSword() { Console.WriteLine("挥剑攻击!"); }
}

public class Mage
{
    public void CastSpell() { Console.WriteLine("释放火球术!"); }
}

public class Archer
{
    public void ShootArrow() { Console.WriteLine("射箭!"); }
}

// 想让所有角色一起攻击时,代码会很难看:
class Game
{
    static void AllAttack()
    {
        Warrior warrior = new Warrior();
        Mage mage = new Mage();
        Archer archer = new Archer();

        warrior.SwingSword();   // 每个类型方法名都不一样
        mage.CastSpell();       // 无法统一处理
        archer.ShootArrow();    // 新增角色就要改代码
    }
}

痛点:每种角色攻击方法名不同,没法统一批量操作,新增角色就得修改大量代码。

解决方案:使用接口

// ========== 用接口解决 ==========

// 第一步:定义一个"可攻击"的接口
public interface IAttackable
{
    void Attack();  // 统一的方法名
}

// 第二步:各角色按自己的方式实现攻击
public class Warrior : IAttackable
{
    public void Attack() { Console.WriteLine("战士挥剑攻击!"); }
}

public class Mage : IAttackable
{
    public void Attack() { Console.WriteLine("法师释放火球术!"); }
}

public class Archer : IAttackable
{
    public void Attack() { Console.WriteLine("弓箭手射箭!"); }
}

// 第三步:统一处理所有角色
class Game
{
    static void AllAttack()
    {
        // 用接口类型统一管理!
        List<IAttackable> team = new List<IAttackable>
        {
            new Warrior(),
            new Mage(),
            new Archer(),
            // 将来新增角色:new Priest() ← 一行代码搞定!
        };

        // 全部统一调用 Attack(),无需关心具体是什么角色
        foreach (IAttackable member in team)
        {
            member.Attack();
        }
    }
}

接口带来的好处总结

好处 说明
统一标准 规定必须实现哪些方法,调用方心里有底
灵活替换 可以随时换一个实现,调用方代码不用改
解耦(松耦合) 调用方只依赖接口,不依赖具体类,修改影响小
便于扩展 新增功能只需新增一个实现了接口的类,不破坏现有代码
多继承能力 C# 一个类只能继承一个父类,但可以实现多个接口

3. 接口的定义语法

基本语法

// 访问修饰符 interface 接口名
// 习惯以大写字母 I 开头
public interface IPlayable
{
    // ===== 接口中可以包含的成员 =====

    // 1. 方法(只有签名,没有方法体)
    void Play();
    void Pause();
    void Stop();

    // 2. 属性(只有声明,没有实现)
    string Name { get; set; }
    int Duration { get; }

    // 3. 事件
    event EventHandler PlaybackEnded;

    // 4. 索引器
    string this[int index] { get; set; }
}

接口的重要规则

// ✅ 所有成员默认就是 public,不能加访问修饰符
public interface IDemo
{
    // public void Method1();  // ❌ 错误!不能加 public(本身就是公开的)

    void Method1();              // ✅ 正确,默认 public
    string Name { get; set; }   // ✅ 正确
}

// ✅ 接口不能有字段
public interface IWrong
{
    // int age;                 // ❌ 错误!接口不能包含字段
    int Age { get; set; }       // ✅ 正确,这是属性不是字段
}

// ✅ 接口不能有构造函数
public interface IWrong2
{
    // IWrong2() { }              // ❌ 错误!接口没有构造函数
}

// ✅ 接口不能有静态成员(C# 8.0 之前)
// C# 8.0+ 允许静态成员,后面会讲

记忆口诀:接口是"只说不做"——只声明有什么,不写具体怎么实现。


4. 实现接口

基本实现

// 定义接口
public interface IFlyable
{
    void TakeOff();     // 起飞
    void Fly();         // 飞行
    void Land();        // 降落
    int MaxHeight { get; }  // 最大飞行高度(只读属性)
}

// 实现接口:使用 冒号(:) 后跟接口名
public class Bird : IFlyable
{
    public string Name { get; set; }

    // 实现接口属性
    public int MaxHeight { get; private set; }

    public Bird(string name, int maxHeight)
    {
        Name = name; MaxHeight = maxHeight;
    }

    // 实现接口方法
    public void TakeOff()
    {
        Console.WriteLine($"{Name} 拍拍翅膀起飞了!");
    }

    public void Fly()
    {
        Console.WriteLine($"{Name} 在 {(MaxHeight / 2)} 米高空翱翔~");
    }

    public void Land()
    {
        Console.WriteLine($"{Name} 缓缓降落在树枝上。");
    }
}

// 另一个实现:飞机
public class Airplane : IFlyable
{
    public string Model { get; set; }
    public int MaxHeight { get; private set; }

    public Airplane(string model, int maxHeight)
    {
        Model = model; MaxHeight = maxHeight;
    }

    public void TakeOff()
    {
        Console.WriteLine($"{Model} 在跑道上加速,拉起机头!");
    }

    public void Fly()
    {
        Console.WriteLine($"{Model} 在 {MaxHeight} 米巡航高度飞行~");
    }

    public void Land()
    {
        Console.WriteLine($"{Model} 放下起落架,平稳着陆。");
    }
}

// ========== 使用 ==========
Bird sparrow = new Bird("小麻雀", 1000);
Airplane boeing = new Airplane("波音737", 10000);

sparrow.TakeOff();    // 小麻雀 拍拍翅膀起飞了!
boeing.TakeOff();     // 波音737 在跑道上加速,拉起机头!

// 通过接口引用操作
IFlyable f1 = sparrow;
IFlyable f2 = boeing;
f1.Fly();   // 小麻雀 在 500 米高空翱翔~
f2.Fly();   // 波音737 在 10000 米巡航高度飞行~

实现多个接口

C# 类只能继承一个基类,但可以同时实现多个接口——这是接口最强大的能力之一。

// ========== 定义多个接口 ==========

// 接口1:可充电
public interface IChargeable
{
    void Charge();
    int BatteryLevel { get; }
}

// 接口2:可拍照
public interface IPhotographable
{
    void TakePhoto();
    void RecordVideo();
}

// 接口3:可打电话
public interface ICallable
{
    void MakeCall(string phoneNumber);
    void ReceiveCall();
}

// ========== 一个类实现多个接口 ==========
// 用逗号分隔多个接口
public class Smartphone : IChargeable, IPhotographable, ICallable
{
    public string Model { get; set; }
    public int BatteryLevel { get; private set; } = 100;

    public Smartphone(string model) { Model = model; }

    // 实现 IChargeable
    public void Charge()
    {
        BatteryLevel = 100;
        Console.WriteLine($"{Model} 充电完成!电量:{BatteryLevel}%");
    }

    // 实现 IPhotographable
    public void TakePhoto()
    {
        Console.WriteLine($"{Model} 咔嚓!拍了一张照片。");
        BatteryLevel -= 5;
    }

    public void RecordVideo()
    {
        Console.WriteLine($"{Model} 开始录制视频...");
        BatteryLevel -= 10;
    }

    // 实现 ICallable
    public void MakeCall(string phoneNumber)
    {
        Console.WriteLine($"{Model} 正在拨打 {phoneNumber}...");
        BatteryLevel -= 3;
    }

    public void ReceiveCall()
    {
        Console.WriteLine($"{Model} 叮铃铃~ 有来电!");
    }
}

// ========== 使用:按需用不同接口类型操作 ==========
Smartphone phone = new Smartphone("iPhone 15");

// 作为手机使用(打电话)
ICallable callable = phone;
callable.MakeCall("10086");

// 作为相机使用(拍照)
IPhotographable camera = phone;
camera.TakePhoto();

// 作为电子设备使用(充电)
IChargeable device = phone;
device.Charge();

// 它们都是同一个对象,只是通过不同"身份"来使用!
Console.WriteLine($"当前电量:{phone.BatteryLevel}%");

关键理解:同一个对象 Smartphone,通过 ICallable 接口就是"电话",通过 IPhotographable 接口就是"相机"。对象还是那个对象,只是我们通过不同的"视角"看它。


5. 实现多个接口

这一节深入介绍多接口实现的更多技巧。

同时继承类 + 实现接口

// 基类
public class Device
{
    public string SerialNumber { get; set; }
    public DateTime ManufactureDate { get; set; }

    public Device()
    {
        SerialNumber = Guid.NewGuid().ToString().Substring(0, 8);
        ManufactureDate = DateTime.Now;
    }

    public void PowerOn() { Console.WriteLine("设备已开机"); }
    public void PowerOff() { Console.WriteLine("设备已关机"); }
}

// 接口
public interface IWiFi
{
    void ConnectWiFi(string ssid);
}

public interface IBluetooth
{
    void PairDevice(string deviceName);
}

// ========== 同时继承类 + 实现多个接口 ==========
// ⚠️ 语法规则:基类必须写在第一个,接口写在后面
public class Tablet : Device, IWiFi, IBluetooth
{
    public string Model { get; set; }

    public Tablet(string model) { Model = model; }

    // 实现 IWiFi
    public void ConnectWiFi(string ssid)
    {
        Console.WriteLine($"{Model} 正在连接 WiFi: {ssid}...");
    }

    // 实现 IBluetooth
    public void PairDevice(string deviceName)
    {
        Console.WriteLine($"{Model} 正在配对蓝牙设备: {deviceName}...");
    }
}

// ========== 使用 ==========
Tablet tablet = new Tablet("iPad Pro");
tablet.PowerOn();                       // 继承自 Device
tablet.ConnectWiFi("MyHomeNetwork");    // 来自 IWiFi
tablet.PairDevice("AirPods");           // 来自 IBluetooth

// 错误示范:
// public class Wrong : IWiFi, Device { }  // ❌ 基类不能写在接口后面

通过 isas 检查接口

// 场景:我们有一个对象,不确定它支持什么功能
object something = new Smartphone("小米12");

// ========== 方式1:用 is 判断(推荐)==========
if (something is IChargeable chargeable)
{
    chargeable.Charge();  // 安全使用
    Console.WriteLine("这个对象可以充电!");
}

if (something is IPhotographable camera)
{
    camera.TakePhoto();  // 安全使用
    Console.WriteLine("这个对象可以拍照!");
}

// ========== 方式2:用 as 转换 ==========
ICallable phone = something as ICallable;
if (phone != null)
{
    phone.MakeCall("110");
}
else
{
    Console.WriteLine("这个对象不能打电话");
}

// ========== 方式3:多个接口判断 ==========
if (something is IChargeable && something is IPhotographable)
{
    Console.WriteLine("这是一个既能充电又能拍照的多功能设备!");
}

6. 接口的多态

接口多态是接口最核心的应用——通过接口类型统一操作不同实现类。

// 定义接口
public interface IPayment
{
    void Pay(decimal amount);
    string GetPaymentMethod();
}

// ========== 各种支付实现 ==========
public class AliPay : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"支付宝支付 ¥{amount},扫码完成!");
    }
    public string GetPaymentMethod() => "支付宝";
}

public class WeChatPay : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"微信支付 ¥{amount},指纹验证通过!");
    }
    public string GetPaymentMethod() => "微信支付";
}

public class CreditCard : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"信用卡支付 ¥{amount},已发送验证码!");
    }
    public string GetPaymentMethod() => "信用卡";
}

public class Cash : IPayment
{
    public void Pay(decimal amount)
    {
        Console.WriteLine($"现金支付 ¥{amount},请当面点清!");
    }
    public string GetPaymentMethod() => "现金";
}

// ========== 多态应用:统一处理所有支付方式 ==========
class ShoppingCart
{
    // 这个方法不需要知道具体是什么支付方式!
    public static void Checkout(IPayment payment, decimal total)
    {
        Console.Write($"使用 {payment.GetPaymentMethod()} ");
        payment.Pay(total);
    }

    // 批量结算
    public static void BatchCheckout(List<IPayment> payments, decimal amount)
    {
        foreach (var payment in payments)
        {
            payment.Pay(amount / payments.Count);
        }
    }
}

// ========== 运行 ==========
class Program
{
    static void Main()
    {
        // 用接口类型统一管理不同支付方式
        List<IPayment> paymentMethods = new List<IPayment>
        {
            new AliPay(),
            new WeChatPay(),
            new CreditCard(),
            new Cash()
        };

        Console.WriteLine("===== 请选择支付方式 =====");
        for (int i = 0; i < paymentMethods.Count; i++)
        {
            Console.WriteLine($"{i + 1}. {paymentMethods[i].GetPaymentMethod()}");
        }

        // 选第2种(微信支付)
        ShoppingCart.Checkout(paymentMethods[1], 299.99m);
        // 输出:使用 微信支付 微信支付 ¥299.99,指纹验证通过!

        // 以后想新增支付方式?只需要新建一个实现 IPayment 的类即可
        // 上面的 ShoppingCart 代码不需要任何修改!
    }
}

多态的核心价值:ShoppingCart 只依赖 IPayment 接口,不依赖任何具体的支付类。新增支付方式时,ShoppingCart 的代码一行都不需要改。这就是"对扩展开放,对修改关闭"的开闭原则。


7. 显式接口实现

当一个类实现了多个接口,而这些接口中有同名方法时,需要用"显式接口实现"来解决冲突。

为什么需要显式实现?

// 两个接口都有同名方法 Greet()
public interface IEnglish
{
    void Greet();
}

public interface IChinese
{
    void Greet();
}

问题:一个类同时实现这两个接口,Greet() 方法到底是英语打招呼还是中文打招呼?

解决方案:显式接口实现

public class BilingualPerson : IEnglish, IChinese
{
    // ========== 方式1:显式实现(每个接口各写一份)==========

    // ⚠️ 显式实现不能加 public 修饰符!
    // ⚠️ 必须用 接口名.方法名 的格式
    void IEnglish.Greet()
    {
        Console.WriteLine("Hello! Nice to meet you!");
    }

    void IChinese.Greet()
    {
        Console.WriteLine("你好!很高兴认识你!");
    }

    // ========== 方式2:公共方法(普通实现)==========
    // 如果你还想要一个"默认"的问候方式
    public void Greet()
    {
        IChinese chinese = this;
        chinese.Greet();  // 默认说中文
        // 或者两手都来:
        // ((IEnglish)this).Greet();
    }
}

// ========== 使用:必须通过对应接口来调用 ==========
BilingualPerson person = new BilingualPerson();

// 普通调用 → 调公共方法
person.Greet();  // 输出:你好!很高兴认识你!

// 通过 IEnglish 接口 → 调英文问候
IEnglish english = person;
english.Greet();  // 输出:Hello! Nice to meet you!

// 通过 IChinese 接口 → 调中文问候
IChinese chinese = person;
chinese.Greet();  // 输出:你好!很高兴认识你!

// ========== 显式实现的语法特点 ==========
// 1. 不能加访问修饰符(不能写 public、private 等)
// 2. 必须加接口名前缀: 接口名.方法名
// 3. 不能通过类的实例直接调用,必须转为接口类型
// 4. 如果同一个方法在多个接口中,需要各自显式实现

更复杂的例子:文件管理器

public interface ISave
{
    void Execute(string path);   // 保存
}

public interface ILoad
{
    void Execute(string path);   // 加载(同名但不同含义!)
}

public class FileManager : ISave, ILoad
{
    // 显式实现 ISave.Execute —— 保存文件
    void ISave.Execute(string path)
    {
        Console.WriteLine($"正在保存文件到:{path}");
        // 保存逻辑...
    }

    // 显式实现 ILoad.Execute —— 加载文件
    void ILoad.Execute(string path)
    {
        Console.WriteLine($"正在加载文件:{path}");
        // 加载逻辑...
    }

    // 公共方法:根据需求选择
    public void Execute(string operation, string path)
    {
        if (operation == "save")
            ((ISave)this).Execute(path);
        else if (operation == "load")
            ((ILoad)this).Execute(path);
    }
}

// ========== 使用 ==========
FileManager fm = new FileManager();

// 方式1:通过接口
ISave saver = fm;
saver.Execute(@"D:\data.txt");    // 保存

ILoad loader = fm;
loader.Execute(@"D:\data.txt");   // 加载

// 方式2:通过公共方法
fm.Execute("save", @"D:\backup.txt");
fm.Execute("load", @"D:\backup.txt");

什么时候用显式接口实现?

  • 多接口有同名方法
  • 某些接口方法不希望被类的实例直接调用,必须通过接口访问
  • 想让 API 更干净,隐藏接口的实现细节

8. 接口的继承

接口也可以继承其他接口,形成接口的层次结构。

// ========== 基础接口 ==========
public interface IDrawable
{
    void Draw();
    string Color { get; set; }
}

// ========== 接口继承接口 ==========
// 一个接口可以继承多个接口
public interface IResizableDrawable : IDrawable
{
    void Resize(double width, double height);
    double Width { get; }
    double Height { get; }
}

// ========== 再继承一层 ==========
public interface IMovableDrawable : IResizableDrawable
{
    void MoveTo(double x, double y);
    double X { get; }
    double Y { get; }
}

// ========== 实现最底层接口即可 ==========
// 实现 IMovableDrawable 需要实现它所有父接口的所有成员
public class Rectangle : IMovableDrawable
{
    public string Color { get; set; } = "红色";
    public double Width { get; private set; }
    public double Height { get; private set; }
    public double X { get; private set; }
    public double Y { get; private set; }

    public Rectangle(double width, double height)
    {
        Width = width; Height = height;
    }

    // 实现 IDrawable
    public void Draw()
    {
        Console.WriteLine($"绘制 {Color} 矩形于 ({X},{Y}),大小 {Width}x{Height}");
    }

    // 实现 IResizableDrawable
    public void Resize(double width, double height)
    {
        Width = width; Height = height;
        Console.WriteLine($"矩形尺寸已调整为 {Width}x{Height}");
    }

    // 实现 IMovableDrawable
    public void MoveTo(double x, double y)
    {
        X = x; Y = y;
        Console.WriteLine($"矩形已移动到 ({X},{Y})");
    }
}

// ========== 使用 ==========
Rectangle rect = new Rectangle(100, 50);

// 通过不同层级的接口使用
IDrawable drawable = rect;
drawable.Draw();   // 只能 Draw()

IResizableDrawable resizable = rect;
resizable.Resize(200, 100);  // 可以 Resize()

IMovableDrawable movable = rect;
movable.MoveTo(50, 30);      // 可以 MoveTo()
movable.Draw();              // 也可以 Draw()(继承来的)

// ========== 接口继承示意图 ==========
//
//   IDrawable          ← 基础接口:有 Draw()、Color
//       ↑
//   IResizableDrawable ← 扩展接口:+ Resize()、Width、Height
//       ↑
//   IMovableDrawable   ← 最终接口:+ MoveTo()、X、Y
//       ↑
//   Rectangle          ← 实现类:必须实现以上全部

接口继承的价值:当你看 IMovableDrawable 的时候,你就知道实现它的类既能画、又能改大小、还能移动。


9. C# 中常用的内置接口

.NET 框架提供了大量内置接口,掌握它们能让你写出更优雅的代码。

9.1 IEnumerable<T> — 让对象可被 foreach 遍历

using System.Collections;
using System.Collections.Generic;

// ========== 实现 IEnumerable<T> ==========
// 让你的类支持 foreach 循环
public class MyCollection : IEnumerable<string>
{
    private List<string> items = new List<string>();

    public void Add(string item) => items.Add(item);

    // 必须实现的泛型版本
    public IEnumerator<string> GetEnumerator()
    {
        foreach (var item in items)
        {
            yield return item;  // yield return 是迭代器的简易写法
        }
    }

    // 必须实现的非泛型版本(显式实现)
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

// ========== 使用 ==========
MyCollection fruits = new MyCollection();
fruits.Add("苹果");
fruits.Add("香蕉");
fruits.Add("橘子");

// 现在可以用 foreach 遍历了!
foreach (string fruit in fruits)
{
    Console.WriteLine(fruit);
}

// 也可以用 LINQ!
var result = fruits.Where(f => f.Contains("果")).ToList();

9.2 IComparable<T> — 让对象可以排序

// ========== 让“学生”可以按分数排序 ==========
public class Student : IComparable<Student>
{
    public string Name { get; set; }
    public int Score { get; set; }

    public Student(string name, int score)
    {
        Name = name; Score = score;
    }

    // 实现 CompareTo:返回负数表示排在前面,正数表示排在后面,0表示相等
    public int CompareTo(Student other)
    {
        if (other == null) return 1;

        // 按分数降序排列
        return other.Score.CompareTo(this.Score);
    }

    public override string ToString() => $"{Name}: {Score}分";
}

// ========== 使用 ==========
List<Student> students = new List<Student>
{
    new Student("张三", 85),
    new Student("李四", 92),
    new Student("王五", 78),
    new Student("赵六", 88)
};

students.Sort();  // 自动调用 CompareTo 进行排序!

Console.WriteLine("===== 按成绩排名 =====");
foreach (var s in students)
{
    Console.WriteLine(s);
}
// 输出:
//   李四: 92分
//   赵六: 88分
//   张三: 85分
//   王五: 78分

9.3 IDisposable — 正确释放资源

// ========== 实现 IDisposable ==========
// 用于释放非托管资源(文件句柄、数据库连接、网络连接等)
public class FileLogger : IDisposable
{
    private StreamWriter writer;
    private bool disposed = false;

    public FileLogger(string filePath)
    {
        writer = new StreamWriter(filePath, append: true);
    }

    public void Log(string message)
    {
        if (disposed)
            throw new ObjectDisposedException(nameof(FileLogger));

        writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}");
    }

    // 实现 Dispose 方法
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);  // 告诉垃圾回收器:不用调析构函数了
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // 释放托管资源
                writer?.Close();
                writer?.Dispose();
            }
            // 释放非托管资源(如果有的话)
            disposed = true;
        }
    }

    // 析构函数:最后的保险
    ~FileLogger()
    {
        Dispose(false);
    }
}

// ========== 使用(推荐 using 语句,自动调用 Dispose)==========
// using 语句结束时,自动调用 Dispose(),即使发生异常也会调用
using (var logger = new FileLogger(@"D:\app.log"))
{
    logger.Log("应用启动");
    logger.Log("执行任务中...");
}  // ← 离开 using 块时自动释放资源

Console.WriteLine("文件日志已安全关闭。");

9.4 IEquatable<T> — 判断对象相等

public class Person : IEquatable<Person>
{
    public string IdCard { get; set; }   // 身份证号
    public string Name { get; set; }

    public Person(string idCard, string name)
    {
        IdCard = idCard; Name = name;
    }

    // 身份证号相同就视为同一个人
    public bool Equals(Person other)
    {
        if (other == null) return false;
        return this.IdCard == other.IdCard;
    }

    // 重写 Equals(与 IEquatable 保持一致)
    public override bool Equals(object obj)
    {
        if (obj is Person person)
            return Equals(person);
        return false;
    }

    public override int GetHashCode()
    {
        return IdCard.GetHashCode();
    }
}

// 使用
Person p1 = new Person("440101199001011234", "张三");
Person p2 = new Person("440101199001011234", "张三丰");  // 同名不同名不重要

Console.WriteLine(p1.Equals(p2));  // True!因为身份证号相同

9.5 其他常用接口速查

接口 用途 关键方法/属性
IEnumerable<T> 支持 foreach 遍历 GetEnumerator()
IComparable<T> 支持排序(Sort) CompareTo(T)
ICloneable 支持克隆对象 Clone()
IDisposable 释放资源 Dispose()
IEquatable<T> 判断相等 Equals(T)
INotifyPropertyChanged 属性变化通知(WPF常用) PropertyChanged 事件
IList<T> 列表操作(继承 ICollection) Add, Remove, this[int]
IDictionary<K,V> 字典操作 Add, ContainsKey, this[K]

10. C# 8.0 新特性:接口默认实现

C# 8.0 开始,接口方法可以拥有默认实现。这是一个重大变化!

为什么需要默认实现?

问题场景:你发布了一个接口,很多类实现了它。后来想给接口加新方法,但所有实现类都要改——怎么办?

// ========== 老版本(C# 7.x):惨案 ==========
public interface ILogger
{
    void Log(string message);
    void LogError(string message);
}

// 已有 100 个类实现了 ILogger...

// 想加一个新方法 LogWarning,但 100 个类都要改!
// 这就是"接口版本化"的痛点

C# 8.0 解决方案:接口默认实现

// ========== C# 8.0+:优雅解决 ==========
public interface ILogger
{
    // 没有实现的(必须由实现类提供)
    void Log(string message);
    void LogError(string message);

    // 有默认实现的(实现类可以选择不重写)
    void LogWarning(string message)
    {
        // 默认实现:降级为普通日志
        Log($"[警告] {message}");
    }

    void LogInfo(string message)
    {
        Log($"[信息] {message}");
    }

    // 还可以有默认实现的属性!
    string LoggerName => "默认日志器";
}

// ========== 旧实现类不需要修改,自动获得新功能 ==========
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }

    public void LogError(string message)
    {
        Console.WriteLine($"[ERROR] {message}");
    }

    // 不需要实现 LogWarning 和 LogInfo —— 自动获得默认实现!
    // 也不需要实现 LoggerName —— 自动获得默认值!
}

// ========== 新实现类可以选择重写默认实现 ==========
public class FileLogger : ILogger
{
    public void Log(string message) { /* 写文件... */ }
    public void LogError(string message) { /* 写文件... */ }

    // 重写默认实现,提供更好的版本
    public void LogWarning(string message)
    {
        Log($"[WARN] {message}");  // 自己的格式
    }
}

// ========== 使用 ==========
ILogger logger = new ConsoleLogger();
logger.LogInfo("系统启动中...");     // 使用默认实现
logger.LogWarning("内存不足!");     // 使用默认实现
Console.WriteLine(logger.LoggerName); // 输出:默认日志器

默认实现的注意事项

public interface IAnimal
{
    void MakeSound();

    // 默认实现
    void Sleep()
    {
        Console.WriteLine("Zzz...动物在睡觉");
    }

    // 静态成员(C# 8.0 也支持)
    static string Kingdom => "动物界";
}

public class Dog : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("汪汪!");
    }

    // Sleep 可以不实现,自动用默认的
    // 也可以重写:
    // public void Sleep() { Console.WriteLine("狗在窝里睡觉..."); }
}

// ========== ⚠️ 重要限制 ==========
Dog dog = new Dog();
// dog.Sleep();  // ❌ 编译错误!默认实现只能通过接口类型调用

IAnimal animal = dog;
animal.Sleep();  // ✅ 必须通过接口变量调用

// 这是因为默认实现不能访问实例字段(接口没有字段),
// C# 设计者决定默认方法必须通过接口类型显式调用,避免误解

理解要点:默认实现主要解决"接口演化"问题——不破坏已有代码的前提下给接口增加新功能。


11. 接口 vs 抽象类 —— 一张表说清楚

这是面试和实际开发中最常被问到的问题。

对比维度 接口(Interface) 抽象类(Abstract Class)
关键字 interface abstract class
可以有字段 ❌ 不能有字段 ✅ 可以有字段
构造函数 ❌ 不能有 ✅ 可以有(给子类用)
方法实现 ❌(C# 8.0前)/ ✅(C# 8.0+默认) ✅ 可以有具体实现
访问修饰符 所有成员默认 public,不能改 可以灵活设置 public/protected 等
多继承 ✅ 一个类可以实现多个接口 ❌ 一个类只能继承一个抽象类
语义关系 "能做什么"(能力/契约) "是什么"(继承关系)
版本演化 C# 8.0+ 支持默认实现,较灵活 可以随意添加方法实现,更灵活
适用场景 跨不同类型的功能定义 有共同基类且有共享代码的场景

决策指南:用接口还是抽象类?

                    开始
                     │
                     v
          ┌─────────────────────┐
          │  这些类是否有共同的基础状态  │
          │  (字段、已实现的方法等)?  │
          └──────────┬──────────┘
                     │
        ┌────────────┼────────────┐
        │是                        │否
        v                         v
  ┌──────────┐             ┌──────────────┐
  │ 用抽象类  │             │ 可能需要多个   │
  │          │             │ 这样的"契约"? │
  └──────────┘             └──────┬───────┘
                                  │
                    ┌─────────────┼─────────────┐
                    │是                          │否
                    v                            v
             ┌──────────┐                ┌──────────┐
             │ 用接口    │                │ 都可以     │
             │          │                │(优先接口)│
             └──────────┘                └──────────┘

具体例子对比

// ========== 场景:动物和飞行能力 ==========

// 抽象类:"是什么"
// 动物有共同的行为和状态
public abstract class Animal
{
    public string Name { get; set; }    // ✅ 字段
    public int Age { get; set; }

    public Animal(string name)          // ✅ 构造函数
    {
        Name = name;
    }

    public void Eat()                   // ✅ 已实现的方法
    {
        Console.WriteLine($"{Name} 在吃东西");
    }

    public abstract void MakeSound();   // 抽象方法:子类必须实现
}

// 接口:"能做什么"
// 飞行是一种能力,不是所有动物都有
public interface IFlyable
{
    void Fly();
    int MaxHeight { get; }
}

public interface ISwimmable
{
    void Swim();
}

// ========== 使用组合 ==========
// 鸟:是动物 + 能飞 + 能游泳(某些鸟会游泳)
public class Duck : Animal, IFlyable, ISwimmable
{
    public Duck(string name) : base(name) { }

    public int MaxHeight => 100;

    public override void MakeSound() => Console.WriteLine("嘎嘎!");

    public void Fly() => Console.WriteLine($"{Name} 拍拍翅膀飞,最高 {MaxHeight} 米");

    public void Swim() => Console.WriteLine($"{Name} 在水面悠闲地游");
}

// 鱼:是动物 + 能游泳,但不能飞
public class Goldfish : Animal, ISwimmable
{
    public Goldfish(string name) : base(name) { }

    public override void MakeSound() => Console.WriteLine("...(鱼不会叫)");

    public void Swim() => Console.WriteLine($"{Name} 在水中游来游去");
}

总结一句话:抽象类回答"是什么",接口回答"能做什么"。


12. 最佳实践与命名规范

命名规范

// ✅ 接口名以 I 开头,Pascal 命名法
public interface IDisposable { }
public interface IEnumerable { }
public interface IComparable<T> { }
public interface IUserService { }
public interface IOrderRepository { }

// ❌ 错误命名
public interface DisposableInterface { }   // 没有 I 前缀
public interface iDisposable { }           // i 应该大写
public interface Idisposable { }           // D 应该大写

// ✅ 泛型接口:I + 名称 + <T>
public interface IRepository<T> { }
public interface IService<TInput, TOutput> { }

// ✅ 能力类接口:I + 形容词(-able 结尾)
public interface ISerializable { }
public interface IComparable { }
public interface ICloneable { }
public interface IObservable<T> { }

接口设计原则

原则1:接口隔离原则(ISP)

不要让实现类依赖它不需要的方法!

// ❌ 坏设计:一个臃肿的接口
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
    void TakeVacation();
    void AttendMeeting();
    void WriteReport();
}

// ✅ 好设计:拆分成独立的小接口
public interface IWorkable
{
    void Work();
}

public interface IEatable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public interface IMeetingAttendee
{
    void AttendMeeting();
}

// 每个类只实现自己需要的接口
public class Robot : IWorkable              // 机器人只要工作
{
    public void Work() { Console.WriteLine("机器人正在工作..."); }
}

public class Human : IWorkable, IEatable, ISleepable  // 人类需要更多
{
    public void Work() { Console.WriteLine("人类在工作"); }
    public void Eat() { Console.WriteLine("人类在吃饭"); }
    public void Sleep() { Console.WriteLine("人类在睡觉"); }
}

接口隔离原则:多个专用接口优于一个庞杂的大接口。

原则2:接口应小且专注

// ❌ “瑞士军刀”式接口(什么都想做)
public interface IDataAccess
{
    void Connect();
    void Disconnect();
    User GetUser(int id);
    void SaveUser(User user);
    Order GetOrder(int id);
    void SaveOrder(Order order);
    void SendEmail(string to, string subject);
    void GeneratePdfReport();
    // ... 20+ 方法
}

// ✅ 拆分为职责单一的接口
public interface IDatabaseConnection
{
    void Connect();
    void Disconnect();
}

public interface IUserRepository
{
    User GetUser(int id);
    void SaveUser(User user);
}

public interface IOrderRepository
{
    Order GetOrder(int id);
    void SaveOrder(Order order);
}

public interface IEmailService
{
    void SendEmail(string to, string subject, string body);
}

public interface IReportGenerator
{
    byte[] GeneratePdfReport(object data);
}

原则3:依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖接口。

// ❌ 坏设计:Business 直接依赖具体的 DataAccess
public class BusinessLogic
{
    private SqlDataAccess db = new SqlDataAccess();  // 紧耦合!

    public void Process()
    {
        var data = db.GetData();  // 如果换成 MySQL 就要改代码
    }
}

// ✅ 好设计:都依赖接口
public interface IDataAccess
{
    Data GetData();
}

public class SqlDataAccess : IDataAccess
{
    public Data GetData() { /* SQL Server 实现 */ }
}

public class MySqlDataAccess : IDataAccess
{
    public Data GetData() { /* MySQL 实现 */ }
}

public class BusinessLogic
{
    private IDataAccess db;  // 依赖接口,不依赖具体类

    public BusinessLogic(IDataAccess dataAccess)
    {
        db = dataAccess;     // 依赖注入:谁用谁传
    }

    public void Process()
    {
        var data = db.GetData();  // 不关心是 SQL Server 还是 MySQL
    }
}

// 使用时灵活切换
BusinessLogic logic1 = new BusinessLogic(new SqlDataAccess());
BusinessLogic logic2 = new BusinessLogic(new MySqlDataAccess());
// BusinessLogic 代码完全不需要改动!

常见设计模式中的接口

// ========== 策略模式(Strategy Pattern)==========
// 不同的计算策略实现同一接口
public interface IShippingStrategy
{
    decimal Calculate(decimal weight, string destination);
}

public class StandardShipping : IShippingStrategy
{
    public decimal Calculate(decimal weight, string destination)
        => weight * 10;
}

public class ExpressShipping : IShippingStrategy
{
    public decimal Calculate(decimal weight, string destination)
        => weight * 20 + 15;
}

public class FreeShipping : IShippingStrategy
{
    public decimal Calculate(decimal weight, string destination)
        => 0;  // 免费!
}

// ========== 工厂方法 ==========
public interface IProduct
{
    string Name { get; }
    decimal Price { get; }
}

public interface IProductFactory
{
    IProduct Create();
}

public class BookFactory : IProductFactory
{
    public IProduct Create() => new Book();
}

public class ElectronicsFactory : IProductFactory
{
    public IProduct Create() => new Electronics();
}

13. 总结:记住这5句话

学习接口,记住下面 5 句话就够了:

  1. 接口是规范/契约:定义了"能做什么",不规定"怎么做"
  2. 一个类可以实现多个接口:弥补 C# 不能多继承的缺憾,赋予对象多种"能力"
  3. 接口实现多态:通过接口类型统一操作不同的实现类,是解耦的关键手段
  4. 接口前缀是 I:这是 C# 的命名约定,看到 I 开头就知道是接口
  5. 面向接口编程:依赖接口而不是具体类,让代码更灵活、更易扩展、更易测试

一个完整的教学案例:动物园管理

using System;
using System.Collections.Generic;
using System.Linq;

// ===== 接口定义 =====
public interface IAnimal
{
    string Name { get; }
    void Speak();       // 发声
    void Move();        // 移动
}

public interface IFlyable
{
    void Fly();
}

public interface ISwimmable
{
    void Swim();
}

public interface ITrainable
{
    void LearnTrick(string trick);
    void Perform();
}

// ===== 各种动物 =====
public class Lion : IAnimal
{
    public string Name { get; }
    public Lion(string name) => Name = name;
    public void Speak() => Console.WriteLine($"{Name}:吼~~~!");
    public void Move() => Console.WriteLine($"{Name} 威风凛凛地踱步");
}

public class Eagle : IAnimal, IFlyable
{
    public string Name { get; }
    public Eagle(string name) => Name = name;
    public void Speak() => Console.WriteLine($"{Name}:唳——!");
    public void Move() => Console.WriteLine($"{Name} 用爪子抓地走路");
    public void Fly() => Console.WriteLine($"{Name} 展翅高飞,直冲云霄!");
}

public class Dolphin : IAnimal, ISwimmable, ITrainable
{
    public string Name { get; }
    private List<string> tricks = new List<string>();

    public Dolphin(string name) => Name = name;
    public void Speak() => Console.WriteLine($"{Name}:叽叽!");
    public void Move() => Console.WriteLine($"{Name} 在水中自由游动");
    public void Swim() => Console.WriteLine($"{Name} 跃出水面,画出美丽的弧线!");
    public void LearnTrick(string trick)
    {
        tricks.Add(trick);
        Console.WriteLine($"{Name} 学会了新技巧:{trick}");
    }
    public void Perform()
    {
        Console.WriteLine($"===== {Name} 的表演时间 =====");
        foreach (var trick in tricks)
            Console.WriteLine($"  ★ {trick}");
        Console.WriteLine("============================");
    }
}

public class Penguin : IAnimal, ISwimmable
{
    public string Name { get; }
    public Penguin(string name) => Name = name;
    public void Speak() => Console.WriteLine($"{Name}:嘎嘎!");
    public void Move() => Console.WriteLine($"{Name} 摇摇晃晃地走路");
    public void Swim() => Console.WriteLine($"{Name} 嗖的一下滑入水中!");
}

// ===== 动物园管理 =====
class Zoo
{
    static void Main()
    {
        // 所有动物
        List<IAnimal> allAnimals = new List<IAnimal>
        {
            new Lion("辛巴"),
            new Eagle("鹰王"),
            new Dolphin("小海"),
            new Penguin("企企")
        };

        Console.WriteLine("===== 1. 所有动物大点名 =====");
        foreach (var animal in allAnimals)
        {
            animal.Speak();
            animal.Move();
        }

        Console.WriteLine("\n===== 2. 飞行表演 =====");
        foreach (var animal in allAnimals)
        {
            if (animal is IFlyable flyer)
            {
                flyer.Fly();
            }
        }

        Console.WriteLine("\n===== 3. 游泳表演 =====");
        foreach (var animal in allAnimals)
        {
            if (animal is ISwimmable swimmer)
            {
                swimmer.Swim();
            }
        }

        Console.WriteLine("\n===== 4. 训练海豚 =====");
        // 找到海豚并训练
        var dolphin = allAnimals.OfType<ITrainable>().FirstOrDefault();
        if (dolphin != null)
        {
            dolphin.LearnTrick("跳圈");
            dolphin.LearnTrick("顶球");
            dolphin.LearnTrick("转圈");
            dolphin.Perform();
        }

        Console.WriteLine("\n===== 5. 动物能力统计 =====");
        Console.WriteLine($"飞行选手:{allAnimals.Count(a => a is IFlyable)} 位");
        Console.WriteLine($"游泳选手:{allAnimals.Count(a => a is ISwimmable)} 位");
        Console.WriteLine($"受训选手:{allAnimals.Count(a => a is ITrainable)} 位");
    }
}

/* 运行结果:
===== 1. 所有动物大点名 =====
辛巴:吼~~~!
辛巴 威风凛凛地踱步
鹰王:唳——!
鹰王 用爪子抓地走路
小海:叽叽!
小海 在水中自由游动
企企:嘎嘎!
企企 摇摇晃晃地走路

===== 2. 飞行表演 =====
鹰王 展翅高飞,直冲云霄!

===== 3. 游泳表演 =====
小海 跃出水面,画出美丽的弧线!
企企 嗖的一下滑入水中!

===== 4. 训练海豚 =====
小海 学会了新技巧:跳圈
小海 学会了新技巧:顶球
小海 学会了新技巧:转圈
===== 小海 的表演时间 =====
  ★ 跳圈
  ★ 顶球
  ★ 转圈
============================

===== 5. 动物能力统计 =====
飞行选手:1 位
游泳选手:2 位
受训选手:1 位
*/

写在最后:接口是 C# 中最重要的抽象机制之一。掌握了接口,你就掌握了编写灵活、可扩展、易维护代码的钥匙。建议多写代码、反复练习,尤其多体会"面向接口编程"的设计思想。

0
博主关闭了当前页面的评论