概要
迭代器是LINQ中的很多方法的实现基础,像Where,Select或Distinct等常用的LINQ方法底层都是通过迭代器最终实现的。
本文从源码角度解释一下迭代器的工作原理,包括像延迟加载,多线程支持,嵌套循环这些问题,如何通过源码最终实现的。
本文以Distinct方法来作为例子,说明迭代器的工作原理。将源码中的iterator.cs, Distinct.cs抽取出来,加上一定的日志,以方便我们了解代码的执行流程。
本文会着重分析所有迭代器共性的代码,不会展开对Distinct的源码的分析,如果想了解Distinct方法源码,请参考我的另一篇博文C# Linq 源码分析之Distinct方法
关键源码和日志代码分析
首先解释一下,为什么能以Distinct方法为例,说明其他迭代器的工作原理。
在LINQ 源码中,Where,Select等方法也使用迭代器,并且定义了自己的迭代器,这些迭代器只有功能层面的区别,例如Distinct的迭代器可以过滤集合中的重复元素,Where方法中的迭代器可以按照条件进行过滤操作等。
对于基本功能,像延迟加载,多线程支持,嵌套循环支持的处理方式都是一致的。
从代码层面说,Where方法,Select方法或其他方法中的迭代器,都是继承自基类迭代器Iterator,所有共性的内容在Iterator类中都已经封装好。
Iterator代码
和本主题无关的代码以及略去。
namespace Iterator.MyLinq
{
using System;
using System.Collections;
using System.Collections.Generic;
public static partial class MyEnumerable
{
internal abstract class Iterator<TSource> : IEnumerable<TSource>, IEnumerator<TSource>
{
public string _identity;
private readonly int _threadId;
internal int _state;
internal TSource _current = default!;
/// <summary>
/// Initializes a new instance of the <see cref="Iterator{TSource}"/> class.
/// </summary>
protected Iterator()
{
_identity = Guid.NewGuid().ToString();
_threadId = Environment.CurrentManagedThreadId;
System.Console.WriteLine(_identity + " Iterator constructor is called and _threadId is " + _threadId);
}
public TSource Current => _current;
/// </remarks>
public abstract Iterator<TSource> Clone();
public virtual void Dispose()
{
_current = default!;
_state = -1;
}
public IEnumerator<TSource> GetEnumerator()
{
System.Console.WriteLine(_identity + " Iterator GetEnumerator _state is " + _state);
System.Console.WriteLine(_identity + " Iterator GetEnumerator Environment.CurrentManagedThreadId is " + Environment.CurrentManagedThreadId);
Iterator<TSource> enumerator = _state == 0 && _threadId == Environment.CurrentManagedThreadId ? this : Clone();
enumerator._state = 1;
return enumerator;
}
public abstract bool MoveNext();
object? IEnumerator.Current => Current;
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
void IEnumerator.Reset() => throw new NotSupportedException();
}
}
}
- Iterator是一个抽象类,作为迭代器,按照C#的迭代器要求,需要实现IEnumerable, IEnumerator
- Iterator因为是抽象类,所以将构造函数访问级别设置为protected。该函数在实例化派生类时候被调用,用于获取当前线程Id, 增加日志打印代码,用于标识当前对象。
- public string _identity;是我新加 代码,用于在多线程环境中,标识不同的迭代器实例。
- 因为Iterator要实现IEnumerator接口,所以定义了MoveNext()方法,Reset方法和Current属性。
(1) 由于Iterator是抽象类,每种迭代器有自己指定的功能,所以可以将MoveNext方法交给派生类的迭代器去实现
(2) 例如其派生类DistinctIterator类中通过使用HashSet过滤重复元素,WhereArrayIterator迭代器可以按照指定条件过滤数组中的元素。 - 因为Iterator要实现IEnumerable接口,所以定义了GetEnumerator方法。该方法给出了具体实现,一般派生类中都是直接使用,不需要再覆写该方法。
- GetEnumerator中,首先打印迭代器状态_state和当前线程Id,
(1)如果当前迭代器状态不是0,表示有其他代码在使用当前迭代器,调用派生类的克隆方法返回一个新的迭代器对象。
(2)如果当前线程和在Iterator构造方法中记录的线程号不一致,表示有其他线程在使用当前迭代器,调用派生类的克隆方法返回一个新的迭代器对象。
(3)否则返回当前迭代器对象。 - 将当前迭代器状态置为1,表示该迭代器不能再同时被其他代码使用。
Distinct方法代码
和主题无关的源码以及略去
public static IEnumerable<TSource> Distinct2<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
{
if (source == null)
{
ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
System.Console.WriteLine("Distinct2 is called");
return new DistinctIterator<TSource>(source, comparer);
}
private sealed partial class DistinctIterator<TSource> : Iterator<TSource>
{
private readonly IEnumerable<TSource> _source;
private readonly IEqualityComparer<TSource>? _comparer;
private HashSet<TSource>? _set;
private IEnumerator<TSource>? _enumerator;
public DistinctIterator(IEnumerable<TSource> source, IEqualityComparer<TSource>? comparer)
{
Debug.Assert(source != null);
_source = source;
_comparer = comparer;
System.Console.WriteLine("DistinctIterator constructor is called.");
}
public override Iterator<TSource> Clone() {
System.Console.WriteLine(base._identity + " DistinctIterator clone is called");
return new DistinctIterator<TSource>(_source, _comparer);
}
public override bool MoveNext()
{
System.Console.WriteLine(base._identity + " DistinctIterator MoveNext is called and _state is " + _state);
switch (_state)
{
case 1:
_enumerator = _source.GetEnumerator();
if (!_enumerator.MoveNext())
{
Dispose();
return false;
}
TSource element = _enumerator.Current;
System.Console.WriteLine(base._identity + " DistinctIterator move next is called current is " + element.ToString());
_set = new HashSet<TSource>(7, _comparer);
_set.Add(element);
_current = element;
_state = 2;
return true;
case 2:
Debug.Assert(_enumerator != null);
Debug.Assert(_set != null);
while (_enumerator.MoveNext())
{
element = _enumerator.Current;
if (_set.Add(element))
{
System.Console.WriteLine(base._identity + " DistinctIterator move next is called current is " + element.ToString());
_current = element;
return true;
}
}
break;
}
Dispose();
return false;
}
public override void Dispose()
{
if (_enumerator != null)
{
_enumerator.Dispose();
_enumerator = null;
_set = null;
}
base.Dispose();
}
}
}
}
- 为了避免和原有的LINQ方法库的Distinct方法命名冲突,将其修改为Distinct2,该方法返回一个DistinctIterator对象。
- DistinctIterator类继承自Iterator类,覆写了Clone方法和MoveNext方法。Clone方法返回DistinctIterator对象,MoveNext方法进行过滤迭代。
- 在MoveNext方法中加入打印迭代器状态和迭代元素。_identity继承自Iterator基类,是一个GUID用于在多线程下表示DistinctIterator对象。
测试验证
延迟加载究竟做了什么
测试代码如下,Student类和比较器代码详见附录。 我们暂时注释掉foreach。
static void Main(string[] args)
{
Student s = new Student("x001", "Tom", "CN-1");
List<Student> studentList = new List<Student>{
new Student("x001", "Tom", "CN-1"),
new Student("x002", "Jack", "CN-1"),
new Student("x003", "Mary", "CN-1"),
new Student("x003", "MAAA", "CN-1"),
};
var stuList = studentList.Distinct2(new StudentEqualityComparer());
/*foreach(var stu in stuList){
Console.WriteLine(stu.Name);
} */
}
从执行结果上看:
- Distinct2方法被调用。
- DistinctIterator迭代器被创建,它的基类Iterator构造函数和DistinctIterator构造函数被依次调用。
Iterator基类的GetEnumerator并没有被调用,DistinctIterator对象的MoveNext方法也没有被调用。只是返回了一个DistinctIterator对象。
呈上,所谓延迟加载其实就是现将迭代器对象返回,并不真正开始迭代,也不会返回具体的迭代结果。
迭代器究竟是什么也就不难理解,其实就是一个迭代的方式和路线的封装,可以就把它理解为一个路线图。例如对于一个有3个元素的数组,迭代方式是基于数组下标,迭代路线是0->1->2,只有通过foreach或者ToList方法,才能真正按照路线图去迭代。
这样的设计也就给迭代优化留下的实现的空间,如果我们有xxx.Where().Where()这样的代码,因为每个Where都是返回一个迭代器,那第二个Where是否可以合并第一个Where的条件,生成一个新的迭代器,以优化迭代过程。
答案是肯定的。具体实现,请参看我的博文 C# Linq源码分析之Where
foreach究竟做了什么
开启foreach循环,我们看看会发生什么
代码如下:
static void Main(string[] args)
{
Student s = new Student("x001", "Tom", "CN-1");
List<Student> studentList = new List<Student>{
new Student("x001", "Tom", "CN-1"),
new Student("x002", "Jack", "CN-1"),
new Student("x003", "Mary", "CN-1"),
new Student("x003", "MAAA", "CN-1"),
};
var stuList = studentList.Distinct2(new StudentEqualityComparer());
foreach(var stu in stuList){
Console.WriteLine(stu.Name);
}
}
执行结果如下:
单线程下对象的Identity可以忽略。
- 依次调用基类和派生类的构造函数,创建DistinctIterator实例。
- 基类构造函数调用时,记录当前线程号是1。
- 迭代器标记为初始是0,表示可以被使用。
- foreach调用Iterator的GetEnumerator方法:
(1) 打印出当前_state是0
(2) 当前线程是1
(3) 条件_state == 0 && _threadId == Environment.CurrentManagedThreadId被满足,所以返回当前DistinctIterator实例
(4)将迭代器标志位置为1,表示该迭代器已经加锁,不能再被其他线程或程序使用。
5. foreach调用DistinctIterator实例的MoveNext方法,正式开始迭代。Id是x003的有两个,比较器中已经规定,两个元素Id相同,即为重复元素,所以第4个元素被过滤掉。
嵌套foreach
测试代码如下:
static void Main(string[] args)
{
Student s = new Student("x001", "Tom", "CN-1");
List<Student> studentList = new List<Student>{
new Student("x001", "Tom", "CN-1"),
new Student("x002", "Jack", "CN-1"),
new Student("x003", "Mary", "CN-1"),
new Student("x003", "MAAA", "CN-1"),
};
var stuList = studentList.Distinct2(new StudentEqualityComparer());
foreach(var stu in stuList){
Console.WriteLine(stu.Name);
foreach(var b in stuList){
Console.WriteLine(b.Name);
}
}
}
上述代码中外层foreach开始迭代,内层foreach也会试图使用该迭代器,迭代操作只能前进,不能后退,外层foreach已经读取了一个元素,这样内层foreach就读不到完整数据。
实际上代码是可以正常执行的,执行结果如下:
从执行结果我们可以看出:
- 外层foreach调用GetEnumerator方法后,_state是0,线程Id是1,直接将当前Distinct2产生成的DistinctIterator对象返回给foreach,并将该迭代器加锁,即标记位设置为1。
- 在外层foreach通过调用DistinctIterator的MoveNext方法,返回第一个元素后,内层foreach也试图使用当前DistinctIterator对象。
- 由于当前DistinctIterator对象已经加锁,所以在内层foreach调用GetEnumerator时候,由于_state不再是0,因此DistinctIterator实例的Clone方法被调用,新的DistinctIterator对象产生,它用于内层foreach。
(1) 外层foreach使用的迭代器对象标识是0f6b80fe-1caf-49fd-8f02-44b017b4b370
(2) 内层foreach使用的迭代器对象标识是c67f88a0-2c53-4014-9201-d9a7289ddee3 - 这样两个DistinctIterator分别服务于两个foreach,互不干扰。
多线程下的foreach
测试代码如下:
public static void printArray(IEnumerable<Student> arr){
var a = "";
foreach(var stu in arr){
a = stu.Id;
}
}
static void Main(string[] args)
{
Student s = new Student("x001", "Tom", "CN-1");
List<Student> studentList = new List<Student>{
new Student("x001", "Tom", "CN-1"),
new Student("x002", "Jack", "CN-1"),
new Student("x003", "Mary", "CN-1"),
new Student("x003", "MAAA", "CN-1"),
};
var stuList = studentList.Distinct2(new StudentEqualityComparer());
Parallel.Invoke(
()=>printArray(stuList),
()=>printArray(stuList),
()=>printArray(stuList)
);
}
为了避免输出内容过多,所以printArray内改为赋值操作。
执行结果如下:
- 第一个printArray函数由线程号是1 的线程执行,foreach调用GetEnumerator方法后,_state是0,线程Id是1,直接将当前Distinct2产生成的DistinctIterator对象返回给foreach,并将该迭代器加锁,即标记位设置为1。
- 4号线程的printArray函数开始执行,它也试图使用线程号是1的线程中的DistinctIterator对象。
- 由于1号线程的DistinctIterator对象已经加锁,所以在foreach调用GetEnumerator时候,由于_state不再是0,并且当前线程是Id是4,不等于1,因此DistinctIterator实例的Clone方法被调用,新的DistinctIterator对象产生,它用于4号线程。
- 5号线程的printArray函数开始执行,它也试图使用线程号是1的线程中的DistinctIterator对象。
- 由于1号线程的DistinctIterator对象已经加锁,所以foreach调用GetEnumerator时候,由于_state不再是0,并且当前线程是Id是5,不等于1,因此DistinctIterator实例的Clone方法被调用,新的DistinctIterator对象产生,它用于5号线程
(1) 1号线程使用的迭代器对象标识是15ee5e5d-9b5e-4abf-8cbb-50fa9428c4ae
(2) 4号线程使用的迭代器对象标识是a19e9d2b-1768-4634-8c86-590b8b163769
(3) 5号线程使用的迭代器对象标识是35d4dc00-ce03-4422-acd3-36398322639a - 这样三个DistinctIterator对象分别服务于3个foreach,互不干扰。
附录
public class Student {
public string Id { get; set; }
public string Name { get; set; }
public string Classroom { get; set; }
public Student(string id, string name, string classroom)
{
this.Id = id;
this.Name = name;
this.Classroom = classroom;
}
}
public class StudentEqualityComparer : IEqualityComparer<Student>
{
public bool Equals(Student b1, Student b2) {
System.Console.WriteLine( b1.Id + " " + b2.Id);
return b1.Id.Equals(b2.Id);
}
public int GetHashCode(Student bx) => bx.Id.GetHashCode();
}