目錄
1.  IEnumerable 與  IEnumerator
2. foreach和MoveNext有差別嗎
3.在枚举中修改枚举器参数?
4. 更多LINQ的讨论
5. 有关IEnumerable枚举的更多问题
首頁 後端開發 C#.Net教程 C#你可能不知道的陷阱, IEnumerable介面的範例程式碼詳解

C#你可能不知道的陷阱, IEnumerable介面的範例程式碼詳解

Mar 09, 2017 pm 03:05 PM

C#你可能不知道的陷阱, IEnumerable介面的範例程式碼詳解:

IEnumerable枚舉器介面的重要性,說一萬句話都不過分。幾乎所有集合都實現了這個接口,Linq的核心也依賴這個萬能的接口。 C語言的for迴圈寫得心煩,foreach就順暢了很多。

我很喜歡這個接口,但在使用上也遇到不少的疑問,你是不是也有跟我一樣的困惑:

(1) IEnumerable 與  IEnumerator到底有什麼區別

(2) 列舉能否越界訪問,越界訪問是什麼後果?為什麼在枚舉中不能改變集合的值?

(3) Linq的具體實作到底是怎樣的,例如Skip,它跳過了一些元素,那麼這些元素被訪問到了麼?

(4) IEnumerable 的本質是什麼?

(5) IEnumerable 枚舉中是否會形成閉包?多個枚舉過程會不會互相干擾?能否在枚舉中動態改變枚舉的元素?

….

如果有興趣,我們接著下面的內容。

開始之前,我們的文章規定,枚舉就是IEnumerable,迭代就是IEnumerator,已經被實例化(例如ToList())就是集合。

1.  IEnumerable 與  IEnumerator

IEnumerable只有一個抽象方法:GetEnumerator(),而IEnumerator又是一個迭代器,真正實現了存取集合的功能。  IEnumerator只有一個Current屬性,MoveNext和Reset兩個方法。

有個小問題,只搞一個訪問器介面不就得了?為什麼要兩個看起來很容易混淆的介面呢?一個叫枚舉器,一個叫迭代器。因為

(1) 實作IEnumerator是個髒活累活,白白加了兩個方法一個屬性,而且這兩個方法其實並不好實現(後面會提到)。

(2) 它需要維護初始狀態,知道如何MoveNext ,如何結束,同時返回迭代的上一個狀態,這些並不容易。

(3)迭代顯然是非執行緒安全的,每次IEnumerable都會產生新的IEnumerator,從而形成多個互相不影響的迭代過程。在迭代過程中,不能修改迭代集合,否則不安全。

所以只要實作了IEnumerable,編譯器就會幫我們實作IEnumerator。何況絕大多數情況都是從現有集合繼承,一般不需要重寫MoveNext和Reset方法。 IEnumerable當然還有泛型實現,這個不影響問題的討論。

IEnumerable讓我們想起了單向鍊錶,C中需要一個指針域保存下一個節點的信息,那麼在IEnumerable中,誰幫忙保存了這個信息?這個過程佔用記憶體麼? 是佔在程式區,還是堆區?

但是,IEnumerable也有它的缺點,它沒法後退,沒法跳躍(只能一個一個的跳​​過去),而且實現Reset並不容易,無法實現索引訪問。想想看, 如果是實例集合的枚舉過程,直接回到第0個元素就可以了,但是如果這個IEnumerable是漫長的訪問鏈條,想找到最初的根是很困難的!所 以CLR via C#的作者告訴你,其實很多Reset的實現根本就是謊言,知道有這個東西就行了,不要太過依賴它。

2. foreach和MoveNext有差別嗎

IEnumerable最大的特點是將存取的過程,交給了被訪客本身控制。在C語言中數組控制權是外在完全掌握的。這個介面卻在內部封裝存取了的過程,進一步提升了封裝性。例如下面:

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;
            }
        }
    }
登入後複製

IEnumerable的本質是狀態​​機,它有點類似事件的概念,將實現丟到外面,實現程式碼間的穿越(想想星際效應),這是Linq的基礎。酷炫的迭代器,真的有我們想像的那麼簡單呢?

在C語言中,數組就是數組,實實在在的記憶體空間,那麼IEnumerable到底是什麼意思呢?如果它是由一個真正的集合(例如List)實現,那麼沒問題,也是實實在在的內存,可是如果是上述的例子呢?篩選返回的yield return 只返回了元素,但可能並不存在這個實際的集合,如果你將簡單的枚舉器的yield return 反編譯後看,會發現其實是一組switch-case, 編譯器在後台為我們做了大量的工作。

產生的新迭代器,如果不MoveNext,其實Current是空的,這是為什麼呢?為什麼一個迭代器不直接指向頭元素呢?

(感謝回答:就像C語言的單向鍊錶的頭指標一樣,這樣可以指定一個不包含任何元素的枚舉,程式設計起來更方便)

foreach每次往前移動一格,到頭了就停止。 等等,你確定它到頭了就會停止麼?我們來做個試驗:

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

以上兩種,是我們常見的方式,注意第二種實現,ReSharper把yield break標成灰色(重複)。

我们再写下如下的测试代码,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) 因為IEnumerable代表了狀態和延遲,因此不難理解許多非同步操作的本質就是IEnumerable。我有一次面試時候,問到了異步的實質,你說異步的實質是什麼?異步不是多線程!非同步的精彩,本質上是程式碼的重新組合,因為長時間的非同步操作就是狀態機。 。 。比如CCR庫。這裡不準備展開說,因為暫時超過了作者的知識儲備,下次再說。

(5) 如果用C語言來實現同樣的枚舉器,同樣酷炫的Linq,不靠編譯器能實現麼?先不提Lambda的梗,我們用函數指標。

(6) IEnumerable寫入MapReduce? Linq for MapReduce?

#(7) IEnumerable如何Sort? 實例化為一個集合再排序麼?如果是一個超大的虛擬集合,如何最佳化?

以上是C#你可能不知道的陷阱, IEnumerable介面的範例程式碼詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1662
14
CakePHP 教程
1418
52
Laravel 教程
1311
25
PHP教程
1261
29
C# 教程
1234
24
使用 C# 的活動目錄 使用 C# 的活動目錄 Sep 03, 2024 pm 03:33 PM

使用 C# 的 Active Directory 指南。在這裡,我們討論 Active Directory 在 C# 中的介紹和工作原理以及語法和範例。

C# 序列化 C# 序列化 Sep 03, 2024 pm 03:30 PM

C# 序列化指南。這裡我們分別討論C#序列化物件的介紹、步驟、工作原理和範例。

C# 中的隨機數產生器 C# 中的隨機數產生器 Sep 03, 2024 pm 03:34 PM

C# 隨機數產生器指南。在這裡,我們討論隨機數產生器的工作原理、偽隨機數和安全數的概念。

C# 資料網格視圖 C# 資料網格視圖 Sep 03, 2024 pm 03:32 PM

C# 資料網格視圖指南。在這裡,我們討論如何從 SQL 資料庫或 Excel 檔案載入和匯出資料網格視圖的範例。

C# 中的階乘 C# 中的階乘 Sep 03, 2024 pm 03:34 PM

C# 階乘指南。這裡我們討論 C# 中階乘的介紹以及不同的範例和程式碼實作。

c#多線程和異步的區別 c#多線程和異步的區別 Apr 03, 2025 pm 02:57 PM

多線程和異步的區別在於,多線程同時執行多個線程,而異步在不阻塞當前線程的情況下執行操作。多線程用於計算密集型任務,而異步用於用戶交互操作。多線程的優勢是提高計算性能,異步的優勢是不阻塞 UI 線程。選擇多線程還是異步取決於任務性質:計算密集型任務使用多線程,與外部資源交互且需要保持 UI 響應的任務使用異步。

C# 中的模式 C# 中的模式 Sep 03, 2024 pm 03:33 PM

C# 模式指南。在這裡,我們討論 C# 中模式的介紹和前 3 種類型,以及其範例和程式碼實作。

C# 中的質數 C# 中的質數 Sep 03, 2024 pm 03:35 PM

C# 質數指南。這裡我們討論c#中素數的介紹和範例以及程式碼實作。

See all articles