1. 什么是接口?
如果说抽象类是带点实现的“半成品”模板,那接口就是一堆要求清单:某个东西(类)必须会啥,才能在某个抽象场景下被用。
在编程里,接口就像是个“合同”或者“要求清单”,规定了对象要有啥行为。它描述了一组公开的方法、属性、索引器和事件,实现这个接口的类必须提供这些东西。接口就像在说:“你要想叫IDriveable(能开车的),那你就得有Drive()和Stop()方法。”
你可以把接口想象成员工要求清单:比如你要招“厨师”,那接口里就写着:“会做饭、会上菜、会洗手”。具体厨师怎么做这些事——那是他自己的事。只要外面看起来都按合同来就行。
注意:接口只描述要有啥,不管怎么做。
用OOP术语简单说
- 接口描述了对象的外部“样子”(API):能干啥。
- 没有状态(没有字段)。传统理解下接口也没有实现,但现在C#新版本有例外(比如默认方法),后面会细说。
- 一个类可以实现任意多个接口(不像类继承只能一个!)。
生活中的类比
USB接口——大家都知道可以插鼠标、键盘、U盘、耳机,甚至咖啡机(真的有!)。“鼠标”里面啥样不重要,只要有USB口、支持协议就行。这就是接口!
接口有啥用?
- 降低耦合。代码只和接口打交道,不管具体类。我们“面向接口编程”。
- 能实现多继承行为:一个类能实现好几个接口。
- 标准化:可以做通用机制:比如所有能比较的对象都实现IComparable接口。
- 方便测试:测试时可以用“假实现”替换真实现。
- 插件扩展:能加新“模块”,不用改老代码。
2. 接口的声明语法
来点代码!在C#里,接口用关键字interface声明,名字一般都用I开头:
// 声明接口
public interface IPrintable
{
// 抽象方法——合同
void Print();
// 也可以声明属性
string Name { get; set; }
}
记住:接口的方法和属性没有实现(没有方法体,只有签名——和抽象方法一样)。
接口的关键点
- 不能在接口里声明字段(成员变量)。
- 所有方法、属性、事件和索引器默认都是public(实现时也必须是)。
- 接口不能有构造函数(因为没状态)。
- 接口不管实现它的类是抽象的还是具体的。
接口实战
来,把接口加到我们的学习小项目里。假设现在有个“可打印”接口(IPrintable),被Report类实现,还有个新类Invoice也实现它。
public interface IPrintable
{
void Print();
string Name { get; set; }
}
现在定义个实现这个接口的类:
public class Report : IPrintable
{
public string Name { get; set; }
public Report(string name)
{
Name = name;
}
// 实现接口的方法
public void Print()
{
Console.WriteLine($"打印报告: {Name}");
}
}
再来一个完全不同的类,但接口一样:
public class Invoice : IPrintable
{
public string Name { get; set; }
public Invoice(string name)
{
Name = name;
}
public void Print()
{
Console.WriteLine($"打印发票: {Name}");
}
}
现在我们可以写个方法,能处理任何可打印的东西:
public static void PrintAnything(IPrintable printable)
{
printable.Print(); // 就这!不管是报告还是发票——只要能打印就行。
}
用法示例:
var report = new Report("月度报告");
var invoice = new Invoice("发票 #12345");
PrintAnything(report); // 打印报告: 月度报告
PrintAnything(invoice); // 打印发票: 发票 #12345
这样接口就让你写出通用、可扩展又优雅的代码啦。
3. 接口的实现
类实现接口
类用冒号实现接口(对,和继承一样):
public class Ticket : IPrintable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine($"打印票据: {Name}");
}
}
注意:类必须实现接口的所有成员。而且实现的成员必须是public。
如果漏实现一个成员:
public class BrokenTicket : IPrintable
{
// 漏了Print()的实现
public string Name { get; set; }
}
// 编译错误: 'BrokenTicket' 没有实现接口成员 'IPrintable.Print()'
多个接口
类可以用逗号实现多个接口:
public interface IStorable
{
void Store();
}
public class MultiPurposeDoc : IPrintable, IStorable
{
public string Name { get; set; }
public void Print()
{
Console.WriteLine("打印文档");
}
public void Store()
{
Console.WriteLine("保存文档");
}
}
4. 接口到底有啥用?
你可能现在心里想:“好吧,语法我懂了。可这玩意儿现实里到底有啥用,除了让我这课更难?”别急!接口其实是专业开发里最常用的工具之一。
职责分离和低耦合(Decoupling / Loose Coupling):
- 比如你要写个音乐播放器。它不关心音乐从哪来——本地文件、网络、CD都行。它只关心音乐源能不能给它音频流。
- 你可以定义个IAudioSource接口,里面有GetAudioStream()方法。
- 然后你有FileAudioSource、InternetAudioSource、CDAudioSource这些类,都实现这个接口。
- 播放器只和IAudioSource打交道,不知道具体类型。明天你要加个BluetoothAudioSource,播放器代码都不用改!只要写个新类实现IAudioSource就行。这样系统就超级灵活、好扩展。这就是低耦合——组件只依赖抽象(接口),不依赖具体实现。
多态和统一处理:
就像前面PrintAnything的例子,你可以有一堆不同类型的对象,但它们有共同的行为(接口描述的)。你可以对所有这些对象调用同一个方法(Print()),不用管它到底是报告、发票还是票据。这样代码又简洁又通用。
单元测试(Unit Testing):
这大概是接口最重要的用途之一。你测某个组件时,它经常要依赖别的组件(比如保存数据的类要用数据库类)。
你不用真的传DatabaseSaver(那得真有数据库才能测!),你可以传个“假货”(或者叫“mock”),只要它实现IDataSaver接口就行。这个“mock”对象只模拟保存行为,不会真去数据库。这样你就能隔离、快速、无依赖地测你的组件。
开发API和框架:
你写库或框架时,肯定想给开发者留“扩展点”。接口就特别适合。你可以说:“你要让你的组件和我系统配合,就实现这个接口。” .NET标准库里接口一大堆(比如IEnumerable<T>、IDisposable、IComparable<T>)——它们为常见场景定了合同。
直接面向接口编程(Programming to an Interface):
老司机常说:“面向接口编程,不要面向实现。”意思是你定义变量类型或方法参数时,别用具体类(Car),用接口(IDriveable)。这样代码更灵活,不依赖细节,实现换起来也方便。
5. 用接口时常见的坑
坑1:想new接口的实例。
你可以写Cat murzik = new Cat("Мурзик", 3);,因为Cat是具体类。但你不能写ITalkable talker = new ITalkable();。接口只是合同、模板,没有实现,不能直接创建。这就像图纸,不是房子。
坑2:忘了实现接口的所有成员。
你说你类实现了接口,比如IMyInterface,那你必须实现它所有方法。漏一个都会编译报错:MyClass没实现IMyInterface.TheMissingMethod()。
坑3:实现时访问修饰符写错。
接口方法默认public,实现时也必须public。你要是写成private或protected,编译器直接报错。承诺了就得公开实现。
坑4:想在接口里加字段或构造函数。
接口描述的是行为,不是状态。所以不能加字段或构造函数。你要加就编译报错。只能有属性,而且只是getter/setter的描述。
坑5:把override和接口实现搞混。
override是重写基类方法用的,实现接口不用它——你只要写public方法,签名对就行。这点很容易搞混。
GO TO FULL VERSION