搜尋
首頁後端開發C#.Net教程C#你可能不知道的陷阱, IEnumerable介面的範例程式碼詳解

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
C#.NET開發人員社區:資源和支持C#.NET開發人員社區:資源和支持May 06, 2025 am 12:11 AM

C#.NET開發者社區提供了豐富的資源和支持,包括:1.微軟的官方文檔,2.社區論壇如StackOverflow和Reddit,3.GitHub上的開源項目,這些資源幫助開發者從基礎學習到高級應用,提升編程技能。

C#.NET優勢:功能,好處和用例C#.NET優勢:功能,好處和用例May 05, 2025 am 12:01 AM

C#.NET的優勢包括:1)語言特性,如異步編程簡化了開發;2)性能與可靠性,通過JIT編譯和垃圾回收機制提升效率;3)跨平台支持,.NETCore擴展了應用場景;4)實際應用廣泛,從Web到桌面和遊戲開發都有出色表現。

C#總是與.NET關聯嗎?探索替代方案C#總是與.NET關聯嗎?探索替代方案May 04, 2025 am 12:06 AM

C#並不總是與.NET捆綁在一起。 1)C#可以在Mono運行時環境中運行,適用於Linux和macOS。 2)在Unity遊戲引擎中,C#用於腳本編寫,不依賴.NET框架。 3)C#還可用於嵌入式系統開發,如.NETMicroFramework。

.NET生態系統:C#的角色和超越.NET生態系統:C#的角色和超越May 03, 2025 am 12:04 AM

C#在.NET生態系統中扮演核心角色,是開發者的首選語言。 1)C#提供高效、易用的編程方式,結合C、C 和Java的優點。 2)通過.NET運行時(CLR)執行,確保跨平台高效運行。 3)C#支持從基本到高級的用法,如LINQ和異步編程。 4)優化和最佳實踐包括使用StringBuilder和異步編程,提高性能和可維護性。

C#作為.NET語言:生態系統的基礎C#作為.NET語言:生態系統的基礎May 02, 2025 am 12:01 AM

C#是微軟在2000年發布的編程語言,旨在結合C 的強大功能和Java的簡潔性。 1.C#是一種類型安全、面向對象的編程語言,支持封裝、繼承和多態。 2.C#的編譯過程將代碼轉化為中間語言(IL),然後在.NET運行時環境(CLR)中即時編譯成機器碼執行。 3.C#的基本用法包括變量聲明、控制流和函數定義,而高級用法涵蓋異步編程、LINQ和委託等。 4.常見錯誤包括類型不匹配和空引用異常,可通過調試器、異常處理和日誌記錄來調試。 5.性能優化建議包括使用LINQ、異步編程和提高代碼可讀性。

c#vs. .net:澄清關鍵差異和相似之處c#vs. .net:澄清關鍵差異和相似之處May 01, 2025 am 12:12 AM

C#是一種編程語言,而.NET是一個軟件框架。 1.C#由微軟開發,適用於多平台開發。 2..NET提供類庫和運行時環境,支持多語言。兩者協同工作,構建現代應用。

超越炒作:評估C#.NET的當前作用超越炒作:評估C#.NET的當前作用Apr 30, 2025 am 12:06 AM

C#.NET是一個強大的開發平台,結合了C#語言和.NET框架的優勢。 1)它廣泛應用於企業應用、Web開發、遊戲開發和移動應用開發。 2)C#代碼編譯成中間語言後由.NET運行時環境執行,支持垃圾回收、類型安全和LINQ查詢。 3)使用示例包括基本控制台輸出和高級LINQ查詢。 4)常見錯誤如空引用和類型轉換錯誤可以通過調試器和日誌記錄解決。 5)性能優化建議包括異步編程和優化LINQ查詢。 6)儘管面臨競爭,C#.NET通過不斷創新保持其重要地位。

See all articles

熱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

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

熱工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具

SublimeText3 英文版

SublimeText3 英文版

推薦:為Win版本,支援程式碼提示!

MantisBT

MantisBT

Mantis是一個易於部署的基於Web的缺陷追蹤工具,用於幫助產品缺陷追蹤。它需要PHP、MySQL和一個Web伺服器。請查看我們的演示和託管服務。

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

Atom編輯器mac版下載

Atom編輯器mac版下載

最受歡迎的的開源編輯器