Почему некоторые Enumerable могут быть изменены внутри foreach, а другие нет? - PullRequest
0 голосов
/ 12 апреля 2020

Я обнаружил интересное поведение результатов запросов LINQ при работе с C#. Я пытаюсь понять это, но не могу найти правильного объяснения, почему это работает так, как есть. Поэтому я спрашиваю здесь, может быть, кто-то может дать мне хорошее объяснение (внутренней работы, которая приводит к такому поведению) или, возможно, некоторые ссылки.

У меня есть этот класс:

    public class A
    {
        public int Id { get; set; }

        public int? ParentId { get; set; }
    }

И этот объект:

var list = new List<A> 
            { 
                new A { Id = 1, ParentId = null }, 
                new A { Id = 2, ParentId = 1 }, 
                new A { Id = 3, ParentId = 1 }, 
                new A { Id = 4, ParentId = 3 },
                new A { Id = 5, ParentId = 7 }
            };

И мой код, который работает с этим объектом:

var result = list.Where(x => x.Id == 1).ToList();
var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId));

Console.WriteLine(result.Count); // 1
Console.WriteLine(valuesToInsert.Count()); //2

foreach (var value in valuesToInsert)
{
    result.Add(value);
}

Console.WriteLine(valuesToInsert.Count()); //3. collection (and its count) was changed inside the foreach loop
Console.WriteLine(result.Count); //4

Итак, переменная Count result равна 1, valuesToInsert count равна 2, и после foreach l oop (который явно не меняет valuesToInsert) счетчик valuesToInsert меняется. И, хотя в начале foreach число valuesToInsert было две , foreach делает три итерации.

Так почему значение этого перечислимого можно изменить внутри foreach? И, например, если я использую этот код для изменения значения Enumerable:

var testEn = list.Where(x => x.Id == 1);
foreach (var x in testEn)
{
    list.Add(new A { Id = 1 });
}

, я получу System.InvalidOperationException: 'Collection was modified; enumeration operation may not execute.'. Какая разница между ними? Почему одна коллекция может быть изменена, а другая - нет?

PS Если я добавлю ToList() вот так:

var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId)).ToList();

Или вот так:

foreach (var value in valuesToInsert.ToList())

Это делает только две итерации.

Ответы [ 3 ]

0 голосов
/ 12 апреля 2020

Коллекция valuesToInsert содержит ссылку на коллекцию result в предложении Where:

var valuesToInsert = list.Where(x => result.Any(y => y.Id == x.ParentId));

Поскольку Enumerable работает с использованием возврата дохода, который он использует самая последняя коллекция result для каждого произведенного предмета.

Если вы не хотите, чтобы это поведение вы сначала оценили valueToInsert , используя ToList()

foreach (var value in valuesToInsert.ToList())

Относительно исключения «Коллекция была изменена». Вы не можете изменить перечисляемое, пока оно перечисляется. Теперь коллекция result изменяется, но не во время перечисления; он перечисляется только каждый раз, когда для каждого l oop запрашивается новый элемент. (Это делает ваш алгоритм добавления дочерних элементов менее эффективным, что станет заметным для огромных коллекций.)

0 голосов
/ 13 апреля 2020

Этот блок кода:

foreach (var value in valuesToInsert)
{
    result.Add(value);
}

... преобразуется компилятором C# в этот эквивалентный блок кода:

IEnumerator<A> enumerator = valuesToInsert.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        var value = enumerator.Current;
        result.Add(value);
    }
}
finally
{
    enumerator.Dispose();
}

Перечислитель, возвращаемый List становится недействительным, когда List видоизменен, что означает, что метод MoveNext сгенерирует InvalidOperationException, если он вызывается после мутации. В этом случае valuesToInsert - это не List, а перечислимое, возвращаемое методом LINQ Where. Этот метод работает путем перечисления перечислителя, который он получает лениво по своему источнику, который в данном случае является list. Таким образом, перечисление одного перечислителя косвенно вызывает перечисление другого, которое скрыто глубже в цепочке magi c LINQ. В первом случае list не изменяется в блоке перечисления, поэтому исключение не выдается. Во втором случае он мутирует, вызывая исключение, которое распространяется от одного MoveNext к другому и, в конечном итоге, вызывается оператором foreach.

Стоит отметить, что это поведение не является частью договор publi c класса List, так что он может быть изменен в будущей версии. NET. Поэтому вам, вероятно, следует избегать зависимости от этого поведения для правильности вашей программы. Это предупреждение не теоретическое. Подобное изменение уже произошло с классом Dictionary в. NET Core 3.0.

0 голосов
/ 12 апреля 2020

Здесь есть несколько вопросов:

Итак, после первого запроса Count переменной результата равен 1, после второго значения valuesToInsert count равно 2 и после foreach l oop (который не ' t изменить valuesToInsert в явном виде) счетчик valuesToInsert меняется.

Как и ожидалось, потому что ссылка, которую мы имеем в переменной, такая же, как и у valuesToInsert, которую держит переменная. Таким образом, объект один и тот же, но несколько ссылок указывают на один и тот же.

Ваш второй вопрос:

Так почему же значение этого перечисляемого может быть изменено внутри foreach?

Коллекция IEnumerable доступна только для чтения, когда мы имеем коллекцию как ссылку типа IEnumerable, но когда мы вызываем метод ToList(), у нас есть копия коллекции, которая указывает на ту же исходную коллекцию, но теперь мы можем добавить больше элементов в коллекцию.

Когда у нас есть коллекция как IEnumerable, коллекция может быть повторена и прочитана, но добавление большего количества элементов при перечислении не будет выполнено, поскольку предполагается, что коллекция будет считываться последовательно.

Thrid:

Он делает только две итерации.

Да, потому что в тот момент времени, сколько бы предметов ни было в коллекции, было перечислено и ссылка на нее получена хранится как новый список, пока он все еще указывает на тот же объект, т.е. IEnumerable, но теперь мы можем добавить больше элементов из-за к его типу как List.

См .:

var result = list.Where(x => x.Id == 1).ToList(); 
// result is collection which can be modified, items add, remove etc

var result = list.Where(x => x.Id == 1);
 // result is IEnumerable which can be iterated to get items one by one
 // modifying this collection would error out normally
...