При написании кода на языке C# достаточно часто приходится использовать конструкцию foreach. Ведь так на много удобнее проходить по коллекциям, по сравнению с использованием цикла for. Безусловно, у каждого метода есть свои плюсы и минусы и выбор всегда зависит от конкретной задачи и ситуации. Но на днях меня удивила одни интересная особенность конструкции foreach, а  именно то, как это работает.

Рассмотрим простой пример:

У нас есть некая коллекция элементов и с помощью foreach мы выводим на консоль имена всех элементов.

При этом класс MyCollection выглядит так:

Класс коллекции реализовывает единственный метод GetEnumerator интерфейса IEnumerable для возможности использования класса в конструкции foreach.

Особенности enumerator’а нас сейчас не интересуют, будем считать что он работает так, как ему положено.

Теперь сделаем простую вещь, а именно, поменяем наш класс MyCollection так, чтобы он больше не реализовывал интерфейс IEnumerable, при этом метод GetEnumerator оставим без изменений.

К моему удивлению, код и дальше продолжил компиляцию и правильно работал. Первым делом, посмотрим какой IL-код у нас получился:

Тут хорошо видно, что при создании IL-кода компилятор не обращает внимания на наличие интерфейса IEnumerable, а непосредственно вызывает метод GetEnumerator. На этом моменте мне стало интересно и я пошел читать спецификацию языка C# (Standard ECMA-334 C# Language Specification), которая, к слову, датирована далёким июнем 2006-го года.  В разделе 8.18 Итераторы сказано следующее:

The foreach statement is used to iterate over the elements of an enumerable collection. In order to be
enumerable, a collection shall have a parameterless GetEnumerator method that returns an enumerator.
Generally, enumerators are difficult to implement, but the task is significantly simplified with iterators.

Таким образом, получается что для использования коллекции в конструкции foreach достаточно только метода GetEnumerator, а наличие интерфейса - необязательное условие.

Пример кода доступен по адресу: https://github.com/e0ne/BlogSamples/tree/master/ForeachTest


Приведение типов на практике встречается довольно часто. Из-за того, что в языке C# существует два оператора, позволяющих решить эту задачу, время от времени возникают InvalidCastException. Остановимся на этих операторах подробнее.

1. () оператор. На уровне IL-кода генерирует вызов функции castclass Выдержка из msdn: A cast explicitly invokes the conversion operator from one type to another; the cast fails if no such conversion operator is defined.
Оператор можно использовать только в том случае, если это предусмотрел разработчик.
Плюсы: простота в использовании, читабельность кода. Работает как с value, так и с reference типами.
Минусы: можно использовать не со всеми типами данных; источник InvalidCastException в случае неудачного вызова.


int? i1 = 2;
// вычисление значения i2
int i2 = (int)i1; // к этому моменту возможна ситуация, когда i1 будет равным null.


Еще один не менее распространённый пример:

public void MyMethod(object intObj)
{
if (intObj != null)
{
int intO = (int) intObj;
// ...
}
}

Такой код, как правило, используется когда порграммист уверен в том, что прийдёт нужный ему тип. Но, в случае использование сторонних библиотек или чужого кода вероятность такого поведения достаточно низкая.
2. Оператор as. На уровне IL-кода генерирует вызов функции isinst, в случае успешного выполнения которой происходит приведение типа. В противном случае – объекту присваивается null. Выдержка из msdn: The as operator is like a cast operation. However, if the conversion is not possible, as returns null instead of raising an exception.
Оператор работает со всеми reference-типами и выдаёт ошибку при компиляции в случае приминения к value-типу.
Плюсы: Работает с большинством типов данных. В случае неккоректного вызова не генерирет исключения. Не зависит от реализации класса.
Минусы: Не работает с vlue-типами. После вызова оператора желательна проверка на null:
string s = someObject as string;
if (s != null)
{
// someObject is a string.
}