Распределение памяти при использовании циклов foreach в С#

Я знаю основы того, как петли foreach работают на С# (Как работают циклы foreach на С#)

Мне интересно, использует ли foreach память, которая может вызывать сборку мусора? (для всех встроенных системных типов).

Например, используя Reflector в классе System.Collections.Generic.List<T>, вот реализация GetEnumerator:

public Enumerator<T> GetEnumerator()
{
    return new Enumerator<T>((List<T>) this);
}

При каждом использовании это выделяет новый Enumerator (и больше мусора).

Все ли это делают? Если да, то почему? (нельзя ли повторить использование одного Enumerator?)

Ответ 1

Foreach может вызывать распределения, но, по крайней мере, в более новых версиях .NET и Mono, это не так, если вы имеете дело с конкретными типами или массивами типа System.Collections.Generic. Старые версии этих компиляторов (например, версия Mono, используемая Unity3D до 5.5) всегда генерируют распределения.

Компилятор С# использует duck typing для поиска метода GetEnumerator() и использует это, если это возможно. Большинство методов GetEnumerator() в типах System.Collection.Generic имеют методы GetEnumerator(), которые возвращают structs, и массивы обрабатываются специально. Если ваш метод GetEnumerator() не выделяется, вы обычно можете избежать выделения.

Однако вы всегда будете получать выделение, если имеете дело с одним из интерфейсов IEnumerable, IEnumerable<T>, IList или IList<T>. Даже если ваш реализующий класс возвращает структуру, структура будет помещена в бокс и добавлена ​​к IEnumerator или IEnumerator<T>, которая требует выделения.


ПРИМЕЧАНИЕ. Поскольку Unity 5.5 обновлен до С# 6, я не знаю текущей версии компилятора, которая все еще имеет это второе выделение.

Есть второе выделение, которое немного сложнее понять. Возьмите этот цикл foreach:

List<int> valueList = new List<int>() { 1, 2 };
foreach (int value in valueList) {
    // do something with value
}

До С# 5.0 он расширяется до примерно такого (с некоторыми небольшими отличиями):

List<int>.Enumerator enumerator = valueList.GetEnumerator();
try {
    while (enumerator.MoveNext()) {
        int value = enumerator.Current;
        // do something with value
    }
}
finally {
    IDisposable disposable = enumerator as System.IDisposable;
    if (disposable != null) disposable.Dispose();
}

В то время как List<int>.Enumerator является структурой и не нужно выделять в куче, cast enumerator as System.IDisposable блокирует структуру, которая является распределением. Спецификация изменена с помощью С# 5.0, запрещающая выделение, но .NET нарушил спецификацию и оптимизировал выделение раньше.

Эти распределения крайне незначительны. Обратите внимание, что распределение сильно отличается от утечки памяти, и с сборкой мусора вам вообще не нужно беспокоиться об этом. Тем не менее, есть некоторые сценарии, когда вы заботитесь даже об этих распределениях. Я работаю Unity3D и до 5.5, мы не могли иметь никаких распределений в операциях, которые бывают в каждом игровом фрейме, потому что, когда сборщик мусора работает, вы получаете заметную неудачу.

Обратите внимание, что петли foreach на массивах обрабатываются специально и не нужно вызывать Dispose. Насколько я могу судить, foreach никогда не выделял при переходе по массивам.

Ответ 2

Нет, перечисление списка не вызывает сбор мусора.

Перечислитель для класса List<T> не выделяет память из кучи. Это структура, а не класс, поэтому конструктор не выделяет объект, он просто возвращает значение. Код foreach сохранил бы это значение в стеке, а не в куче.

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

Ответ 3

Поскольку перечислитель сохраняет текущий элемент. Он похож на курсор по сравнению с базами данных. Если несколько потоков будут обращаться к одному и тому же счетчику, вы потеряете контроль над последовательностью. И вы должны будете reset его к первому элементу каждый раз, когда foreach справляется с ним.

Ответ 4

Как уже упоминалось в комментариях, это, как правило, не должно быть проблемой, о которой вам нужно беспокоиться, поскольку в этом заключается суть коллекции мусора. Тем не менее, я понимаю, что да, каждый цикл foreach будет генерировать новый объект Enumerator, который в конечном итоге будет собран мусором. Чтобы понять, почему, посмотрите здесь документацию по интерфейсу здесь. Как вы можете видеть, есть вызов функции, который запрашивает следующий объект. Возможность сделать это означает, что счетчик имеет состояние и должен знать, какой из них следующий. Что касается того, почему это необходимо, изображение вы вызываете взаимодействие между каждой перестановкой элементов коллекции.

foreach(var outerItem in Items)
{
   foreach(var innterItem in Items)
   {
      // do something
   }
}

Здесь у вас есть два счетчика в одной коллекции одновременно. Очевидно, что совместное местоположение не достигнет вашей цели.