Массивы в качестве параметров

Массивы могут передаваться в методы в качестве параметров, а также возвращаться из методов. Для возврата массива достаточно объявить массив как тип возврата, как показано в следующем методе GetPersons():

static Person[] GetPersons()

{

return new Person [ ] {

new Person { FirstName="Damon", LastName="Hill" },

new Person { FirstName="Niki", LastName="Lauda" },

new Person { FirstName="Ayrton", LastName="Senna" },

new Person { FirstName="Graham", LastName="Hill" }

};

}

Для передачи массивов в метод массив объявляется в параметре, как показано в следую­щем методе DisplayPersons():

static void DisplayPersons(Person[] persons)

{

//...

Ковариантость массивов

Для массивов поддерживается ковариантность. Это значит, что массив может быть объ­явлен как базовый тип, и его элементам могут присваиваться элементы производных ти­пов. Например, можно объявить параметр типа object[] и передать в нем Person[]:

static void DisplayArray(object[] data) .

{

//...

}

Ковариантность массивов возможна только, для ссылочных типов, но не для ти­пов значений.

 

С ковариантностью массивов связана проблема, которая может быть решена толь­ко через исключения времени выполнения. Если присвоить массив Person массиву object, mo массив object затем может быть использован со всем, что наследует­ся от object. Например, компилятор разрешит передавать строки в элементах такого массива. Однако, поскольку на самом деле ссылка на массив Person производится через массив object, возникает исключение времени выполнения.

Структура ArraySegment<T>

Структура ArraySegment<T>представляет сегмент массива. Это структура может при­меняться, когда нужно вернуть или передать методу части массива. Вместо передачи в ме­тод массива, смещения и счетчика в отдельных параметрах, можно передать единствен­ный параметр ArraySegment<T>.В этой структуре информация о сегменте (смещение и счетчик) заключена непосредственно в ее членах.

Метод SumOfSegmentsпринимает массив элементов ArraySegment<int>для вычисле­ния суммы всех целых чисел, определенных в сегменте, и возвращает эту сумму:

static int SumOfSegments(ArraySegment<int>[] segments)

{

int sum =0;

foreach (var segment in segments)

{

for (int i=segment.Offset; i<segment.Offset+segment.Count; i++)

{

sum += segment.Array[i];

}

}

return sum;

}

Этот метод используется посредством передачи массива сегментов. Первый элемент массива ссылается на три элемента ar1,начиная с первого, а второй элемент - на три элемента аr2, начиная с четвертого:

int[] ar1 = { 1, 4, 5, 11, 13, 18 };

int[] ar2 = { 3, 4, 5, 18, 21, 27, 33 };

var segments = new ArraySegment<int>[2]

{

new ArraySegment<int>(ar1, 0, 3),

new ArraySegment<int>(ar2, 3, 3)

} ;

var sum = SumOfSegments(segments);

 

Важно отметить, что сегменты массива не копируют элементы исходного мас­сива. Вместо этого через ArraySegment<T> можно получить доступ к исходно­му массиву Если изменяются элементы сегмента, то эти изменения будут видны в исходном массиве.

Перечисления

С помощью оператора foreachможно выполнять итерацию по элементам коллекции (см. раздел 6.10) без необходимости знания количества ее элементов. Оператор foreach использует перечислитель (enumerator). На рис. 6.7 показано отношение между клиен­том, вызывающим foreach,и коллекцией. Массив или коллекция реализует интерфейс IEnumerableс методом GetEnumerator(). Метод GetEnumerator() возвращает перечис­литель, реализующий интерфейс IEnumerable.Интерфейс IEnumerableзатем применя­ется оператором foreachдля выполнения итерации по элементам коллекции.

 

 

Метод GetEnumerator() определен в интерфейсе IEnumerable. Оператор foreach в действительности не нуждается в там, чтобы класс коллекции реали­зовывал этот интерфейс. Достаточно иметь метод по имени GetEnumerator(), который возвращает объект, реализующий интерфейс IEnumerator.

 


Рисунок 6.7 - Отношение между клиентом, вызывающим foreach, и коллекцией

Интерфейс IEnumerator

Оператор foreach использует методы и свойства интерфейса IEnumerator для итера­ции по всем элементам коллекции. Для этого IEnumerator определяет свойство Current для возврата элемента, на котором позиционирован курсор, и метод MoveNext() возвра­щает true, если есть элемент, и false, если доступных элементов больше нет.

Обобщенная версия этого интерфейса IEnumerator<T> - унаследована от интер­фейса IDisposable, и потому определяет метод Dispose() для очистки ресурсов, выде­ленных для перечислителя.

Интерфейс lEnumerator также определяет метод Reset() для возможно­сти взаимодействия с СОМ. Реализация этого метода во многих перечислите­лях .NETсводится к генерации исключения типа NotSupportedException.

Оператор foreach

Оператор foreach в C# не преобразуется к оператору foreach в IL. Вместо этого ком­пилятор C# преобразует оператор foreach в методы и свойства интерфейса IEnumerable. Ниже приведен простой пример оператора foreach для итерации по всем элементам мас­сива персон и отображения их друг за другом:

foreach (var р in persons)

{

Console.WriteLine(р);

}

Оператор foreach преобразуется в следующий сегмент кода. Сначала вызывается ме­тод GetEnumerator() для получения перечислителя для массива. Внутри цикла while - до тех пор, пока MoveNext() возвращает true - элементы массива доступны через свойство Current:

IEnumerator<Person> enumerator = persons.GetEnumerator();

while (enumerator.MoveNext())

{

Person p = (Person)enumerator.Current;

Console.WriteLine(p);

}

Оператор yield

Со времени первого выпуска C# позволяет легко выполнять итерации по коллекциям с помощью оператора foreach. В C# 1.0 для получения перечислителя приходилось выпол­нять немалую работу. В C# 2.0 добавлен оператор yield для легкого создания перечислите­лей. Оператор yield return возвращает один элемент коллекции и перемещает текущую позицию на следующий элемент, а оператор yield break прекращает итерацию.

В следующем примере показана реализация простой коллекции с применением опера­тора yield return.Класс HelloCollectionимеет метод GetEnumerator(). Реализация метода GetEnumerator () содержит два оператора yield return,где возвращаются стро­ки "Hello" и "World".

using System;

using System.Collections;

 

namespace Wrox.ProCSharp.Arrays

{

public class HelloCollection

{

public XEnumerator<string> GetEnumerator()

{

yield return "Hello";

yield return "World";

}

}

Метод или свойство, содержащее операторы yield, также известно как блок итератора. Блок итератора должен быть объявлен для возврата интерфейса IEnumerator или IEnumerable либо их обобщенных версий. Этот блок мо­жет содержать множество операторов yield return или yield break; оператор return не разрешен.

Теперь возможно провести итерацию по коллекции с использованием оператора foreach:

public void HelloWorld()

{

var helloCollection = new HelloCollection();

foreach (string s in helloCollection)

{

Console.WriteLine(s);

}

}

}

С блоком итератора компилятор генерирует тип yield, включая конечный автомат, как показано в следующем фрагменте кода. Тип yield реализует свойства и методы интер­фейсов IEnumerator и IDisposable. В примере можно видеть тип yield как внутренний класс Enumerator. Метод GetEnumerator() внешнего класса создает экземпляр и возвра­щает тип yield. Внутри типа yield переменная state определяет текущую позицию ите­рации и изменяется каждый раз, когда вызывается метод MoveNext(). Метод MoveNext() инкапсулирует код блока итератора и устанавливает значение текущей переменной таким образом, что свойство Current возвращает объект, зависящий от позиции.

 

public class HelloCollection

{

public IEnumerator GetEnumerator()

{

return new Enumerator (0);

}

public class Enumerator: IEnumerator<string>,IEnumerator,IDisposable

{

private int state;

private object current;

public Enumerator(int state)

{

this.state = state;

}

bool System.Collections.IEnumerator.MoveNext()

{

switch (state)

{

case 0:

current = "Hello";

state = 1;

return true;

case 1:

current = "World";

state = 2;

return true;

case 2:

break;

}

return false;

}

string System.Collections.Generic.IEnumerator<string>.Current

{

get

{

return current;

}

}

object System.Collections.IEnumerator.Current

{

get

{

return current;

}

}

void IDisposable.Dispose()

{

}

}

}

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