目录

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);
			}
		}
	}
}