Пользователь
0,0
рейтинг
22 января 2014 в 10:05

Разработка → Интересные моменты в C# (foreach)

C#*
В этой статье мы коротко пройдемся по особенностям foreach. Первый момент вы скорее всего знаете, второй момент вы скорее всего не знаете.

Предыдущая статья об Array

Первый момент


На собеседованиях часто спрашивают — «Что необходимо сделать что бы ваш класс работал с foreach?». Ответ на этот вопрос обычно звучит так — «Реализовать IEnumerable». Ответ этот правильный, но не полный. В принципе, этого ответа на собеседовании достаточно и я ни разу не встречал чтобы кто то считал его неправильным. На самом деле, foreach использует «утиную типизацию». Для того чтобы наш класс работал в foreach достаточно иметь метод GetEnumerator возвращающий нечто имеющее метод MoveNext и свойство Current.

Запоминать эти методы не обязательно, если подсунуть свой неправильный класс в foreach компилятор честно подскажет чего конкретно не хватает в этом классе.

Примеры

Тестовый foreach:
class Program
{
	static void Main(string[] args)
	{
		var container = new Container();

		foreach (var item in container)
		{
		}
	}
}

Неправильный контейнер:
public class Container
{
}

Ошибка компилятора:
foreach statement cannot operate on variables of type 'Container' because 'Container' does not contain a public definition for 'GetEnumerator'

Добавим метод GetEnumerator в контейнер и класс энумератора.

Правильный контейнер:
public class Container
{
	public Enumerator GetEnumerator()
	{
		return new Enumerator();
	}
}

Неправильный энумератор:
public class Enumerator
{
}

Ошибка компилятора:
foreach requires that the return type 'Enumerator' of 'Container.GetEnumerator()' must have a suitable public MoveNext method and public Current property

Добавим метод MoveNext и свойство Current в энумератор.

Правильный энумератор:
public class Enumerator
{
	public bool MoveNext()
	{
		return false;
	}

	public object Current
	{
		get { return null; }
	}
}


Теперь компилятор все устраивает.

Примечание:
Свойство Current может возвращать любой тип, как ref type так и value type. Собственно это и стало причиной использования «утиной типизации», во времена когда не было generics, для избежания ненужных boxing и unboxing.

Второй момент


На собеседовании встречаются вопросы про IDisposable и помимо общих вопросов про ручное управление ресурсами есть вопрос про то, в каких случаях компилятор может автоматически вызывать метод Dispose. Ответ все мы знаем — Dispose вызывается автоматически при использовании оператора using(). Ответ этот правильный, но неполный! Метод Dispose может вызывается в двух случаях, помимо using(), он вызывается в foreach для энумератора, если энумератор реализует IDisposable.

Примеры

Энумератор с Dispose:
using System;

public class Enumerator : IDisposable
{
	public bool MoveNext()
	{
		return false;
	}

	public object Current
	{
		get { return null; }
	}

	public void Dispose()
	{
		Console.WriteLine("Dispose");
	}
}


Теперь при запуске примера мы увидим строчку «Dispose» в консоли.

Для тех кому интересно, вот код который генерит компилятор для нашего случая:
Container container = new Container();
Enumerator enumerator = container.GetEnumerator();
try
{
	while (enumerator.MoveNext())
	{
		var element = enumerator.Current;
		// содержимое foreach
	}
}
finally
{
	IDisposable disposable = enumerator as IDisposable;
	if (disposable != null)
		disposable.Dispose();
}

Всем спасибо за внимание!
Семёнов Альберт @mynameco
карма
21,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (36)

  • –1
    Интересные моменты. Особенно про утиную типизацию. Если на Dispose еще можно было как то случайно нарваться, то про Enumerator вряд ли. Спасибо за заметку.
    • +8
      Если читать книжки по c#, например, C# in a Nutshell, можно узнать действительно все особенности языка, включая эти особенности foreach :)
      • 0
        Не читал книгу данных авторов, не хуже Richter'а и Skeet'а?
        • 0
          Книжка отличная, я бы её не стал сравнивать с Рихтером, читать нужно обе: CLR via C# для изучения .NET, C# in a Nutshell — для изучения C#.
      • 0
        Спасибо, не слышал о данной книге. Надо будет взглянуть
    • +1
      await тоже работает на утиной типизации (ищет GetAwaiter).
    • 0
      Кажется, оба нюанса есть в книге Скита. По крайней мере второй точно.
  • +2
    Еще тонкость. Для старых компиляторов эквивалентный код был на самом деле такой:
    Container container = new Container();
    Enumerator enumerator = container.GetEnumerator();
    try
    {
        object element;
        while (enumerator.MoveNext())
        {
            element = enumerator.Current;
            // содержимое foreach
        }
    }
    finally
    {
        IDisposable disposable = enumerator as IDisposable;
        if (disposable != null)
            disposable.Dispose();
    }
    

    То есть отличалось место определения переменной цикла.

    Подробности: habrahabr.ru/post/141270/
    • +1
      Ога, та же тема с индексной переменной в цикле for. Это было исправлено для того, чтобы замыкания в цикле работали так, как «ожидает» большинство разработчиков: т.е. в каждой итерации захватывали актуальное значение элемента/индексной переменной.

      Пардон, в ссылку не заглянул сначала :).
  • –1
    Очень интересный момент про Dispose() в foreach.
    И неприятный, на мой взгляд.

    А как обнаружили?
    • 0
      Посмотрел в интерфейс
         public interface IEnumerator<out T> : IDisposable, IEnumerator
          {
              // Summary:
              //     Gets the element in the collection at the current position of the enumerator.
              //
              // Returns:
              //     The element in the collection at the current position of the enumerator.
              T Current { get; }
          }
      


      теперь понятно, откуда ноги.
    • 0
      Декомпилятором, очевидно же…

      Лично я сначала просто написал:
      public IEnumerable<string> Lines(string path) {
        string line;
        using (var file = File.OpenText(Path.Combine(basepath, path)))
          while ((line = file.ReadLine()) != null)
            yield return line;
      }
      
      А потом я задумался, в какой же момент этот файл будет закрыт…
      • 0
        удалено
    • –2
      Без Dispose невозможно было бы отследить изменение коллекции в процессе итеррирования по ней. Ибо нельзя узнать, есть ли читающие пользователи у коллекции или нет. Так же написание многопоточных контейнеров тоже упрощается, так как энумератор это частичка контейнера, которая отдается наружу, и нужно точно знать когда все началось и когда все закончилось.
      • 0
        Без Dispose невозможно было бы отследить изменение коллекции в процессе итеррирования по ней. Ибо нельзя узнать, есть ли читающие пользователи у коллекции или нет.


        Можете перефразировать или пояснить как-то?
        Как связан метод очистки ресурсов и событие изменения коллекции?
        • –2
          Можно реализовать следующим способом, отдал энумератор +1, отдал второй еще +1, вызывался диспоз -1 и т.д.
          А в методе Add или Remove можно проверять равно ли число 0, если нет, то мы портим коллекцию для читающих пользователей.
          Это один из вариантов, можно и другими способами.
          • +6
            На самом деле, это реализовано немного по-другому: все операции изменения коллекции инкрементируют её версию. При создании итератора, в нём фиксируется текущая версия коллекции, а далее при продвижении вперед каждый раз проверяется, не изменилась ли версия. Если изменилась, значит коллекция была модифицирована, и, следовательно, надо кинуть исключение.
      • +2
        Допустим, отследить изменение коллекции можно запросто — достаточно счетчика модификаций в коллекции — и его копии в перечислителе. А вот если требуется хотя бы блокировка — тут да, без Dispose никак.
        • +1
          Собственно, именно так оно и реализуется в стандартных коллекциях — поле version в самой коллекции, которое инкрементируется при каждом изменении, и сохраняется в энумераторе в момент его создания — а потом сравнивается в Current/MoveNext. Коллекция о своих энумераторах не знает.

          Dispose же нужен для коллекций и прочих перечислений, у которых энумераторы реально владеют какими-то требующими явного высвобождения ресурсами — например, курсором БД, или файловым хэндлом. А также для вызова блоков finally в методах-итераторах.
        • 0
          Извиняюсь, слово «невозможно» было очень сильным.
      • +1
        В стандартном List<T> например хранится int version, который инкрементится на каждое действие, изменяющее структуру листа. При создании итератора (а он является внутренним классом листа) ему в приватное поле копируется эта версия, а на MoveNext проверяется, есть ли разница между исходным version и текущим и если есть, то вызывается исключение. Реализация Dispose пустая.
    • 0
      А в чем неприятность Dispose в foreach? foreach явно создает новый экземпляр энумератора в начале работы, он же его и подчищает в конце, если необходимо. Если бы он этого не делал, как бы вы реализовывали подчистку после foreach? Ведь сам энумератор не доступен, это внутренняя переменная, видимая только компилятору.
    • +1
      IEnumerator стал Disposable после добавления yield return. Например:
       using (var disposable = new SomeDisposable())
       {
        yield return 1;
        yield return 2;
      
      // some code
      
       }
      }
      


      Когда мы закончим итерироваться по этому генертору (причем, мы же можем закончить даже не доходя до конца), нужно закрыть someDisposable. Поэтому компилятор вычисляет такие структуры и кладет их Dispose в Dispose() метод сгенеренного IEnumerator.
  • +4
    У Сергея Теплякова есть хороший пост на эту тему habrahabr.ru/post/148905/.
  • 0
    Что за тэги «факты, особенности, тонкости»? Вы понимаете значение словосочетания «ключевые слова»? Как по вашим тэгам можно будет что-то найти в этом кривом поисковике?
    • 0
      Дико извиняюсь, поправил, если что то еще нужно добавить, добавлю.
      • +1
        Я тоже извиняюсь, что-то я совсем злой в последнее время, просто накипело, очень часто встречаю топики с бессмысленными тэгами, что приходится постоянно добавлять самому (благо, хоть такая возможность есть) =(
        Ну, так уже получше :)
    • +3
      Что за тэги «факты, особенности, тонкости»?

      Нужны тэги: «скандалы, интриги, расследования».
  • 0
    Dispose также вызывается в деструкторе, если какой-то умник забудет вызвать его «вручную».
    • –1
      Только, если другой «умник» написал такой деструктор.
      • 0
        Для всех стандартных классов, реализующих IDisposable, так лучше?
        • 0
          Я, если честно, не знаю — лучше ли. В смысле — на практике.
          Моё мнение, что если человек не в курсе, что IDisposable надо оборачивать в using, то, чем раньше оно упадет — тем лучше. Потому что есть шанс на фэйл до выхода в продакшн, и у автора будет возможность исправить косяк.

          А когда в финализаторе происходит высвобождение ресурсов — это ведет к труднообъяснимым и невоспроизводимым глюкам.
          Т.е. например, выйдете ли вы за пределы пула соединений с БД зависит уже от GC, поведение которого меняется от версии .NET и от настроек в app.config.
          • 0
            Я не спорю, что человека, который не закрывает ресурсы юзингом, нужно гнать в шею. Но упомянуть про обязательное освобождение в деструкторе, как последнем рубеже обороны, в статье про IDisposable считаю необходимым.
            • 0
              Во-первых, это статья — совсем не про IDIsposable. А во-вторых, не каждая реализация IDisposable нуждается в финализаторе. Например, чаще всего бывает, что вызов Dispose всего лишь делегируется другому объекту. В таком случае вызывать Dispose в финализаторе нет необходимости — все ресурсы соберет другой объект самостоятельно.
            • 0
              В момент вызова деструктора состояние объектов класса не определен, так что вызывать диспоз в деструкторе не всегда возможно. Так же порядок вызов деструкторов не определен. Я могу вызвать диспоз для себя но для дочерних он может быть уже вызван. Собственно поэтому связка деструктор + диспоз это целый паттерн(ы).
              Но как сказали выше, статья не об этом =)
              • 0
                Поэтому они перед началом работы проверяют флаг IsDisposed, все верно.

                И да, речь действительно не об этом, поэтому я и на предыдущий комментарий не ответил. Засим завершим беседу.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.