Pull to refresh

Интересные моменты в C# (foreach)

Reading time 2 min
Views 78K
В этой статье мы коротко пройдемся по особенностям 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();
}

Всем спасибо за внимание!
Tags:
Hubs:
+48
Comments 36
Comments Comments 36

Articles