C# LINQ源码分析之迭代器

27 篇文章 1 订阅
23 篇文章 1 订阅

概要

迭代器是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();
        }
    }
}
  1. Iterator是一个抽象类,作为迭代器,按照C#的迭代器要求,需要实现IEnumerable, IEnumerator
  2. Iterator因为是抽象类,所以将构造函数访问级别设置为protected。该函数在实例化派生类时候被调用,用于获取当前线程Id, 增加日志打印代码,用于标识当前对象。
  3. public string _identity;是我新加 代码,用于在多线程环境中,标识不同的迭代器实例。
  4. 因为Iterator要实现IEnumerator接口,所以定义了MoveNext()方法,Reset方法和Current属性。
    (1) 由于Iterator是抽象类,每种迭代器有自己指定的功能,所以可以将MoveNext方法交给派生类的迭代器去实现
    (2) 例如其派生类DistinctIterator类中通过使用HashSet过滤重复元素,WhereArrayIterator迭代器可以按照指定条件过滤数组中的元素。
  5. 因为Iterator要实现IEnumerable接口,所以定义了GetEnumerator方法。该方法给出了具体实现,一般派生类中都是直接使用,不需要再覆写该方法。
  6. GetEnumerator中,首先打印迭代器状态_state和当前线程Id,
    (1)如果当前迭代器状态不是0,表示有其他代码在使用当前迭代器,调用派生类的克隆方法返回一个新的迭代器对象。
    (2)如果当前线程和在Iterator构造方法中记录的线程号不一致,表示有其他线程在使用当前迭代器,调用派生类的克隆方法返回一个新的迭代器对象。
    (3)否则返回当前迭代器对象。
  7. 将当前迭代器状态置为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();
            }
        }
    }
}
  1. 为了避免和原有的LINQ方法库的Distinct方法命名冲突,将其修改为Distinct2,该方法返回一个DistinctIterator对象。
  2. DistinctIterator类继承自Iterator类,覆写了Clone方法和MoveNext方法。Clone方法返回DistinctIterator对象,MoveNext方法进行过滤迭代。
  3. 在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); 
           } */ 
           
       }

在这里插入图片描述
从执行结果上看:

  1. Distinct2方法被调用。
  2. 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可以忽略。

  1. 依次调用基类和派生类的构造函数,创建DistinctIterator实例。
  2. 基类构造函数调用时,记录当前线程号是1。
  3. 迭代器标记为初始是0,表示可以被使用。
  4. 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就读不到完整数据。

实际上代码是可以正常执行的,执行结果如下:
在这里插入图片描述
从执行结果我们可以看出:

  1. 外层foreach调用GetEnumerator方法后,_state是0,线程Id是1,直接将当前Distinct2产生成的DistinctIterator对象返回给foreach,并将该迭代器加锁,即标记位设置为1。
  2. 在外层foreach通过调用DistinctIterator的MoveNext方法,返回第一个元素后,内层foreach也试图使用当前DistinctIterator对象。
  3. 由于当前DistinctIterator对象已经加锁,所以在内层foreach调用GetEnumerator时候,由于_state不再是0,因此DistinctIterator实例的Clone方法被调用,新的DistinctIterator对象产生,它用于内层foreach。
    (1) 外层foreach使用的迭代器对象标识是0f6b80fe-1caf-49fd-8f02-44b017b4b370
    (2) 内层foreach使用的迭代器对象标识是c67f88a0-2c53-4014-9201-d9a7289ddee3
  4. 这样两个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内改为赋值操作。

执行结果如下:
在这里插入图片描述

  1. 第一个printArray函数由线程号是1 的线程执行,foreach调用GetEnumerator方法后,_state是0,线程Id是1,直接将当前Distinct2产生成的DistinctIterator对象返回给foreach,并将该迭代器加锁,即标记位设置为1。
  2. 4号线程的printArray函数开始执行,它也试图使用线程号是1的线程中的DistinctIterator对象。
  3. 由于1号线程的DistinctIterator对象已经加锁,所以在foreach调用GetEnumerator时候,由于_state不再是0,并且当前线程是Id是4,不等于1,因此DistinctIterator实例的Clone方法被调用,新的DistinctIterator对象产生,它用于4号线程。
  4. 5号线程的printArray函数开始执行,它也试图使用线程号是1的线程中的DistinctIterator对象。
  5. 由于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
  6. 这样三个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();
        
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值