C#-鸭子类型
什么是鸭子类型?
“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子”
如果一个对象具有某个方法或属性,那么它就可以被当作拥有这个方法或属性的类型来使用,而不需要严格地遵循一些规定与要求。
foreach 语句
C#标准库为我们提供了大量的集合类型,比如 List、Stack、Queue、ObservableCollection等等。
这些集合类型都实现了 IEnumerable 接口,所以我们可以使用 foreach 语法来遍历它们。
但实际上,foreach 语法并不要求类必须实现 IEnumerable 接口。只要类中有一个名为 GetEnumerator 的方法,
返回一个 IEnumerator 类型的对象,就可以使用 foreach 语法。
创建一个自定义类 MyEnumerableClass , 并通过实现 GetEnumerator(),让它可以像集合一样被 foreach 遍历,而遍历的数据来自内部数组 [1, 2, 3]。
void Main()
{
var c = new MyEnumerableClass();
foreach (var item in c)
{
item.Dump();
}
}
class MyEnumerableClass
{
// 定义一个私有数组
private int[] items = new int[] { 1, 4, 2, 3};
public IEnumerator GetEnumerator()
{
return items.GetEnumerator();
}
}为什么 foreach 能工作?
foreach 在编译时会被转换为类似下面的代码:
var enumerator = c.GetEnumerator();
while (enumerator.MoveNext())
{
var item = enumerator.Current;
Console.WriteLine(item);
}也就是说 foreach 依赖 枚举器(Enumerator)。
枚举器需要提供:
| 方法/属性 | 作用 |
|---|---|
| MoveNext() | 移动到下一个元素 |
| Current | 获取当前元素 |
| Reset() | 重置 |
这些都在 IEnumerator 接口内。
代码没有自己实现枚举器,而是 借用了数组的枚举器:
return items.GetEnumerator();数组 int[] 本身就支持枚举,所以直接返回它的枚举器。
相当于:
MyEnumerableClass
│
│ GetEnumerator()
▼
int[] 的 Enumerator
│
▼
foreach 遍历代码执行顺序:
new MyEnumerableClass()
│
▼
foreach 开始
│
▼
调用 c.GetEnumerator()
│
▼
返回 items 的 Enumerator
│
▼
MoveNext() → 1 → print
MoveNext() → 2 → print
MoveNext() → 3 → print
MoveNext() → false → 结束所以也可以写为以下形式:
foreach (var x in new DuckCollection())
{
x.Dump();
}
class DuckCollection
{
public DuckEnumerator GetEnumerator()
{
return new DuckEnumerator();
}
}
class DuckEnumerator
{
int i = 0;
public bool MoveNext()
{
return ++i <= 3;
}
public int Current => i;
} 还可以这样玩:
foreach (var i in 1..5)
{
i.Dump();
}
static class MyExtensions
{
public static IEnumerator<int> GetEnumerator(this Range range)
{
for (int i = range.Start.Value; i <= range.End.Value; i++)
{
yield return i;
}
}
}总结: foreach 是基于 pattern-based enumeration 的。只要类型提供 GetEnumerator(),并返回类型具有 bool MoveNext() 和 Current 成员,编译器就允许使用 foreach。
简单来说:foreach 使用的是 “枚举模式(enumeration pattern)”,而不是强制要求 IEnumerator 接口。
await 语句
await 语法也不要求类必须继承 Task 类或hi先一些 底部接口。只要类中有一个名为 GetAwaiter 的方法,返回一个 IAwaiter 类型的对象,就可以使用 await 语法。
并且与上面 foreach 的例子相同,也可以把 GetAwaiter 方法定义为一个扩展方法,从而“扩展”一些我们无法修改的类。
await TimeSpan.FromSeconds(1);
await 1.0;
static class Extensions
{
public static TaskAwaiter GetAwaiter(this TimeSpan ts) => Task.Delay(ts).GetAwaiter();
public static TaskAwaiter GetAwaiter(this double sec) => Task.Delay(TimeSpan.FromSeconds(sec)).GetAwaiter();
}但是在实际开发中最好不要使用,会严重污染常用的类型。
using 语句
using 语句只能用于实现了 IDisposable 接口的类。
对于一个 class 类型的对象, 如果它没有实现 IDisposable 接口,那么即使它拥有 public void Dispose() 方法,它仍然时无法使用 using 语句的(编译器会提示,这个对象必须可以隐式转换为 IDisposable 对象)
但是在 C# 中有一个类型:ref struct 类型。这种类型的对象在离开作用域时会自动被销毁,所以它们不需要实现 IDisposable 接口,所以为这种类型的对象添加一个 Dispose 方法,这样就可以使用 using 语句来释放资源了。
using var s = new MyDisposableStructType();
ref struct MyDisposableStructType()
{
public void Dispose()
{
Console.WriteLine("MyDisposableStructType Disposed.");
}
}集合初始化器
C# 中的很多集合类型都支持集合初始化器语法 { } 来初始化集合对象。比如 List 类型可以这样初始化:
var list = new List<int> { 1, 2, 3 };只要类实现了 IEnumerable 接口,并且包含一个名为 Add 的方法,那么这个类就可以使用集合初始化器语法。
var collection = new PlanetCollection<string>(8)
{
"mercury", "venus", "earth", "mars", "jupiter", "saturn", "uranus", "neptune"
};
foreach (var element in collection)
{
element.Dump();
}
class PlanetCollection<T> : IEnumerable
{
public T[] Planets { get; init; }
private int _index = 0;
public PlanetCollection(int count)
{
Planets = new T[count];
}
public void Add(T item)
{
if (_index >= Planets.Length)
{
throw new IndexOutOfRangeException();
}
Planets[_index++] = item;
}
public IEnumerator GetEnumerator()
{
return Planets.GetEnumerator();
}
}元组拆分
C# 在引入了元组后,也引入了元组拆分语法。比如可以这样写:
var (a, b) = (1, 2);
(int c, int d) = (3, 4);很多原生的类型也支持元组拆分。比如:
var pair = new KeyValuePair<string, int>("key", 42);
var (key, value) = pair;
var dt = DateTime.Now;
var (year, month, day) = dt;此外,如果我们声明一个 record 类型,那么底层也会为我们提供元组拆分的功能。
实际上,元组拆分的语法是通过 Deconstruct 方法实现的。只要类中有一个名为 Deconstruct 的方法,并且用 out 的方式进行传参,那么这个类就可以使用元组拆分语法。
var point = new Point2d { X = 1, Y = 2 };
var (x, y) = point;
(x,y).Dump();
class Point2d
{
public int X { get; set; }
public int Y { get; set; }
public void Deconstruct(out int x, out int y)
{
x = X;
y = Y;
}
}Linq 的 SelectMany 方法
LINQ 中的 SelectMany 方法,以及多层 from 语句。比如我们现在有一个“数组的数组”一样的结构,例如:
class Person
{
public string Name { get; set; }
public List<Pet> Pets { get; set; }
}
class Pet
{
public string Name { get; set; }
}使用 SelectMany 来进行展开:
var people = GetPeople();
// 使用查询表达式
var pets = (from person in people
from pet in person.Pets
select pet).ToList();
// 或者链式表达式
var pets = people.SelectMany(p => p.Pets).ToList();完整版:
var people = GetPeople();
// 使用查询表达式
var petsQuery = (from person in people
from pet in person.Pets
select pet).ToList();
petsQuery.Dump();
foreach (var pet in petsQuery)
{
pet.Name.Dump();
}
var petsMethod = people.SelectMany(p => p.Pets).ToList();
petsMethod.Dump();
static List<Person> GetPeople()
{
return new List<Person>
{
new Person
{
Name = "Alice",
Pets = new List<Pet>
{
new Pet { Name = "Fluffy" },
new Pet { Name = "Spot" },
}
},
new Person
{
Name = "Bob",
Pets = new List<Pet>
{
new Pet { Name = "Fido" }
}
},
new Person
{
Name = "Charlie",
Pets = new List<Pet>()
}
};
}
class Person
{
public string Name { get; set; }
public List<Pet> Pets { get; set; }
}
class Pet
{
public string Name { get; set; }
}实际上,只要为类提供正确的 SelectMany 方法,那么就可以使用多层 from 语句来进行展开:
var collection = new PlanetCollection<string>(3);
collection.Add("Earth");
collection.Add("Mars");
collection.Add("Venus");
// LINQ 查询
var query =
from planet in collection
from letter in planet
select letter;
// 把字符序列转换为大写字符串并 Dump
new string(query.ToArray()).ToUpper().Dump();
// 定义 PlanetCollection
class PlanetCollection<T> : IEnumerable<T>
{
public T[] Planets { get; init; }
private int _index = 0;
public PlanetCollection(int count)
{
Planets = new T[count];
}
public void Add(T item)
{
if (_index >= Planets.Length)
throw new IndexOutOfRangeException();
Planets[_index++] = item;
}
public IEnumerator<T> GetEnumerator() => ((IEnumerable<T>)Planets).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
// 扩展方法
static class Extensions
{
public static IEnumerable<TResult> SelectMany<TSource, TCollection, TResult>(
this PlanetCollection<TSource> source,
Func<TSource, IEnumerable<TCollection>> collectionSelector,
Func<TSource, TCollection, TResult> resultSelector
)
{
foreach (var item in source)
{
foreach (var subItem in collectionSelector(item))
{
yield return resultSelector(item, subItem);
}
}
}
}