1. 关键接口分类
如果你觉得接口只是架构师和“洁癖代码”爱玩的东西,实际开发可以不用,那我要让你大吃一惊:任何严肃的C#项目都离不开接口。为啥?因为.NET标准库几乎每个部分都是靠接口搭起来的!没有它们你连集合都用不了,文件都读不了,LINQ里都没法过滤集合。
接口是.NET里多态和可扩展性的基石,正是它们决定了整个框架的“积木”怎么拼起来。
.NET标准库里接口多到爆。为了不被淹没,我给你分个类(当然不全——全讲完得活几辈子!):
| 类别 | 接口 | 干啥用的? |
|---|---|---|
| 集合 | , , , , |
遍历、修改、按索引访问、键值对操作 |
| 资源操作 | |
释放资源(文件、连接、流) |
| 比较 | , , |
排序、比较、对象唯一性 |
| 序列化 | |
对象转字节流(和反过来) |
| LINQ和查询 | , |
支持复杂查询(比如查数据库) |
| 异步 | , |
异步遍历和资源清理 |
| 事件和通知 | , |
属性/集合变化时响应 |
| 日期和时间 | |
自定义字符串格式化 |
| 数据结构 | , |
集合和元组的深度比较 |
| 流/输入输出 | , , , |
流操作,push/pull通知 |
注意! 上面很多接口都有类型参数:List<string>。一般用在集合和集合接口里Int[] → List<int>。后面讲集合时会细说。
2. 集合接口
IEnumerable 和 IEnumerator —— 你的foreach通行证
.NET里几乎每个集合都实现了IEnumerable或者它的泛型版IEnumerable<T>。正是这个接口让你能用神奇的foreach语法:
List<int> numbers = new List<int> { 1, 2, 3 };
foreach (int n in numbers) // 能用是因为List<int>实现了IEnumerable<int>
{
Console.WriteLine(n);
}
接口本身长这样,极简版:
public interface IEnumerable
{
IEnumerator GetEnumerator();
}
IEnumerator就是那个“遍历器”接口,专门负责在你的集合里溜达:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
实际开发里你超级常用IEnumerable<T>做参数和返回值。比如返回数组里所有偶数的方法:
public IEnumerable<int> GetEvenNumbers(int[] array)
{
foreach (var x in array)
if (x % 2 == 0) yield return x;
}
对,yield关键字就是魔法——它帮你自动实现接口,后面会细讲。
ICollection 和 IList —— 支持索引和修改的集合
如果你不光要遍历,还想加/删元素,甚至按索引拿元素,就得用更专业的接口:
- ICollection<T> —— 多了Add、Remove方法,还有Count属性。
- IList<T> —— 支持更多操作,包括按索引访问(this[int index])。
public void PrintFirstItem(IList<string> list)
{
if (list.Count > 0)
Console.WriteLine(list[0]);
}
List<T>同时实现了IEnumerable<T>、ICollection<T>和IList<T>。超方便——你既能当普通可遍历列表用,也能用它所有高级方法。
IDictionary<TKey, TValue> —— “键-值”对
喜欢用字典(其实大家都离不开!)就用IDictionary<TKey, TValue>接口。它保证你能通过key拿到value,反过来也行。
public void PrintAllPairs(IDictionary<string, int> ages)
{
foreach (var pair in ages)
Console.WriteLine($"{pair.Key}: {pair.Value}");
}
这里ages可以是Dictionary<string, int>,也可以是SortedDictionary<string, int>啥的——只要实现了这个接口就行!
3. IDisposable接口:正确释放资源
所有输入输出、文件操作、网络连接、数据库在.NET里都离不开IDisposable接口。它规定了一个超重要的约定:对象有非托管资源就必须“清理”用完的东西。对,就是它让using语法成为可能:
using (StreamReader reader = new StreamReader("file.txt"))
{
// 用文件,出了using自动关掉!
string line = reader.ReadLine();
}
接口本身其实超简单:
public interface IDisposable
{
void Dispose();
}
但所有“正经”库都实现它!想看最佳实践,去官方文档。
4. 比较和唯一性接口
IComparable<T> 和 IComparer<T>
想让你的对象集合能排序,对象就得能互相比较。为此有IComparable<T>接口:
public class Student : IComparable<Student>
{
public string Name { get; set; }
public int Score { get; set; }
public int CompareTo(Student? other)
{
// 按分数降序排
if (other == null) return 1;
return other.Score.CompareTo(this.Score);
}
}
// 现在可以直接排序学生了:
var students = new List<Student> { ... };
students.Sort();
IComparer<T>让你在类外自定义比较——比如有时按名字排,有时按分数排:
public class NameComparer : IComparer<Student>
{
public int Compare(Student? x, Student? y)
{
return string.Compare(x?.Name, y?.Name);
}
}
IEquatable<T> —— 判断相等
想让你的对象能进HashSet<T>(也就是在集合里“唯一”)?实现IEquatable<T>:
public class Person : IEquatable<Person>
{
public string Name { get; set; }
public bool Equals(Person? other)
{
if (other == null) return false;
return this.Name == other.Name;
}
}
5. 事件和通知接口
你有没有想过让程序“知道”对象属性变了?比如用户改了名字,界面自动刷新?这就是INotifyPropertyChanged接口的用武之地。它在图形界面程序(比如WPF、Xamarin)里超常用。
接口签名很简单:
public interface INotifyPropertyChanged
{
event PropertyChangedEventHandler? PropertyChanged;
}
实现例子:
public class User : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string name;
public string Name
{
get => name;
set
{
if (name != value)
{
name = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name)));
}
}
}
}
现在界面上所有绑定到这个对象的地方,Name一变就自动刷新。想深入了解——去官方文档。
6. 其他有用接口
IFormattable接口:灵活格式化
想让你的对象能漂亮地输出成字符串(比如小数点后几位),就实现IFormattable接口:
public class Temperature : IFormattable
{
public double Celsius { get; }
public Temperature(double celsius) => Celsius = celsius;
public string ToString(string? format, IFormatProvider? formatProvider)
{
// 不复杂,按格式显示'C'或'F'
if (format == "F")
return $"{Celsius * 9 / 5 + 32} F";
return $"{Celsius} C";
}
}
现在你可以temp.ToString("F", null)或temp.ToString("C", null)。这就是为啥DateTime、double、decimal等内置类型都能灵活格式化。
异步相关接口
C#有了异步后,异步世界也需要新接口。比如你的对象要异步释放资源——实现IAsyncDisposable:
public interface IAsyncDisposable
{
ValueTask DisposeAsync();
}
现在你可以用await using代替using!
异步枚举(await foreach)用IAsyncEnumerable<T>,它让你不阻塞主线程地遍历元素。比如分块读大文件、处理网络数据流啥的。详细见第58关 :P
序列化接口:ISerializable
要把对象变成字节流保存/传输(比如发到网络),.NET有ISerializable接口。现在用得不多了,因为有更方便的机制(比如属性和现成的序列化器),但还是值得知道。接口签名如下:
public interface ISerializable
{
void GetObjectData(SerializationInfo info, StreamingContext context);
}
7. 实战:接口在真实应用里的用法
假设你在写一个图书馆图书管理的控制台程序(别笑,总得有人写吧!)。你想能输出图书列表、排序、筛选、加载和保存数据。会用.NET接口你就能:
- 通过IEnumerable<Book>返回各种集合类型,用户不用关心是数组还是列表。
- 支持多种排序:实现IComparable<Book>按书名排,单独写IComparer<Book>按作者排。
- 高效释放资源,比如文件操作用IDisposable。
- 用LINQ按类型或作者筛选图书,LINQ支持所有实现IEnumerable<T>的集合。
- 让程序易扩展——比如以后把文件存储换成数据库,只要你的类用接口(IBookStorage)而不是具体实现就行。
8. 常见错误和注意点
新手最常犯的错之一——用具体实现(“List”)声明变量和方法参数,而不是接口(“IEnumerable”、“IList”)。记住:“面向接口编程”你的代码才灵活、好扩展。
有时候要注意选对接口层级——比如你写的方法只需要遍历功能,就用IEnumerable<T>,别用IList<T>,别给别人加不必要的限制。
还有个细节:如果类实现了很多接口,接口里有重名方法或属性,就得用显式接口实现(explicit implementation),不然编译器会懵逼。
GO TO FULL VERSION