Как правильно писать тесты 46 Цикл разработки 46 Структура проекта с тестами 51 Утверждения (Asserts) 52 Утверждения в форме ограничений 54 Категории 56

Вид материалаТесты

Содержание


Циклы foreach
Подобный материал:
1   ...   16   17   18   19   20   21   22   23   ...   47

Циклы foreach




Цикл foreach связан с идеей перечислителей (Enumerators). Перечислители являются механизмом доступа к последовательности объектов из коллекции без раскрытия структуры данных коллекции. При этом перечислители предлагают универсальный способ перемещения между элементами любых коллекций.

Разные коллекции могут иметь разные стратегии перемещения между элементами, но пользователю не нужно об этом задумываться – он пользуется одним интерфейсом.

Другими словами, в коллекциях обязанности по хранению данных отделены от обязанностей по перемещению между элементами данных. Реализация стратегии перемещения может быть стандартной или предоставленной пользователем коллекции.


Таким образом, оператор foreach языка С# — это больше, нежели еще одна вариация операторов цикла: do, while и for. Для любой имеющейся коллекции он генерирует самый лучший программный код перемещения между элементами (итерации). Его определение связано с интерфейсами коллекций в .NET Framework, и компилятор С# генерирует наилучший код для конкретного типа коллекции. Если нужно выполнить итерацию для коллекции, используйте оператор foreach вместо других циклических конструкций. Исследуем три цикла:

int [] foo = new int[100];


// Loop 1:

foreach ( int i in foo)

Console.WriteLine( i.ToString( ));


// Loop 2:

for ( int index = 0;

index < foo.Length;

index++ )

Console.WriteLine( foo[index].ToString( ));


// Loop 3:

int len = foo.Length;

for ( int index = 0;

index < len;

index++ )

Console.WriteLine( foo[index].ToString( ));

Цикл №1 является самым лучшим. Даже для его записи требуется меньше усилий. Цикл №3, конструкция которого покажется наиболее эффективной большинству программистов на С и С++, оказывается худшим из вариантов. При вынесении nepeменной Length из цикла в программу вносится изменение, не позволяющее JIT-компилятору убрать из цикла проверку принадлежности диапазону.


Программа на С# выполняется в безопасной, управляемой среде. Каждый адрес памяти проверяется, включая и индексы массивов. Фактический код для цикла 3 будет выглядеть примерно следующим образом:

// Loop 3, as generated by compiler:

int len = foo.Length;

for ( int index = 0;

index < len;

index++ )

{

if ( index < foo.Length )

Console.WriteLine( foo[index].ToString( ));

else

throw new IndexOutOfRangeException( );

}

JIT-компилятору в C# не понравится, если вы попробуете помочь ему подобным образом. Ваша попытка вынести доступ к свойству Length из цикла вынуждает компилятор JIT проделывать больший объем работы, в результате чего будет сгенерирован более медленный программный код. CLR гарантирует, что вы не сможете написать программу, которая выйдет за границы памяти, отведенной для хранения используемых переменных. Во время выполнения программы перед обращением к каждому элементу массива производится проверка фактических границ массива (а не переменной len). Получается одна проверка границ по цене двух.


Циклы 1 и 2 выполняются быстрее потому, что компилятор С# и JIT-компилятор могут один раз выполнить проверку того, что границы цикла являются безопасными.


Кроме того, при использовании foreach не нужно помнить, с какого индекса начинаются допустимые значения для индексирования массивов и коллекций в C#.


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


Оператор foreach работает с многомерными массивами. Предположим, что вы создаете шахматную доску и для этого пишете следующие два фрагмента:

private Square[,] _theBoard = new Square[ 8, 8 ];


// elsewhere in code:

for ( int i = 0; i < _theBoard.GetLength( 0 ); i++ )

for( int j = 0; j < _theBoard.GetLength( 1 ); j++ )

_theBoard[ i, j ].PaintSquare( );


Однако можно упростить раскраску доски:

foreach( Square sq in _theBoard )

sq.PaintSquare( );


Этот же способ будет работать, даже если у доски станет три измерения. Старый способ с циклом for потребовал бы изменений:

for ( int i = 0; i < _theBoard.GetLength( 0 ); i++ )

for( int j = 0; j < _theBoard.GetLength( 1 ); j++ )

for( int k = 0; k < _theBoard.GetLength( 2 ); k++ )

_theBoard[ i, j, k ].PaintSquare( );

Кроме того, foreach позволит сохранить большую часть программы без изменений, если когда-либо потребуется изменить структуры данных, лежащих в основе массива. Для начала рассмотрим простой массив:

int [] foo = new int[100];

Предположим, что возникают требования, которые не так-то просто реализовать с помощью обычных массивов (array). В таком случае можно заменить массив на ArrayList:

// Set the initial size:

ArrayList foo = new ArrayList( 100 );


При этом будет нарушена работа всех циклов for:

int sum = 0;

for ( int index = 0;

// won't compile: ArrayList uses Count, not Length

index < foo.Length;

index++ )

// won't compile: foo[ index ] is object, not int.

sum += foo[ index ];


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


С точки зрения оператора foreach, чтобы класс считался коллекцией, у него должно иметься одно из следующих свойств. Наличие общедоступного метода GetEnumerator() придает классу свойства коллекции. Явная реализация интерфейса IEnumerable придает классу свойства коллекции. foreach будет работать в каждом из этих случаев.


У foreach имеется одно дополнительное преимущество, относящееся к управлению ресурсами. Интерфейс IEnumerable содержит метод GetEnumerator(). Оператор foreach генерирует для перечислимого типа следующий код (после некоторой оптимизации):

IEnumerator it = foo.GetEnumerator( ) as IEnumerator;

using ( IDisposable disp = it as IDisposable )

{

while ( it.MoveNext( ))

{

int elem = ( int ) it.Current;

sum += elem;

}

}

Компилятор автоматически оптимизирует код для блока finally, если он может определить, реализует ли переменная цикла интерфейс IDisposable.


fоreach — многогранный оператор. Он генерирует правильный код перемещения между элементами коллекция и массивов с учетом их верхних и нижних границ, итерирует многомерные массивы, принудительно приводит операнды к требуемым типам (используя для этого наиболее эффективные конструкции) и, наконец, генерирует наиболее эффективные конструкции для циклов. Это наилучший из способов итерирования коллекций.