В чём отличие работы GetEnumerator()-ов в C#?

Рейтинг: 1Ответов: 2Опубликовано: 15.07.2023
var list = new List<int> { 1, 2, 3 };

var x1 = new { Items = ((IEnumerable<int>)list).GetEnumerator() };
while (x1.Items.MoveNext())
{
    Console.WriteLine(x1.Items.Current);
}

Console.ReadLine();

var x2 = new { Items = list.GetEnumerator() };
while (x2.Items.MoveNext())
{
    Console.WriteLine(x2.Items.Current);
}

Почему первый код выводит 1 2 3 А второй бесконечно спамит 0.

Я понимаю что здесь дело в утиной типизации и методе GetEnumerator(). Но хотелось бы услышать подробное объяснение.

Ответы

▲ 3Принят

Ответ здесь:

static void Main(string[] args)
{
    var list = new List<int> { 1, 2, 3 };
    var x1 = new { Items = ((IEnumerable<int>)list).GetEnumerator() };
    var x2 = new { Items = list.GetEnumerator() };
    Console.WriteLine(ReferenceEquals(x1.Items, x1.Items));
    Console.WriteLine(ReferenceEquals(x2.Items, x2.Items));
}

Вывод в консоль

True
False

А всё почему? А вот почему

public struct Enumerator : IEnumerator<T>, IEnumerator
{
    // ...
}

List<T>.Enumerator - структура. А что происходит, когда вы пытаетесь получить структуру из свойста? Правильно, копирование. Точно так же как это происходит при возврате значения из метода.

Даже вот здесь предупреждение

// CA2013 Do not pass an argument with value type 'System.Collections.Generic.List<int>.Enumerator' to 'ReferenceEquals'.
// Due to value boxing, this call to 'ReferenceEquals' will always return 'false'.
Console.WriteLine(ReferenceEquals(x2.Items, x2.Items));

Апкаст к IEnumerator или IEnumerator<T> позволяет забоксить структуру лишь однажды при присваивании.

А сделано это так для того чтобы при вызове .GetEnumerator() на списке не производилась аллокация в куче. Такой энумератор на стеке отработает быстрее и не будет создавать лишней работы для сборщика мусора.


Вот этот код не сгенерирует мусора.

List<int>.Enumerator enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    Console.WriteLine(enumerator.Current);
}

Даже Dispose этому энумератору вызывать не нужно. Так как он сидит в стеке, то будет уничтожен вместе с возвратом из метода.

▲ -1

Если нужно просто пройтись по списку, то это делается так:

var x3 = list.GetEnumerator();
while (x3.MoveNext())
{
    Console.WriteLine(x3.Current);
}

И всё работает, как надо. Заставить работать исходный код тоже можно, достаточно получить копию структуры в локальную переменную:

var x2 = new { Items = list.GetEnumerator() };
var x4 = x2.Items;
while (x4.MoveNext())
{
    Console.WriteLine(x4.Current);
}

Попробует разобраться, что вы запрограммировали. В первом случае "x1.Items" имеет тип "System.Collections.Generic.IEnumerator<int>", что является ссылочным типом, во втором "x2.Items" имеет тип "System.Collections.Generic.List<int>.Enumerator", что представляет собой структуру (значащий тип).

Единственными членами анонимого типа могут быть только свойства, а получение свойства подразумевает копирование его значения из внутреннего поля объекта. Именно поэтому свойство выдаёт копию структуры, и метод MoveNext() применяется к копии, а не к оригинальной структуре. Можно вместо свойства ипользовать поле, тогда всё будет работать, но если поле "только для чтения", то тоже будет происходить копирование. Иллюстрируется это следующим примером:

using System;
namespace TestBh {
    class AppBh {
        struct Astruct {
            int i;
            public void MoveNext() { i++; }
            public int Current { get { return i; } }
        } 
        class Aclass {
            Astruct item;
            public Astruct Item1 { get { return item; } }
            public Astruct Item2;
            public readonly Astruct Item3;
        } 
        static void Main() {
            var x1 = new Aclass();
            x1.Item1.MoveNext(); // Using property
            Console.WriteLine(x1.Item1.Current); // 0
            x1.Item2.MoveNext(); // Using field
            Console.WriteLine(x1.Item2.Current); // 1
            x1.Item3.MoveNext(); //Using read-only field
            Console.WriteLine(x1.Item3.Current); // 0
        }
    }
}

Вторым решение может быть такое:

class Aclass { public List<int>.Enumerator Items; }
...
var x2 = new Aclass() { Items = list.GetEnumerator() };
while (x2.Items.MoveNext())
{
    Console.WriteLine(x2.Items.Current);
}