>  기사  >  백엔드 개발  >  당신이 모를 수도 있는 C# 트랩, IEnumerable 인터페이스의 샘플 코드에 대한 자세한 설명

당신이 모를 수도 있는 C# 트랩, IEnumerable 인터페이스의 샘플 코드에 대한 자세한 설명

黄舟
黄舟원래의
2017-03-09 15:05:305049검색

모를 수도 있는 C# 트랩, IEnumerable 인터페이스의 샘플 코드에 대한 자세한 설명:

IEnumerable 열거자 인터페이스의 중요성, 그것에 대해 이야기해 봅시다. 말로는 충분하지 않습니다. 거의 모든 컬렉션이 이 인터페이스를 구현하며 Linq의 핵심도 이 범용 인터페이스에 의존합니다. C 언어의 for 루프는 작성하기가 불편하지만 foreach가 훨씬 더 부드럽습니다.

(2) 범위를 벗어나서 열거형에 액세스할 수 있나요? 열거형에서 컬렉션의 값을 변경할 수 없는 이유는 무엇입니까?

(3) Linq의 구체적인 구현은 무엇입니까? 예를 들어 Skip은 일부 요소를

액세스

합니까?

(4) IEnumerable의 본질은 무엇인가요? (5) IEnumerable 열거형에

클로저

가 형성됩니까? 여러 열거 프로세스가 서로 간섭합니까? 열거형 내에서 열거형 요소를 동적으로 변경할 수 있나요?

….관심 있으신 분들은 아래 내용으로 계속 진행하겠습니다.

시작하기 전에 기사에서는 열거형은 IEnumerable이고 반복은 IEnumerator이며 인스턴스화된 것(예: ToList())은 컬렉션이라고 규정하고 있습니다.

1. IEnumerable 및 IEnumerator

IEnumerable에는 GetEnumerator()라는 하나의 추상 메서드만 있고 IEnumerator는 컬렉션에 액세스하는 기능을 실제로 구현하는 반복자입니다. IEnumerator에는 하나의 Current 속성과 두 개의 메서드(MoveNext 및 Reset)만 있습니다.

접근자 인터페이스만 구축하는 것만으로는 충분하지 않은가요? 혼란스러워 보이는 두 개의 인터페이스가 있는 이유는 무엇입니까? 하나는 열거자(enumerator)라고 하고 다른 하나는 반복자(iterator)라고 합니다.

(1) IEnumerator를 구현하는 것은 더러운 작업이기 때문에 두 개의 메서드와 하나의 속성을 추가하는 것이 헛된 일이며 이 두 메서드는 실제로 구현하기 쉽지 않습니다(나중에 언급됨).

(2) 초기 상태를 유지하고 MoveNext와 종료 방법을 알아야 하며 동시에

는 반복의 이전 상태

를 반환하는데 쉽지 않습니다.

(3) 반복은 분명히 스레드로부터 안전하지 않습니다. 각 IEnumerable은 새로운 IEnumerator를 생성하므로 서로 영향을 주지 않는 여러 반복 프로세스를 형성합니다. 반복 프로세스 중에는 반복 컬렉션을 수정할 수 없습니다. 그렇지 않으면 안전하지 않습니다. IEnumerable을 구현하는 한 컴파일러는 IEnumerator 구현을 도와줄 것입니다. 또한 대부분의 경우 기존 컬렉션에서 상속되므로 일반적으로 MoveNext 및 Reset 메서드를 재정의할 필요가 없습니다. 물론 IEnumerable에는 문제 논의에 영향을 주지 않는 일반 구현도 있습니다.

IEnumerable은 단방향 연결 목록을 연상시킵니다. C에서는 다음 노드의 정보를 저장하기 위해 포인터 필드가 필요합니다. 그러면 IEnumerable에서는 이 정보를 저장하는 데 도움을 주는 사람이 누구입니까? 이 프로세스가 메모리를 차지합니까? 프로그램 영역을 점유합니까, 아니면 힙 영역을 점유합니까?

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)으로 구현된 경우 문제 없습니다. 실제 메모리이기도 하지만 위의 예인 경우에는 어떻게 될까요? 필터링에 의해 반환된 수익률 반환은 요소만 반환하지만 이 실제 컬렉션은 존재하지 않을 수 있습니다. 간단한 열거자의 수익률 반환을 디컴파일하면 실제로는 스위치 케이스 집합이고 컴파일러가 다음에서 작동하고 있음을 알 수 있습니다. 배경은 우리를 위해 많은 일을 해주었습니다.

새 반복자가 생성될 때 MoveNext가 사용되지 않으면 현재는 실제로 비어 있습니다. 이유는 무엇입니까? 반복자가 헤드 요소를 직접 가리키지 않는 이유는 무엇입니까? (답변 감사합니다: 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가 항복 중단을 회색(중복)으로 표시한다는 점에 유의하세요.

我们再写下如下的测试代码,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로 작성됩니까?

(7) IEnumerable을 세트로 인스턴스화한 다음 정렬할 수 있습니까? 매우 큰 가상 컬렉션인 경우 이를 최적화하는 방법은 무엇입니까?

위 내용은 당신이 모를 수도 있는 C# 트랩, IEnumerable 인터페이스의 샘플 코드에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.