Visual Basic, .NET, ASP, VBScript
 

   
 
Описание для автора не найдено
 
     
   
 
Спецификация С# версии 2.0: Введение в C# 2.0
Перевод: Надежда Шатохина(sna@uneta.org), Ukraine .Net Alliance (http://www.uneta.org/)
Июль 2003
Применяется к:
  • Microsoft C#
  • Обзор:
    C# 2.0 представляет несколько расширений языка, самые важные из которых — Шаблоны (Generics), Анонимные методы (Anonymous Methods), Итераторы (Iterators) и Неполные типы (Partial Types).
    Содерджание:
  • Введение
  • Шаблоны
  • Почему шаблоны?
  • Создание и использование шаблонов
  • Создание экземпляров шаблонного типа
  • Ограничения
  • Шаблонные методы
  • Анонимные методы
  • Преобразования группы методов
  • Итераторы
  • Неполные типы
  • Введение

    C# 2.0 представляет несколько расширений языка, самые важные из которых — Шаблоны (Generics), Анонимные методы (Anonymous Methods), Итераторы (Iterators) и Неполные типы (Partial Types).

    • Шаблоны позволяют параметризовать классы, структуры, интерфейсы, делегаты и методы типами данных, которые они сохраняют и которыми манипулируют. Удобство шаблонов в том, что они обеспечивают более строгую проверку типов во время компилирования, требуют менее явных преобразований между типами данных и сокращают необходимость в операциях упаковки/распаковки и динамической проверки типов.
    • Анонимные методы обеспечивают возможность вставки блоков кода там, где применяются значения делегатов. Анонимные методы подобны лямбда-функциям в языке программирования Lisp. C# 2.0 поддерживает создание «замкнутых областей», в которых анонимные методы работают с окружающим локальным переменным и параметрам.
    • Итераторы — это методы, которые инкрементно вычисляют и формируют последовательности значений. Итераторы облегчают работу типам по определению того, как оператор foreach должен производить итерацию их элементов.
    • Неполные типы позволяют разбивать классы, структуры и интерфейсы на множество кусочков, хранящихся в разных исходных файлах, для облегчения разработки и сопровождения. Кроме того, неполные типы делают возможным разделение сгенерированных машиной и написанных пользователем частей типов, что облегчает расширение кода, генерируемого инструментом.

    Эта глава знакомит вас с этими новыми возможностями. За ней следуют четыре раздела, в которых приведено их полное техническое описание.

    Расширения языка в C# 2.0 были разработаны, чтобы обеспечить максимальную совместимость с существующим кодом. Например, даже несмотря на то, что C# 2.0 присваивает особые значения словам where, yield и partial в определенных контекстах, они все еще могут использоваться и как идентификаторы. На самом деле, C# 2.0 не вводит никаких новых ключевых слов, поскольку они могут конфликтовать с идентификаторами в существующем коде.

    Шаблоны

    Шаблоны позволяют параметризовать классы, структуры, интерфейсы, делегаты и методы типами данных, которые они сохраняют и которыми манипулируют. Шаблоны C# будут привычными для людей, использующих шаблоны Eiffel, Ada или шаблоны C++, хотя им и не присуща сложность последних.

    Почему шаблоны?

    Без шаблонов структуры данных общего назначения могут использовать тип object для сохранения данных любого типа. Например, приведенный далее простой класс Stack сохраняет свои данные в массиве object, и два его метода, Push и Pop, используют object для приема и возвращения данных, соответственно:

    public class Stack{	
    	object[] items;	
    	int count;	
    	public void Push(object item) {...}	
    	public object Pop() {...}
    }
    

    В то время как использование типа object делает класс Stack очень гибким, не обошлось и без недостатков. Например, есть возможность помещать в стек значения любого типа, как экземпляр Customer. Однако после получения значения результат метода Pop должен быть опять явно приведен к соответствующему типу, а это довольно утомительно для реализации и отражается на производительности при динамической проверке типов:

    Stack stack = new Stack();
    stack.Push(new Customer());
    Customer c = (Customer)stack.Pop();
    

    Если значение типа-значения, например, int, передается в метод Push, оно автоматически упаковывается. Когда позже int извлекается, он должен быть распакован с явным приведением типа:

    Stack stack = new Stack();
    stack.Push(3);
    int i = (int)stack.Pop();
    

    Такие операции упаковки/распаковки увеличивают непроизводительные издержки, поскольку приводят к динамическим перераспределениям памяти и динамическим проверкам типов.

    Следующая проблема класса Stack —невозможность задания типа данных, помещаемых в стек. Действительно, экземпляр Customer может быть помещен в стек и затем после восстановления в памяти случайно преобразован в другой тип:

    Stack stack = new Stack();
    stack.Push(new Customer());
    string s = (string)stack.Pop();
    

    Хотя приведенный выше код и демонстрирует неправильное использование класса Stack, технически он вполне верен и ошибок при его компилировании не возникнет. Проблема не будет выявлена до тех пор, пока код не будет приведен в исполнение и не возникнет InvalidCastException.

    Класс Stack несомненно выиграет от возможности задавать тип своего элемента. С шаблонами это становится возможным.

    Создание и использование шаблонов

    Шаблоны предоставляют возможность создания типов, обладающих параметрами типов (type parameters). Приведенный ниже пример объявляет шаблонный класс Stack с параметром типа T. Параметр типа задается в угловых скобках < и > после имени класса. Вместо преобразований в и из object, экземпляры Stack<T> принимают тип, для которого они созданы, и сохраняют данные этого типа без преобразований. Параметр типа T действует как структурный нуль до тех пор, пока не будет указан реальный тип. Обратите внимание, что T используется как тип элемента для внутреннего массива элементов, тип параметра метода Push и тип результата для метода Pop:

    public class Stack<T>
    {
    	T[] items;
    	int count;
    	public void Push(T item) {...}
    	public T Pop() {...}
    }
    

    Когда используется шаблонный класс Stack<T>, вместо T подставляется реальный тип. В следующем примере int задается как аргумент типа (type argument) для T:

    Stack<int> stack = new Stack<int>();
    stack.Push(3);
    int x = stack.Pop();
    

    Тип Stack<int> называют составным типом (constructed type). В типе Stack<int> каждое T заменяется аргументом типа int. Когда экземпляр Stack<int> создан, элементами массива items являются int[], а не object[], обеспечивая достаточную эффективность хранения по сравнению с нешаблонным Stack. Методы Push и Pop класса Stack<int> также оперируют значениями int, при этом помещение в стек значений другого типа приводит к ошибкам компиляции, также отсутствует необходимость в явном приведении извлеченных значений к их исходному типу.

    Шаблоны обеспечивают строгий контроль типов, это означает, например, что помещение int в стек объектов Customer является ошибкой. Точно так же, как Stack<int> ограничен возможностью работать только со значениями типа int, так и Stack<Customer> ограничен объектами Customer, и компилятор выдаст ошибку в последних двух строках следующего примера:

    Stack<Customer> stack = new Stack<Customer>();
    stack.Push(new Customer());
    Customer c = stack.Pop();
    stack.Push(3);						// Ошибка несоответствия типов 
    int x = stack.Pop();				// Ошибка несоответствия типов
    

    Описания шаблонного типа могут иметь любое количество параметров типа. В приведенном выше примере в Stack<T> есть только один параметр типа, но шаблонный класс Dictionary должен иметь два параметра типа, один для типа ключей и другой для типа значений:

    public class Dictionary<K,V>
    {
    	public void Add(K key, V value) {...}
    	public V this[K key] {...}
    }
    

    Когда используется Dictionary<K,V>, должны быть заданы два аргумента типа:

    Dictionary<string,Customer> dict = new Dictionary<string,Customer>();
    dict.Add("Peter", new Customer());
    Customer c = dict["Peter"];
    
    Создание экземпляров шаблонного типа

    Подобно нешаблонным типам, компилированным представлением шаблонного типа являются инструкции промежуточного языка (IL) и метаданные. Конечно же, в представлении шаблонного типа также кодирует существование и использование параметров типа.

    Когда приложение впервые создает экземпляр составного шаблонного типа, такого как Stack<int>, just-in-time (JIT) компилятор Общеязыковой среды выполнения (Common Language Runtime) .NET превращает шаблонный IL и метаданные в машинный код, подставляя в процессе вместо параметров типов реальные типы. Тогда последующие ссылки на этот составной шаблонный тип используют тот же машинный код. Процесс создания конкретного составного типа из шаблонного типа известен как создание экземпляров шаблонного типа.

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

    Ограничения

    Обычно шаблонный класс будет делать больше, чем просто хранить данные, основанные на параметре типа. Часто шаблонный класс захочет вызывать методы объектов, чей тип задан как параметр типа. Например, методу Add в классе Dictionary<K,V> может понадобится сравнить ключи с помощью метода CompareTo:

    public class Dictionary<K,V>
    {
    	public void Add(K key, V value)
    	{
    		...
    		if (key.CompareTo(x) < 0) {...}		// Error, нет метода CompareTo
    		...
    	}
    }
    

    Поскольку тип аргумента типа, определенного для K, может быть любым, параметром key могут быть только члены, объявленные типом object, такие как Equals, GetHashCode и ToString; следовательно, в приведенном выше примере возникает ошибка компиляции. Конечно же, можно привести параметр key к типу, в котором есть метод CompareTo. Например, параметр key может быть приведен к IComparable:

    public class Dictionary<K,V>
    {
    	public void Add(K key, V value)
    	{
    		...
    		if (((IComparable)key).CompareTo(x) < 0) {...}
    		...
    	}
    }
    

    Хотя это решение и работает, но оно требует динамической проверки типов, что увеличивает непроизводительные издержки. Более того, в нем сообщение об ошибке откладывается на время выполнения, и исключение InvalidCastException возникает, если key не реализовывает IComparable.

    Чтобы обеспечить более строгую проверку типов при компиляции и сократить количество приведений типов, C# допускает, чтобы для каждого параметра типа был предоставлен необязательный список ограничений. Ограничение параметра типа определяет требование, которому должен удовлетворять тип, чтобы использоваться в качестве аргумента для этого параметра типа. Ограничения объявляются с помощью слова where, за которым следует имя параметра типа и список типов класса или интерфейса или конструктор-ограничение new().

    Чтобы в классе Dictionary<K,V> гарантировать, что key всегда реализовывает IComparable, описание класса может задать ограничение для параметра типа K:

    public class Dictionary<K,V> where K: IComparable
    {
    	public void Add(K key, V value)
    	{
    		...
    		if (key.CompareTo(x) < 0) {...}
    		...
    	}
    }			
    

    Исходя из этого описания, компилятор будет гарантировать, что любой аргумент типа, поставляемый для K, будет иметь тип, который реализовывает IComparable. Более того, больше не обязательно явно приводить параметр key к IComparable перед вызовом метода CompareTo; все члены типа, заданные как ограничения для параметра типа, непосредственно доступны в значениях этого типа параметра типа.

    Для данного параметра типа в качестве ограничений можно задать любое количество интерфейсов, но не более одного класса. У каждого подчиненного ограничениям параметра типа есть отдельный оператор where. В приведенном ниже примере у параметра типа K есть ограничения-интерфейсы, а у параметра типа E — ограничение-класс и ограничение-конструктор:

    public class EntityTable<K,E>
    	where K: IComparable<K>, IPersistable
    	where E: Entity, new()
    {
    	public void Add(K key, E entity)
    	{
    		...
    		if (key.CompareTo(x) < 0) {...}
    		...
    	}
    }			
    

    Ограничение-конструктор new() в приведенном выше примере гарантирует, что у типа, используемого в качестве аргумента типа для E, есть открытый конструктор без параметров, и он позволяет шаблонному классу использовать new E() для создания экземпляров этого типа.

    Ограничениями параметра типа надо пользоваться аккуратно. Они обеспечивают более строгий контроль типов при компилировании и в некоторых случаях повышают производительность, но, в то же время, ограничивают возможные применения шаблонного типа. Например, шаблонный класс List<T> может заставить T реализовать IComparable таким образом, что метод Sort списка сможет сравнивать элементы. Однако это воспрепятствует использованию List<T> для типов, которые не реализовывают IComparable, даже если в них метод Sort никогда и не вызывается.

    Шаблонные методы

    В некоторых случаях параметр типа нужен не для всего класса, а только внутри отдельного метода. Часто это происходит при создании метода, который принимает шаблонный тип в качестве параметра. Например, при использовании описанного ранее класса Stack<T> общим принципом может быть помещение множества значений в одну строку, и, возможно, удобнее будет написать метод, который делает это за один вызов. Для конкретного составного типа, такого как Stack<int>, метод может выглядеть следующим образом:

    void PushMultiple(Stack<int> stack, params int[] values) {
    	foreach (int value in values) stack.Push(value);
    }
    

    Этот метод может использоваться для помещения множества значений int в Stack<int>:

    Stack<int> stack = new Stack<int>();
    PushMultiple(stack, 1, 2, 3, 4);
    

    Однако приведенный выше метод работает только с конкретным составным типом Stack<int>. Чтобы заставить его работать с любым Stack<T>, он должен быть написан как шаблонный метод (generic method). У шаблонного метода есть один или более параметров типа, указанных в угловых скобках < и > после имени метода. Параметры типа могут использоваться в списке параметров, возвращаемом типе и теле метода. Шаблонный метод PushMultiple может выглядеть следующим образом:

    void PushMultiple<T>(Stack<T> stack, params T[] values) {
    	foreach (T value in values) stack.Push(value);
    }
    

    Используя этот шаблонный метод, можно поместить множество элементов в любой Stack<T>. При вызове шаблонного метода аргументы задаются в угловых скобках в вызове метода. Например:

    Stack<int> stack = new Stack<int>();
    PushMultiple<int>(stack, 1, 2, 3, 4);
    

    Этот шаблонный метод PushMultiple более пригоден для многократного использования, чем предыдущая версия, поскольку он работает с любым Stack<T>, но оказывается, что вызывать его не так удобно, т.к. необходимый T должен поставляться в метод как аргумент типа. Во многих случаях, однако, компилятор с помощью логического вывода типа может получить соответствующий аргумент типа из аргументов, переданных в метод. В приведенном выше примере, поскольку тип первого обычного аргумента Stack<int> и тип последующих аргументов — int, компилятор может сделать вывод, что параметр типа должен иметь тип int. Таким образом, шаблонный метод PushMultiple может вызываться без указания параметра типа:

    Stack<int> stack = new Stack<int>();
    PushMultiple(stack, 1, 2, 3, 4);
    
    Анонимные методы

    Обработчики событий и другие функции обратного вызова всегда вызываются исключительно через делегатов и никогда прямо. Даже в этом случае до сих пор было необходимо помещать код обработчиков событий и функций обратного вызова в отдельные методы, для которых явно создавались делегаты. В отличие от этого, анонимные методы позволяют написание кода, ассоциированного с делегатом, прямо «в строке», в которой используется делегат, удобно связывая код непосредственно с экземпляром делегата. Кроме этого удобства, анонимные методы имеют совместный доступ к локальному состоянию члена содержащей функции. Чтобы достигнуть такого же совместного использования состояния с помощью именованных методов, необходимо превратить локальные переменные в поля в экземплярах созданных вручную вспомогательных классов.

    В следующем примере показана простая форма ввода, содержащая окно списка, текстовое окно и кнопку. При нажатии кнопки элемент, содержащий текст в текстовом окне, добавляется в окно списка.

    class InputForm: Form
    {
    	ListBox listBox;
    	TextBox textBox;
    	Button addButton;
    	public MyForm() {
    		listBox = new ListBox(...);
    		textBox = new TextBox(...);
    		addButton = new Button(...);
    		addButton.Click += new EventHandler(AddClick);
    	}
    	void AddClick(object sender, EventArgs e) {
    		listBox.Items.Add(textBox.Text);
    	}
    }
    

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

    class InputForm: Form
    {
    	ListBox listBox;
    	TextBox textBox;
    	Button addButton;
    	public MyForm() {
    		listBox = new ListBox(...);
    		textBox = new TextBox(...);
    		addButton = new Button(...);
    		addButton.Click += delegate {
    			listBox.Items.Add(textBox.Text);
    		};
    	}
    }
    

    Анонимный метод состоит из ключевого слова delegate, необязательного списка параметров и списка операторов, заключенного в фигурные скобки { и }. Анонимный метод в предыдущем примере не использует параметры, поставляемые делегатом, и поэтому может не включать список параметров. Чтобы получить доступ к параметрам, анонимный метод может включать список параметров:

    addButton.Click += delegate(object sender, EventArgs e) {
    	MessageBox.Show(((Button)sender).Text);
    };
    

    В предыдущих примерах происходит неявное преобразование анонимного метода в тип делегата EventHandler (тип события Click). Это неявное преобразование возможно, потому что список параметров и возвращаемый тип типа делегата совместимы с анонимным методом. Правила совместимости следующие:

    • Список параметров делегата совместим с анонимным методом, если выполняется одно из нижеследующих условий:
      • У анонимного метода нет списка параметров, и у делегата нет параметров out.
      • Анонимный метод включает список параметров, количество, типы и модификаторы которых точно совпадают с параметрами делегатов.
    • Возвращаемый тип делегата совместим с анонимным методом, если выполняется одно из условий:
      • Возвращаемый тип делегата — void, и в анонимном методе нет операторов return или есть только операторы return без выражений.
      • Возвращаемый тип делегата не void, и выражения, ассоциированные со всеми операторами return в анонимном методе, могут быть явно преобразованы в возвращаемый тип делегата.

    Чтобы могло произойти неявное преобразование, и список параметров, и возвращаемый тип делегата должны быть совместимы с анонимным методом.

    В следующем примере анонимные методы используются для написания функций «в строке». Анонимные методы передаются как параметры типа делегата Function.

    using System;
    delegate double Function(double x);
    class Test
    {
    	static double[] Apply(double[] a, Function f) {
    		double[] result = new double[a.Length];
    		for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
    		return result;
    	}
    	static double[] MultiplyAllBy(double[] a, double factor) {
    		return Apply(a, delegate(double x) { return x * factor; });
    	}
    	static void Main() {
    		double[] a = {0.0, 0.5, 1.0};
    		double[] squares = Apply(a, delegate(double x) { return x * x; });
    		double[] doubles = MultiplyAllBy(a, 2.0);
    	}
    }
    

    Метод Apply применяет данную Function к элементам double[], возвращая double[]с результатами. В методе Main второй передаваемый в Apply параметр — это анонимный метод, совместимый с типом делегата Function. Анонимный метод просто возвращает квадрат своего аргумента и, таким образом, результат вызова Apply — массив double[], содержащий квадраты значений a.

    Метод MultiplyAllBy возвращает double[], созданный путем умножения каждого из значений массива аргументов a на заданный factor. Чтобы генерировать результат, MultiplyAllBy вызывает метод Apply, передающий анонимный метод, который умножает аргумент x на factor.

    Локальные переменные и параметры, область видимости которых включает анонимный метод, называют внешними переменными (outer variables) анонимного метода. В методе MultiplyAllBya и factor — внешние переменные анонимного метода, передаваемого в Apply, и поскольку анонимный метод использует factor, говорят, что factor был захвачен (captured) анонимным методом. Обычно время жизни локальной переменной ограничено временем выполнения блока или оператора, с которым она ассоциирована. Однако время жизни захваченной внешней переменной продлевается, по крайней мере, до тех пор, пока делегат, использующий анонимный метод, не будет собран сборщиком.

    Преобразования группы методов

    Как было описано в предыдущем разделе, анонимный метод может быть неявно преобразован в совместимый тип делегата. C# 2.0 позволяет такой же тип преобразования для группы методов, практически во всех случаях обеспечивая возможность не создавать явно экземпляры делегатов. Например, выражения

    addButton.Click += new EventHandler(AddClick);
    Apply(a, new Function(Math.Sin));
    

    Могут быть записаны следующим образом

    addButton.Click += AddClick;Apply(a, Math.Sin);
    

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

    Итераторы

    Оператор C# foreach используется для перебора элементов перечислимой (enumerable) коллекции. Чтобы быть перечислимой, коллекция должна иметь метод GetEnumerator без параметров, который возвращает перечислитель (enumerator). Как правило, реализовать счетчики трудно, но с итераторами задача существенно упрощается.

    Итератор (iterator) — это блок оператора, который формирует (yields) упорядоченную последовательность значений. Итератор отличается от обычного блока оператора наличием одного или более операторов yield:

    • Оператор yield return формирует следующее значение итерации.
    • Оператор yield break показывает, что итерация завершена.

    Итератор может использоваться как тело члена функции до тех пор, пока возвращаемый тип члена функции является одним из интерфейсов-перечислителей (enumerator interfaces) или одним из перечислимых интерфейсов (enumerable interfaces)

    • Интерфейсы-перечислители — это System.Collections.IEnumerator и типы, составленные из System.Collections.Generic.IEnumerator<T>.
    • Перечислимые интерфейсы — это System.Collections.IEnumerable и типы, составленные из System.Collections.Generic.IEnumerable<T>.

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

    Следующий класс Stack<T> реализовывает метод GetEnumerator, используя итератор. Итератор перечисляет элементы стека сверху вниз.

    using System.Collections.Generic;
    public class Stack<T>: IEnumerable<T>
    {
    	T[] items;
    	int count;
    	public void Push(T data) {...}
    	public T Pop() {...}
    	public IEnumerator<T> GetEnumerator() {
    		for (int i = count – 1; i >= 0; --i) {
    			yield return items[i];
    		}
    	}
    }
    

    Присутствие метода GetEnumerator делает Stack<T> перечислимым типом, что позволяет использовать экземпляры Stack<T> в операторе foreach. В следующем примере значения от 0 до 9 помещаются в стек целых, а затем с помощью цикла foreach отображаются в обратном порядке.

    using System;
    class Test
    {
    	static void Main() {
    		Stack<int> stack = new Stack<int>();
    		for (int i = 0; i < 10; i++) stack.Push(i);
    		foreach (int i in stack) Console.Write("{0} ", i);
    		Console.WriteLine();
    	}
    }
    

    Результат такой:

    9 8 7 6 5 4 3 2 1 0
    

    Оператор foreach неявно вызывает метод без параметров GetEnumerator коллекции, чтобы получить перечислитель. Всего может существовать лишь один такой определенный коллекцией метод без параметров GetEnumerator, однако, часто целесообразно иметь несколько способов перечисления и способов контроля за перечислением через параметры. В подобных случаях коллекция может использовать итераторы для реализации свойств или методов, которые возвращают один из перечислимых интерфейсов. Например, Stack<T> может представить два новых свойства, TopToBottom и BottomToTop, типа IEnumerable<T>:

    using System.Collections.Generic;
    public class Stack<T>: IEnumerable<T>
    {
    	T[] items;
    	int count;
    	public void Push(T data) {...}
    	public T Pop() {...}
    	public IEnumerator<T> GetEnumerator() {
    		for (int i = count – 1; i >= 0; --i) {
    			yield return items[i];
    		}
    	}
    	public IEnumerable<T> TopToBottom {
    		get {
    			return this;
    		}
    	}
    	public IEnumerable<T> BottomToTop {
    		get {
    			for (int i = 0; i < count; i++) {
    				yield return items[i];
    			}
    		}
    	}
    }
    

    Чтобы получить аксессор для TopToBottom, свойство просто возвращает this, поскольку сам стек является перечислимым. Свойство BottomToTop возвращает перечисление, реализованное с помощью C#-итератора. Следующий пример показывает, как могут использоваться свойства для перечисления элементов стека в любом порядке:

    using System;
    class Test
    {
    	static void Main() {
    		Stack<int> stack = new Stack<int>();
    		for (int i = 0; i < 10; i++) stack.Push(i);
    		foreach (int i in stack.TopToBottom) Console.Write("{0} ", i);
    		Console.WriteLine();
    		foreach (int i in stack.BottomToTop) Console.Write("{0} ", i);
    		Console.WriteLine();
    	}
    }
    

    Конечно же, эти свойства могут использоваться и вне оператора foreach. В следующем примере результаты вызова свойств передаются в отдельный метод Print. В примере также показан итератор, используемый как тело метода FromToBy, который принимает параметры:

    using System;
    using System.Collections.Generic;
    class Test
    {
    	static void Print(IEnumerable<int> collection) {
    		foreach (int i in collection) Console.Write("{0} ", i);
    		Console.WriteLine();
    	}
    	static IEnumerable<int> FromToBy(int from, int to, int by) {
    		for (int i = from; i <= to; i += by) {
    			yield return i;
    		}
    	}
    	static void Main() {
    		Stack<int> stack = new Stack<int>();
    		for (int i = 0; i < 10; i++) stack.Push(i);
    		Print(stack.TopToBottom);
    		Print(stack.BottomToTop);
    		Print(FromToBy(10, 20, 2));
    	}
    }
    

    В результате выполнения этого примера будет получено следующее:

    9 8 7 6 5 4 3 2 1 0
    0 1 2 3 4 5 6 7 8 9
    10 12 14 16 18 20
    

    Шаблонные и нешаблонные перечислимые интерфейсы содержат единственный член, метод GetEnumerator, который принимает аргументы и возвращает интерфейс-перечислитель. Перечислимый интерфейс работает как фабрика перечислителей. Правильно реализованные перечисления генерируют независимые перечислители при каждом вызове их метода GetEnumerator. Если предположить, что внутреннее состояние перечисления не менялось между двумя обращениями к GetEnumerator, оба возвращенных перечислителя должны создать одинаковый набор значений в одинаковой последовательности. Это условие сохраняется, даже если времена жизни перечислителей перекрываются, как в следующем примере:

    using System;
    using System.Collections.Generic;
    class Test
    {
    	static IEnumerable<int> FromTo(int from, int to) {
    		while (from <= to) yield return from++;
    	}
    	static void Main() {
    		IEnumerable<int> e = FromTo(1, 10);
    		foreach (int x in e) {
    			foreach (int y in e) {
    				Console.Write("{0,3} ", x * y);
    			}
    			Console.WriteLine();
    		}
    	}
    }
    

    Приведенный выше код печатает простую таблицу умножения целых от 1 до 10. Обратите внимание, что метод FromTo вызывается только один раз для формирования перечислимого e. Однако e.GetEnumerator() вызывается много раз (операторами foreach) для генерирования множества эквивалентных перечислителей. Все эти перечислители инкапсулируют код итератора, определенный в описании FromTo. Обратите внимание, что код итератора изменяет параметр from. Тем не менее, перечислители работают независимо, потому что у каждого из них есть его собственная копия параметров from и to. Совместное использование переходного состояния перечислителями — это один из нескольких обычных неявных дефектов, которых надо избегать при реализации перечислимых и перечислителей. Итераторы C# разработаны, чтобы помочь избежать этих проблем и для простой наглядной реализации устойчивых перечислимых и перечислителей.

    Неполные типы

    В то время как размещение всего исходного кода для типа в отдельный файл является хорошей практикой программирования, иногда тип настолько велик, что это требование становится невыполнимым. Более того, разработчики часто используют генераторы исходного кода для создания исходной структуры приложения, а затем дополняют ее. К сожалению, при последующих выпусках исходного кода, существующие изменения перезаписываются.

    Неполные типы (Partial types) обеспечивают возможность разбивать классы, структуры и интерфейсы на множество частей, хранящихся в различных исходных файлах, для облегчения разработки и эксплуатации. Кроме того, неполные типы позволяют разделение сгенерированных машиной и написанных пользователем частей типов, таким образом, становится легче дополнять код, сгенерированный инструментальным средством.

    При определении типа в нескольких частях используется новый модификатор типа — partial. Далее приведен пример неполного класса, который реализован в двух частях. Эти две части могут находиться в различных исходных файлах, потому что первая часть сгенерирована инструментом преобразования данных базы данных, а вторая часть написана вручную:

    public partial class Customer
    {
    	private int id;
    	private string name;
    	private string address;
    	private List<Order> orders;
    public Customer() {
    		...
    	}
    }
    public partial class Customer
    {
    	public void SubmitOrder(Order order) {
    		orders.Add(order);
    	}
    	public bool HasOutstandingOrders() {
    		return orders.Count > 0;
    	}
    }			
    

    После совместной компиляции этих двух частей получается код, аналогичный тому, если бы класс был написан как единый модуль:

    public class Customer
    {
    	private int id;
    	private string name;
    	private string address;
    	private List<Order> orders;
    	public Customer() {
    		...
    	}
    	public void SubmitOrder(Order order) {
    		orders.Add(order);
    	}
    	public bool HasOutstandingOrders() {
    		return orders.Count > 0;
    	}
    }
    

    Все части неполного типа должны компилироваться вместе, из условия чтобы они могли быть объединены во время компиляции. В частности, неполные типы не допускают расширения уже откомпилированных типов.

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

    Материал, изложенный в данной статье, многократно проверен. Но, поскольку вероятность технических ошибок все равно существует, сообщество не может гарантировать абсолютную точность и правильность приводимых сведений. В связи с этим сообщество не несет ответственности за возможные ошибки, связанные с использованием статьи.
     
         

       
       
         
      VBNet рекомендует