zoukankan      html  css  js  c++  java
  • .NET面试题系列[9]

    .NET面试题系列目录

    什么是IEnumerable?

    IEnumerable及IEnumerable的泛型版本IEnumerable<T>是一个接口,它只含有一个方法GetEnumerator。Enumerable这个静态类型含有很多扩展方法,其扩展的目标是IEnumerable<T>。

    实现了这个接口的类可以使用Foreach关键字进行迭代(迭代的意思是对于一个集合,可以逐一取出元素并遍历之)。实现这个接口必须实现方法GetEnumerator。

    如何实现一个继承IEnumerable的类型?

    实现一个继承IEnumerable的类型等同于实现方法GetEnumerator。想知道如何实现方法GetEnumerator,不妨思考下实现了GetEnumerator之后的类型在Foreach之下的行为:

    • 可以获得第一个或当前成员
    • 可以移动到下一个成员
    • 可以在集合没有下一个成员时退出循环。

    假设我们有一个很简单的Person类(例子来自MSDN):

        public class Person
        {
            public Person(string fName, string lName)
            {
                FirstName = fName;
                LastName = lName;
            }
    
            public string FirstName;
            public string LastName;
        }

    然后我们想构造一个没有实现IEnumerable的类型,其储存多个Person,然后再对这个类型实现IEnumerable。这个类型实际上的作用就相当于Person[]或List<Person>,但我们不能使用它们,因为它们已经实现了IEnumerable,故我们构造一个People类,模拟很多人(People是Person的复数形式)。这个类型允许我们传入一组Person的数组。所以它应当有一个Person[]类型的成员,和一个构造函数,其可以接受一个Person[],然后将Person[]类型的成员填充进去作为初始化。

        //People类就是Person类的集合
        //但我们不能用List<Person>或者Person[],因为他们都实现了IEnumerable
        //我们要自己实现一个IEnumerable
        //所以请将People类想象成List<Person>或者类似物
        public class People : IEnumerable
        {
            private readonly Person[] _people;
            public People(Person[] pArray)
            {
                //构造一个Person的集合
                _people = new Person[pArray.Length];
    
                for (var i = 0; i < pArray.Length; i++)
                {
                    _people[i] = pArray[i];
                }
            }
    
            //实现IEnumerable需要实现GetEnumerator方法
            public IEnumerator GetEnumerator()
            {
                throw new NotImplementedException();
            }
        }

    我们的主函数应当是:

            public static void Main(string[] args)
            {
                //新的Person数组
                Person[] peopleArray = 
                {
                    new Person("John", "Smith"),
                    new Person("Jim", "Johnson"),
                    new Person("Sue", "Rabon"),
                };
    
                //People类实现了IEnumerable
                var peopleList = new People(peopleArray);
    
                //枚举时先访问MoveNext方法
                //如果返回真,则获得当前对象,返回假,就退出此次枚举
                foreach (Person p in peopleList)
                    Console.WriteLine(p.FirstName + " " + p.LastName);
            }

    但现在我们的程序不能运行,因为我们还没实现GetEnumerator方法。

    实现方法GetEnumerator

    GetEnumerator方法需要一个IEnumerator类型的返回值,这个类型是一个接口,所以我们不能这样写:

    return new IEnumerator();

    因为我们不能实例化一个接口。我们必须再写一个类PeopleEnumerator,它继承这个接口,实现这个接口所有的成员:Current属性,两个方法MoveNext和Reset。于是我们的代码又变成了这样:

            //实现IEnumerable需要实现GetEnumerator方法
            public IEnumerator GetEnumerator()
            {
                return new PeopleEnumerator();
            }

    在类型中:

        public class PeopleEnumerator : IEnumerator
        {
            public bool MoveNext()
            {
                throw new NotImplementedException();
            }
    
            public void Reset()
            {
                throw new NotImplementedException();
            }
    
            public object Current { get; }
        }

    现在问题转移为实现两个方法,它们的功能看上去一目了然:一个负责将集合中Current向后移动一位,一个则将Current初始化为0。我们可以查看IEnumerator元数据,其解释十分清楚:

    • Enumerator代表一个类似箭头的东西,它指向这个集合当前迭代指向的成员
    • IEnumerator接口类型对非泛型集合实现迭代
    • Current表示集合当前的元素,我们需要用它仅有的get方法取得当前元素
    • MoveNext方法根据Enumerator是否可以继续向后移动返回真或假
    • Reset方法将Enumerator移到集合的开头

    通过上面的文字,我们可以理解GetEnumerator方法,就是获得当前Enumerator指向的成员。我们引入一个整型变量position来记录当前的位置,并且先试着写下:

        public class PeopleEnumerator : IEnumerator
        {
            public Person[] _peoples;
            public object Current { get; }
    
            //当前位置
            public int position;
    
            //构造函数接受外部一个集合并初始化自己内部的属性_peoples
            public PeopleEnumerator(Person[] peoples)
            {
                _peoples = peoples;           
            }
            
            //如果没到集合的尾部就移动position,返回一个bool
            public bool MoveNext()
            {
                if (position < _peoples.Length)
                {
                    position++;
                    return true;
                }
                return false;
            }
    
            public void Reset()
            {
                position = 0;
            }
        }

    这看上去好像没问题,但一执行之后却发现:

    • 当执行到MoveNext方法时,position会先增加1,这导致第一个元素(在位置0)会被遗漏,故position的初始值应当为-1而不是0
    • 当前位置变量position显然应该是私有的
    • 需要编写Current属性的get方法取出当前位置(position)上的集合成员

    通过不断的调试,最后完整的实现应当是:

    public class PeopleEnumerator : IEnumerator
    {
            public Person[] People;
    
            //每次运行到MoveNext或Reset时,利用get方法自动更新当前位置指向的对象
            object IEnumerator.Current
            {
                get
                {
                    try
                    {
                        //当前位置的对象
                        return People[_position];
                    }
                    catch (IndexOutOfRangeException)
                    {
                        throw new InvalidOperationException();
                    }
                }
            }
    
            //当前位置
            private int _position = -1;
    
            public PeopleEnumerator(Person[] people)
            {
                People = people;           
            }
    
            //当程序运行到foreach循环中的in时,就调用这个方法获得下一个person对象
            public bool MoveNext()
            {
                _position++;
                //返回一个布尔值,如果为真,则说明枚举没有结束。
                //如果为假,说明已经到集合的结尾,就结束此次枚举
                return (_position < People.Length);
            }
    
            public void Reset() => _position = -1;
        }

    为什么当程序运行到in时,会呼叫方法MoveNext呢?我们并没有直接调用这个方法啊?当你试图查询IL时,就会得到答案。实际上下面两段代码的作用是相同的:

    foreach (T item in collection)
    {
      ...
    }
    IEnumerator<T> enumerator = collection.GetEnumerator();
    while (enumerator.MoveNext())
    {
      T item = enumerator.Current;
      ...
    }

    注意:第二段代码中,没有呼叫Reset方法,也不需要呼叫。当你呼叫时,你会得到一个异常,这是因为编译器没有实现该方法。

    使用yield关键字实现方法GetEnumerator

    如果iterator本身有实现IEnumerator接口(本例就是一个数组),则可以有更容易的方法:

            public IEnumerator GetEnumerator()
            {
                return _people.GetEnumerator();
            }

    注意,这个方法没有Foreach的存在,所以如果你改用for循环去迭代这个集合,你得自己去呼叫MoveNext,然后获得集合的下一个成员。而且会出现一个问题,就是你无法知道集合的大小(IEnumerable没有Count方法,只有IEnumerable<T>才有)。此时,可以做个试验,如果我们知道一个集合有3个成员,故意迭代多几次,比如迭代10次,那么当集合已经到达尾部时,将会抛出InvalidOperationException异常。

        class Program
        {
            static void Main(string[] args)
            {
                Person p1 = new Person("1");
                Person p2 = new Person("2");
                Person p3 = new Person("3");
    
                People p = new People(new Person[3]{p1, p2, p3});
                var enumerator = p.GetEnumerator();
    
                //Will throw InvalidOperationException
                for (int i = 0; i < 5; i++)
                {
                    enumerator.MoveNext();
                    if (enumerator.Current != null)
                    {
                        var currentP = (Person) enumerator.Current;
                        Console.WriteLine("current is {0}", currentP.Name);
                    }
                }
    
                Console.ReadKey();
            }
        }
    
        public class Person
        {
            public string Name { get; set; }
    
            public Person(string name)
            {
                Name = name;
            }
        }
    
        public class People : IEnumerable
        {
            private readonly Person[] _persons;
    
            public People(Person[] persons)
            {
                _persons = persons;
            }
    
            public IEnumerator GetEnumerator()
            {
                return _persons.GetEnumerator();
            }
        }

    使用yield关键字配合return,编译器将会自动实现继承IEnumerator接口的类和上面的三个方法。而且,当for循环遍历超过集合大小时,不会抛出异常,Current会一直停留在集合的最后一个元素。

            public IEnumerator GetEnumerator()
            {
                foreach (Person p in _people)
                    yield return p;
            }

    如果我们在yield的上面加一句:

            public IEnumerator GetEnumerator()
            {
                foreach (var p in _persons)
                {
                    Console.WriteLine("test");
                    yield return p;
                }
            }

    我们会发现test只会打印三次。后面因为已经没有新的元素了,yield也就不执行了,整个Foreach循环将什么都不做。

    yield的延迟执行特性 – 本质上是一个状态机

    关键字yield只有当真正需要迭代并取到元素时才会执行。yield是一个语法糖,它的本质是为我们实现IEnumerator接口。

            static void Main(string[] args)
            {
                IEnumerable<string> items = GetItems();
                Console.WriteLine("Begin to iterate the collection.");
                var ret = items.ToList();
                Console.ReadKey();
            }
    
            static IEnumerable<string> GetItems()
            {
                Console.WriteLine("Begin to invoke GetItems()");
                yield return "1";
                yield return "2";
                yield return "3";
            }

    在上面的例子中,尽管我们呼叫了GetItems方法,先打印出来的句子却是主函数中的句子。这是因为只有在ToList时,才真正开始进行迭代,获得迭代的成员。我们可以使用ILSpy察看编译后的程序集的内容,并在View -> Option的Decompiler中,关闭所有的功能对勾(否则你将仍然只看到一些yield),然后检查Program类型,我们会发现编译器帮我们实现的MoveNext函数,实际上是一个switch。第一个yield之前的所有代码,统统被放在了第一个case中。

         bool IEnumerator.MoveNext()
         {
            bool result;
            switch (this.<>1__state)
            {
            case 0:
                this.<>1__state = -1;
                Console.WriteLine("Begin to invoke GetItems()");
                this.<>2__current = "1";
                this.<>1__state = 1;
                result = true;
                return result;
            case 1:
                this.<>1__state = -1;
                this.<>2__current = "2";
                this.<>1__state = 2;
                result = true;
                return result;
            case 2:
                this.<>1__state = -1;
                this.<>2__current = "3";
                this.<>1__state = 3;
                result = true;
                return result;
            case 3:
                this.<>1__state = -1;
                break;
            }
            result = false;
            return result;
        }

    如果某个yield之前有其他代码,它会自动包容到它最近的后续的yield的“统治范围”:

            static IEnumerable<string> GetItems()
            {
                Console.WriteLine("Begin to invoke GetItems()");
                Console.WriteLine("Begin to invoke GetItems()");
                yield return "1";
                Console.WriteLine("Begin to invoke GetItems()");
                yield return "2";
                Console.WriteLine("Begin to invoke GetItems()");
                Console.WriteLine("Begin to invoke GetItems()");
                Console.WriteLine("Begin to invoke GetItems()");
                yield return "3";
            }

    它的编译结果也是可以预测的:

            case 0:
                this.<>1__state = -1;
                Console.WriteLine("Begin to invoke GetItems()");
                Console.WriteLine("Begin to invoke GetItems()");
                this.<>2__current = "1";
                this.<>1__state = 1;
                result = true;
                return result;
            case 1:
                this.<>1__state = -1;
                Console.WriteLine("Begin to invoke GetItems()");
                this.<>2__current = "2";
                this.<>1__state = 2;
                result = true;
                return result;
            case 2:
                this.<>1__state = -1;
                Console.WriteLine("Begin to invoke GetItems()");
                Console.WriteLine("Begin to invoke GetItems()");
                Console.WriteLine("Begin to invoke GetItems()");
                this.<>2__current = "3";
                this.<>1__state = 3;
                result = true;
                return result;
            case 3:
                this.<>1__state = -1;
                break;

    这也就解释了为什么第一个打印出来的句子在主函数中,因为所有不是yield的代码统统都被yield吃掉了,并成为状态机的一部分。而在迭代开始之前,代码是无法运行到switch分支的。

    令人瞩目的是,编译器没有实现reset方法,这意味着不支持多次迭代:

        void IEnumerator.Reset()
        {
            throw new NotSupportedException();
        }

    这部分的文章还可以参考http://www.alloyteam.com/2016/02/generators-in-depth/

    yield只返回,不赋值

    下面这个例子来自http://www.cnblogs.com/artech/archive/2010/10/28/yield.html#!comments。不过我认为Artech大大分析的不是很好,我给出自己的解释。

    class Program
        {
            static void Main(string[] args)
            {
                IEnumerable<Vector> vectors = GetVectors();
    
                //Begin to call GetVectors
                foreach (var vector in vectors)
                {
                    vector.X = 4;
                    vector.Y = 4;
                }
    
                //Before this iterate, there are 3 members in vectors, all with X and Y = 4
                foreach (var vector in vectors)
                {
                    //But this iterate will change the value of X and Y BACK to 1/2/3
                    Console.WriteLine(vector);
                }
            }
    
            static IEnumerable<Vector> GetVectors()
            {
                yield return new Vector(1, 1);
                yield return new Vector(2, 3);
                yield return new Vector(3, 3);
            }
        }
        public class Vector
        {
            public double X { get; set; }
            public double Y { get; set; }
            public Vector(double x, double y)
            {
                this.X = x;
                this.Y = y;
            }
    
            public override string ToString()
            {
                return string.Format("X = {0}, Y = {1}", this.X, this.Y);
            }
        }

    我们进行调试,并将断点设置在第二次迭代之前,此时,我们发现vector的值确实变成4了,但第二次迭代之后,值又回去了,好像被改回来了一样。但实际上,并没有改任何值,yield只是老老实实的吐出了新的三个vector而已。Yield就像一个血汗工厂,不停的制造新值,不会修改任何值。

    从编译后的代码我们发现,只要我们通过foreach迭代一个IEnumerable,我们就会跑到GetVectors方法中,而每次运行GetVectors方法,yield都只会返回全新的三个值为(1,1),(2,2)和(3,3)的vector,仿佛第一次迭代完全没有运行过一样。原文中,也有实验证明了vector创建了六次,实际上每次迭代都会创建三个新的vector

    解决这个问题的方法是将IEnumerable转为其子类型例如List或数组。

    在迭代的过程中改变集合的状态

    foreach迭代时不能直接更改集合成员的值,但如果集合成员是类或者结构,则可以更改其属性或字段的值。不能在为集合删除或者增加成员,这会出现运行时异常。For循环则可以。

                var vectors = GetVectors().ToList();
    
                foreach (var vector in vectors)
                {
                    if (vector.X == 1)
                        //Error
                        //vectors.Remove(vector);
    
                        //This is OK
                        vector.X = 99;
    
                    Console.WriteLine(vector);
                }

    IEnumerable的缺点

    • IEnumerable功能有限,不能插入和删除。
    • 访问IEnumerable只能通过迭代,不能使用索引器。迭代显然是非线程安全的,每次IEnumerable都会生成新的IEnumerator,从而形成多个互相不影响的迭代过程。
    • 在迭代时,只能前进不能后退。新的迭代不会记得之前迭代后值的任何变化。
  • 相关阅读:
    HDU 2639 Bone Collector II (01背包,第k解)
    POJ 2184 Cow Exhibition 奶牛展(01背包,变形)
    hihoCoder #1165 : 益智游戏 (挑战赛11 B题)
    UVA 562 Dividing coins 分硬币(01背包,简单变形)
    POJ Charm Bracelet 挑饰品 (常规01背包)
    hiho一下 第四十四周 博弈游戏·Nim游戏(直接公式解)
    UVA 624 CD(01背包,要记录路径)
    118 Pascal's Triangle 帕斯卡三角形 杨辉三角形
    117 Populating Next Right Pointers in Each Node II 每个节点的右向指针 II
    116 Populating Next Right Pointers in Each Node 每个节点的右向指针
  • 原文地址:https://www.cnblogs.com/haoyifei/p/5768379.html
Copyright © 2011-2022 走看看