CSharp(二十九) 接口(Interface)详解
目录
- 什么是接口?——用生活例子理解
- 为什么需要接口?——接口解决什么问题
- 接口的定义语法
- 实现接口
- 实现多个接口
- 接口的多态
- 显式接口实现
- 接口的继承
- C# 中常用的内置接口
- C# 8.0 新特性:接口默认实现
- 接口 vs 抽象类 —— 一张表说清楚
- 最佳实践与命名规范
- 总结:记住这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 { } // ❌ 基类不能写在接口后面
通过 is 和 as 检查接口
// 场景:我们有一个对象,不确定它支持什么功能
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 句话就够了:
- 接口是规范/契约:定义了"能做什么",不规定"怎么做"
- 一个类可以实现多个接口:弥补 C# 不能多继承的缺憾,赋予对象多种"能力"
- 接口实现多态:通过接口类型统一操作不同的实现类,是解耦的关键手段
- 接口前缀是 I:这是 C# 的命名约定,看到
I开头就知道是接口 - 面向接口编程:依赖接口而不是具体类,让代码更灵活、更易扩展、更易测试
一个完整的教学案例:动物园管理
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# 中最重要的抽象机制之一。掌握了接口,你就掌握了编写灵活、可扩展、易维护代码的钥匙。建议多写代码、反复练习,尤其多体会"面向接口编程"的设计思想。