Heim  >  Artikel  >  Backend-Entwicklung  >  C#-Traps, die Sie vielleicht nicht kennen, ausführliche Erläuterung des Beispielcodes der IEnumerable-Schnittstelle

C#-Traps, die Sie vielleicht nicht kennen, ausführliche Erläuterung des Beispielcodes der IEnumerable-Schnittstelle

黄舟
黄舟Original
2017-03-09 15:05:305115Durchsuche

C#-Traps, die Sie vielleicht nicht kennen, detaillierte Erklärung des Beispielcodes der IEnumerable-Schnittstelle:

Die Bedeutung der IEnumerable-Enumeratorschnittstelle, Lass uns darüber reden. Keine Worte reichen aus. Fast alle Sammlungen implementieren diese Schnittstelle, und auch der Kern von Linq basiert auf dieser universellen Schnittstelle. Das Schreiben der for-Schleife in der C-Sprache ist nervig, aber foreach ist viel reibungsloser.

Mir gefällt diese Schnittstelle sehr gut, aber ich stoße bei der Verwendung auch auf viele Fragen. Haben Sie die gleiche Verwirrung wie ich:

(1) Was ist der Unterschied zwischen IEnumerable und IEnumerator?

(2) Kann auf Aufzählungen außerhalb der Grenzen zugegriffen werden? Welche Konsequenzen hat der Zugriff außerhalb der Grenzen? Warum kann der Wert einer Sammlung in einer Aufzählung nicht geändert werden?

(3) Was ist die spezifische Implementierung von Linq? Überspringt Skip beispielsweise einige Elemente auf ?

(4) Was ist die Essenz von IEnumerable?

(5) Wird in der IEnumerable-Aufzählung ein Abschluss gebildet? Beeinträchtigen mehrere Aufzählungsprozesse einander? Kann ich die Elemente einer Aufzählung innerhalb einer Aufzählung dynamisch ändern?

….

Bei Interesse fahren wir mit den folgenden Inhalten fort.

Bevor wir beginnen, legt unser Artikel fest, dass eine Aufzählung ein IEnumerable, eine Iteration ein IEnumerator und eine instanziierte Instanz (z. B. ToList()) eine Sammlung ist.

1. IEnumerable und IEnumerator

IEnumerable hat nur eine abstrakte Methode: GetEnumerator(), und IEnumerator ist ein Iterator, der die Funktion des Zugriffs auf eine Sammlung wirklich realisiert. IEnumerator verfügt nur über eine Current-Eigenschaft und zwei Methoden: MoveNext und Reset.

Es gibt ein kleines Problem. Reicht es nicht aus, nur eine Accessor-Schnittstelle zu erstellen? Warum zwei Schnittstellen, die verwirrend aussehen? Einer wird als Enumerator und der andere als Iterator bezeichnet. Da

(1) die Implementierung von IEnumerator eine schmutzige Arbeit ist, ist das Hinzufügen von zwei Methoden und einem Attribut vergeblich, und diese beiden Methoden sind tatsächlich nicht einfach zu implementieren (wird später erwähnt).

(2) Es muss den Anfangszustand beibehalten, wissen, wie man als nächstes vorgeht und wie man es beendet, und gleichzeitig den vorherigen Zustand der Iteration zurückgeben , was nicht einfach ist.

(3) Die Iteration ist offensichtlich nicht threadsicher. Jedes IEnumerable generiert einen neuen IEnumerator und bildet so mehrere Iterationsprozesse, die sich nicht gegenseitig beeinflussen. Während des Iterationsprozesses kann die Iterationssammlung nicht geändert werden, da sie sonst unsicher ist.

Solange Sie also IEnumerable implementieren, hilft uns der Compiler bei der Implementierung von IEnumerator. Darüber hinaus werden sie in den meisten Fällen von vorhandenen Sammlungen geerbt und es besteht im Allgemeinen keine Notwendigkeit, die Methoden MoveNext und Reset zu überschreiben. Natürlich verfügt IEnumerable auch über eine generische Implementierung, die jedoch keinen Einfluss auf die Diskussion des Problems hat.

IEnumerable erinnert uns an eine einseitig verknüpfte Liste. In C wird ein Zeigerfeld benötigt, um die Informationen des nächsten Knotens zu speichern. Beansprucht dieser Vorgang Speicher? Besetzt es den Programmbereich oder den Heap-Bereich?

IEnumerable hat jedoch auch seine Mängel: Es kann nicht zurückgehen oder springen (es kann nur einzeln springen), und es ist nicht einfach, ein Zurücksetzen zu implementieren, und es kann kein Indexzugriff erreicht werden. Denken Sie darüber nach: Wenn es sich um einen Aufzählungsprozess einer Sammlung von Instanzen handelt, kehren Sie einfach direkt zum 0. Element zurück. Wenn es sich bei diesem IEnumerable jedoch um eine lange Zugriffskette handelt, ist es sehr schwierig, den ursprünglichen Stamm zu finden! Der Autor von CLR über C# sagt Ihnen also, dass viele Reset-Implementierungen einfach Lügen sind. Wissen Sie einfach, dass dieses Ding existiert, und verlassen Sie sich nicht zu sehr darauf.

2. Gibt es einen Unterschied zwischen foreach und MoveNext?

Das größte Merkmal von IEnumerable ist, dass es den Zugriffsprozess unter die Kontrolle des Besuchers stellt. In der Sprache C erfolgt die Array-Steuerung vollständig extern. Diese Schnittstelle kapselt den Zugriffsprozess intern und verbessert so die Kapselung weiter. Zum Beispiel:

public class People  //定义一个简单的实体类
    {
        public string Name { get; set; }
        public int Age { get; set; }
    }

    public class PersonList
    {
        private readonly List<People> peoples;

        public PersonList()  //为了方便,构造过程中插入元素
        {
            peoples = new List<People>();
            for (int i = 0; i < 5; i++)
            {
                peoples.Add(new People {Name = "P" + i, Age = 30 + i});
            }
        }

        public int OldAge = 31;
        public IEnumerable<People> OlderPeoples
        {
            get
            {
                foreach (People people in _people)
                {
                    if (people.Age > OldAge)
                        yield return people;
                }
                yield break;
            }
        }
    }

Das Wesen von IEnumerable ist eine Zustandsmaschine, die dem Konzept von Ereignissen etwas ähnelt. Sie wirft die Implementierung nach außen und realisiert Reisen zwischen Codes (denken Sie an interstellar). Reisen), Dies ist die Grundlage von Linq. Sind coole Iteratoren wirklich so einfach, wie wir denken?

In der Sprache C ist ein Array ein Array, ein echter Speicherplatz. Was bedeutet also IEnumerable? Wenn es durch eine echte Sammlung (z. B. Liste) implementiert wird, ist das kein Problem, es ist auch echter Speicher, aber was ist, wenn es sich um das obige Beispiel handelt? Der durch die Filterung zurückgegebene Yield-Return gibt nur Elemente zurück, aber diese tatsächliche Sammlung ist möglicherweise nicht vorhanden. Wenn Sie den Yield-Return eines einfachen Enumerators dekompilieren, werden Sie feststellen, dass es sich tatsächlich um eine Reihe von Switch-Fällen handelt und der Compiler in der arbeitet Hintergrund. Hat uns viel Arbeit gebracht.

Current ist tatsächlich leer, wenn MoveNext nicht im neuen generierten Iterator verwendet wird. Warum zeigt ein Iterator nicht direkt auf das Kopfelement?

(Danke für die Antwort: Genau wie der Kopfzeiger einer einseitig verknüpften Liste in C-Sprache können Sie auf diese Weise eine Aufzählung angeben, die keine Elemente enthält, was die Programmierung komfortabler macht)

foreach jedes Mal einen Schritt vorwärts bewegen und anhalten, wenn das Ende erreicht ist. Warten Sie, sind Sie sicher, dass es aufhört, wenn es zu Ende ist? Machen wir ein Experiment:

public IEnumerable<People> Peoples1   //直接返回集合
        {
            get { return peoples; }
        }public IEnumerable<People> Peoples2  //包含yield break;
        {
            get
            {
                foreach (var people in peoples)
                {
                    yield return people;
                }
                yield break;  //其实这个用不用都可以
            }
        }

Die beiden oben genannten sind unsere gängigen Methoden. Achten Sie auf die zweite Implementierung, die den Ertragsbruch in Grau markiert (Duplikat).

我们再写下如下的测试代码,peopleList集合只有五个元素,但尝试去MoveNext 8次。可以把peopleList.Peoples1换成2,3,分别测试。

            var peopleList = new PeopleList();  //内部构造函数插入了五个元素
            IEnumerator<People> e1 = peopleList.Peoples1.GetEnumerator();
            if (e1.Current == null)
            {
                Console.WriteLine("迭代器生成后Current为空");
            }
            int i = 0;
            while (i<8)  //总共只有五个元素,看看一直迭代会发生什么效果
            {
                e1.MoveNext();
                if (e1.Current == null)
                {
                    Console.WriteLine("迭代第{0}次后为空",i);
                }
                else
                {
                    Console.WriteLine("迭代第{0}次后为{1}",i,e1.Current.Name);
                }
                i++;
            }
//PeopleEnumerable1   (直接返回集合)
迭代器生成后Current为空
迭代第0次后为P0
迭代第1次后为P1
迭代第2次后为P2
迭代第3次后为P3
迭代第4次后为P4
迭代第5次后为空
迭代第6次后为空
迭代第7次后为空

//PeopleEnumerable2 (不加yield break)
迭代器生成后Current为空
迭代第0次后为P0
迭代第1次后为P1
迭代第2次后为P2
迭代第3次后为P3
迭代第4次后为P4
迭代第5次后为P4
迭代第6次后为P4
迭代第7次后为P4

//PeopleEnumerable2 (加上yield break)
迭代器生成后Current为空
迭代第0次后为P0
迭代第1次后为P1
迭代第2次后为P2
迭代第3次后为P3
迭代第4次后为P4
迭代第5次后为P4
迭代第6次后为P4
迭代第7次后为P4

越界枚举测试结果

真让人吃惊,返回原始集合,越界之后就返回null了,但如果是MoveNext,不论有没有加yield break, 越界迭代后还是返回最后一个元素! 也许就是我们在第1节里提到的,迭代器只返回上一次的状态,因为无法后移,所以就重复返回,那为什么List集合就不会这样呢?问题留给大家。

(感谢回答:越界枚举到底是null还是最后一个元素的问题,其实没有明确规定,具体看.NET的实现,在.NET Framework中,越界后依然是最后一个元素)。

不过各位看官尽管放心,在foreach的标准枚举过程下,枚举是肯定能枚举完的,这就说明了MoveNext和foreach两种在实现上的不同,显然foreach更安全。同时还注意,不能在yield过程中实现try-catch代码块,为什么呢?因为yield模式组合了来自不同位置的代码和逻辑,怎么可能靠编译给每个引用的代码块加上try-catch?这太复杂了。

枚举的特性在处理大数据的时候很有帮助,就是因为它的状态性,一个超大的文件,我只要每次读一部分,就可以顺次的读取下去,直到文件结束,由于不需要实例化集合,内存占用是很低的。对数据库也是如此,每次读取一部分,就能应对很多难以应付的情况。

3.在枚举中修改枚举器参数?

在枚举过程中,集合是不能被修改的,比如在foreach循环中,如果插入或者删除一个元素,肯定会报运行时异常。有经验的程序员告诉 你,此时用for循环。for和foreach的本质区别是什么呢?

在MoveNext中,我突然改变了枚举的参数,使得它的数据量变多或者变少了,又会发生什么?

           Console.WriteLine("不修改OldAge参数");
            foreach (var olderPeople in peopleList.OlderPeoples)
            {
                Console.WriteLine(olderPeople);

            }

            Console.WriteLine("修改了OldAge参数");
            i = 0;
            foreach (var olderPeople in peopleList.OlderPeoples)
            {
                Console.WriteLine(olderPeople);
                i++;
                if (i ==1)
                    peopleList.OldAge = 33;  //只枚举一次后,修改OldAge 的值
            }

测试结果是:

不修改OldAge参数
ID:2,NameP2,Age32
ID:3,NameP3,Age33
ID:4,NameP4,Age34

修改了OldAge参数
ID:2,NameP2,Age32
ID:4,NameP4,Age34

可以看到,在枚举过程中修改了控制枚举的值,能动态改变枚举的行为。上面是在一个yield结构中改变变量的情况,我们再试试在迭代器和Lambda表达式的情况(代码略), 得到结果是:

在迭代中修改变量值
ID:2,NameP2,Age32
ID:4,NameP4,Age34
在Lambda表达式中修改变量值
ID:2,NameP2,Age32
ID:4,NameP4,Age34

可以看出,外部修改变量能够控制内部的迭代过程,动态改变了“集合的元素”。 这是一个好事,因为它的行为确实是对的;也是坏事:在迭代过程中,修改了变量的值,上下文语境变化,可是如果还按之前的语境进行处理,显然就会酿成大错。 这里和闭包没关系。

因此,如果一个枚举需要在上下文会发生变化的情况下保持原有的行为,就需要手动保存变量的副本。

如果你把两个集合A,B用Concat函数顺次拼接起来,也就是A-B, 而且不实例化,那么在枚举A的阶段中,修改集合B的元素,会报错么? 为什么?

比如如下的测试代码:

       List<People> peoples=new List<People>(){new People(){Name = "PA"}};
            Console.WriteLine("将一个虚拟枚举A连接到集合B,并在枚举A阶段修改集合B的元素");
            var e8 = peopleList.PeopleEnumerable1.Concat(peoples);
            i = 0;
            foreach (var people in e8)
            {
                Console.WriteLine(people);
                i++;
                if (i == 1)   
                  peoples.Add(new People(){Name = "PB"});  //此时还在枚举PeopleEnumerable1阶段
        }

如果你想知道,可以自己做个试验(在我附件里也有这个例子)。留给大家讨论。

4. 更多LINQ的讨论

你可以在yield中插入任何代码,这就是延迟(Lazy)的表现,只是需要执行的时候才执行。 我们不难想象Linq很多函数的实现方式,比较有意思的包括Concat,它将两个集合连在了一起,就像下面这样:

public static IEnumerable<T> Concat<T>(this IEnumerable<T> source, IEnumerable<T> source2)
       {
           foreach (var r in source)
           {
               yield return r;
           }
           foreach (var r in source2)
           {
               yield return r;
           }
       }

还有Select, Where都好实现,就不讨论了。

Skip怎么实现的呢?  它跳过了集合中的一部分元素,我猜是这样的:

public static IEnumerable<T> Skip<T>(this IEnumerable<T> source, int count)
       {
           int t = 0;
           foreach (var r in source)
           {
               t++;
               if(t<=count)
                   continue;
               yield return r;
           }
       }

那么,被跳过的元素,到底被访问过没有?它的代码被执行了么?

 Console.WriteLine("Skip的元素是否会被访问到?");
 IEnumerable<People> e6 = peopleList.PeopleEnumerable1.Select(d =>
       {
              Console.WriteLine(d);
              return d;
       }).Skip(3);
 Console.WriteLine("只枚举,什么都不做:");
 foreach (var  r in e6){}  
 Console.WriteLine("转换为实体集合,再次枚举");
 IEnumerable<People> e7 = e6.ToList();
 foreach (var r in e7){}

测试结果如下:

只枚举,什么都不做:
ID:0,NameP0,Age30
ID:1,NameP1,Age31
ID:2,NameP2,Age32
ID:3,NameP3,Age33
ID:4,NameP4,Age34
转换为实体集合,再次枚举
ID:0,NameP0,Age30
ID:1,NameP1,Age31
ID:2,NameP2,Age32
ID:3,NameP3,Age33
ID:4,NameP4,Age34

可以看出,Skip虽然是跳过,但还是会“访问”元素的,因此会执行额外的操作,比如lambda表达式,这不论是枚举器还是实体集合都是如此。这个角度说,要优化表达式,应当尽可能在linq中早的Skip和Take,以减少额外的副作用。

但对于Linq to SQL的实现中,显然Skip是做过额外优化的。我们是否也能优化Skip的实现,使得上层尽可能提升海量数据下的Skip性能呢?

5. 有关IEnumerable枚举的更多问题

(1) 枚举过程如何暂停?有暂停这一说么? 如何取消?

(2) PLinq的实现原理是什么?它改变的到底是IEnumerable接口的哪种特性?是否产生了乱序枚举?这种乱序枚举到底是怎么实现?

(3) IEnumerable实现了链条结构,这是Linq的基础,但这个链条的本质是什么?

(4) Da IEnumerable Status und Verzögerung darstellt, ist es nicht schwer zu verstehen, dass die Essenz vieler asynchroner Vorgänge IEnumerable ist. Während eines meiner Interviews wurde ich nach dem Wesen der Asynchronität gefragt. Was ist Ihrer Meinung nach das Wesen der Asynchronität? Async ist kein Multithreading! Das Schöne an Asynchronität ist im Wesentlichen die Neuorganisation des Codes, da langfristige asynchrone Operationen Zustandsmaschinen sind. . . Zum Beispiel die CCR-Bibliothek. Ich werde hier nicht näher darauf eingehen, da es vorübergehend außerhalb der Wissensreserven des Autors liegt. Ich werde beim nächsten Mal darüber sprechen.

(5) Wenn derselbe Enumerator in der C-Sprache implementiert ist, kann dann derselbe coole Linq implementiert werden, ohne auf einen Compiler angewiesen zu sein? Lassen Sie uns den Lambda-Trick nicht erwähnen, sondern verwenden Sie Funktionszeiger.

(6) IEnumerable ist in MapReduce geschrieben?

(7) Wie wird IEnumerable in eine Menge instanziiert und dann sortiert? Wenn es sich um eine sehr große virtuelle Sammlung handelt, wie kann sie optimiert werden?

Das obige ist der detaillierte Inhalt vonC#-Traps, die Sie vielleicht nicht kennen, ausführliche Erläuterung des Beispielcodes der IEnumerable-Schnittstelle. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn