Массивы могут передаваться в методы в качестве параметров, а также возвращаться из методов. Для возврата массива достаточно объявить массив как тип возврата, как показано в следующем методе 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, происходит обращение к перечислителю. Это обеспечивает возможность итерации по огромным объемам данных без необходимости читать их целиком в память в один прием.