русс | укр

Языки программирования

ПаскальСиАссемблерJavaMatlabPhpHtmlJavaScriptCSSC#DelphiТурбо Пролог

Компьютерные сетиСистемное программное обеспечениеИнформационные технологииПрограммирование

Все о программировании


Linux Unix Алгоритмические языки Аналоговые и гибридные вычислительные устройства Архитектура микроконтроллеров Введение в разработку распределенных информационных систем Введение в численные методы Дискретная математика Информационное обслуживание пользователей Информация и моделирование в управлении производством Компьютерная графика Математическое и компьютерное моделирование Моделирование Нейрокомпьютеры Проектирование программ диагностики компьютерных систем и сетей Проектирование системных программ Системы счисления Теория статистики Теория оптимизации Уроки AutoCAD 3D Уроки базы данных Access Уроки Orcad Цифровые автоматы Шпаргалки по компьютеру Шпаргалки по программированию Экспертные системы Элементы теории информации

Базовые элементы .NET Framework


Дата добавления: 2013-12-23; просмотров: 1353; Нарушение авторских прав


Наследование классов

Язык C# полностью поддерживает объектно-ориентированную концепцию наследования. Чтобы указать, что один класс является наследником другого, используется следующий синтаксис:

class <имя наследника> : <имя базового класса> {<тело класса>}

Наследник обладает всеми полями, методами и свойствами предка, однако элементы предка с модификатором private не доступны в наследнике. Конструкторы класса-предка не переносятся в класс-наследник.

При наследовании нельзя расширить область видимости класса: internal–класс может наследоваться от public–класса, но не наоборот.

Для обращения к методам непосредственного предка класс-наследник может использовать ключевое слово base в форме base.<имя метода базового класса>. Если конструктор наследника должен вызвать конструктор предка, то для этого также используется base:

<конструктор наследника>([<параметры>]): base([<параметры_2>])

Для конструкторов производного класса справедливо следующее замечание: конструктор должен вначале совершить вызов другого конструктора своего или базового класса. Если вызов конструктора базового класса отсутствует, компилятор автоматически подставляет в заголовок конструктора вызов :base(). Если в базовом классе нет конструктора без параметров, происходит ошибка компиляции.

Наследование от двух и более классов в C# запрещено.

Для классов можно указать два модификатора, связанных с наследованием. Модификатор sealed задает класс, от которого запрещено наследование. Модификатор abstract задает абстрактный класс, у которого обязательно должны быть наследники. Объект абстрактного класса создать нельзя, хотя статические члены такого класса можно вызвать, используя имя класса. Модификаторы наследования указываются непосредственно перед ключевым словом class:



sealed class FinishedClass { }

abstract class AbstractClass { }

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

class CPet {

public void Speak() {

Console.WriteLine("I'm a pet");

}

}

 

class CDog : CPet {

public void Speak() {

Console.WriteLine("I'm a dog");

}

}

. . .

CPet Pet = new CPet();

CDog Dog = new CDog();

Pet.Speak();

Dog.Speak();

При компиляции данного фрагмента будет получено предупреждающее сообщение о том, что метод CDog.Speak() закрывает метод базового класса CPet.Speak(). Чтобы подчеркнуть, что метод класса-наследника замещает метод базового класса, используется ключевое слово new:

class CDog : CPet

{

new public void Speak() { //Компиляция без предупреждений

Console.WriteLine("I'm a dog");

}

}

Ключевое слово new может размещаться как до, так и после модификаторов доступа для метода. Данное ключевое слово применимо и к полям класса.

Замещение методов класса не является полиморфным по умолчанию. Следующий фрагмент кода печатает две одинаковые строки:

CPet Pet, Dog;

Pet = new CPet();

Dog = new CDog(); // Допустимо по правилам присваивания

Pet.Speak(); // Печатает "I'm a pet"

Dog.Speak(); // Так же печатает "I'm a pet"

Для организации полиморфного вызова методов применяется пара ключевых слов virtual и override: virtual указывается для метода базового класса, который мы хотим сделать полиморфным, override – для методов производных классов. Эти методы должны совпадать по имени и сигнатуре с перекрываемым методом класса-предка.

class CPet {

public virtual void Speak() {

Console.WriteLine("I'm a pet");

}

}

 

class CDog : CPet {

public override void Speak() {

Console.WriteLine("I'm a dog");

}

}

. . .

CPet Pet, Dog;

Pet = new CPet();

Dog = new CDog();

Pet.Speak(); // Печатает "I'm a pet"

Dog.Speak(); // Теперь печатает "I'm a dog"

Если на некоторой стадии построения иерархии классов требуется запретить дальнейшее переопределение виртуального метода в производных классах, этот метод помечается ключевым словом sealed:

class CDog : CPet {

public sealed override void Speak() { . . . }

Для методов абстрактных классов (классов с модификатором abstract) возможно задать модификатор abstract, который говорит о том, что метод не реализуется в классе, а должен обязательно переопределяться в наследнике.

abstract class AbstractClass

{

//Реализации метода в классе нет

public abstract void AbstractMethod();

}

Отметим, что наряду с виртуальными методами в C# можно описать виртуальные свойства (свойство транслируется в методы). Статические элементы класса не могут быть виртуальными.

1.16. Перегрузка операЦИЙ

Язык C# позволяет организовать для объектов пользовательского класса или структуры перегрузку операций. Могут быть перегружены унарные операции +, -, !, ~, ++, --, true, false и бинарные операции +, -, *, /, %, &, |, ^, <<, >>, ==, !=, >, <, >=, <=.

При перегрузке бинарной операции автоматически перегружается соответствующая операция с присваиванием (например, при перегрузке операции + перегрузится и операция +=). Некоторые операции могут быть перегружены только парами: == и !=, > и <, >= и <=, true и false.

Для перегрузки операций используется специальный статический метод, имя которого образовано из ключевого слова operator и знака операции. Количество формальных параметров метода зависит от типа операции: унарная операция требует одного параметра, бинарная – двух. Метод обязательно должен иметь модификатор доступа public.

Рассмотрим перегрузку операций на примере. Определим класс для представления комплексных чисел с перегруженной операцией сложения:

class Complex {

public double Re;

public double Im;

public Complex(double Re, double Im) {

this.Re = Re;

this.Im = Im;

}

public override string ToString() {

return String.Format("Re = {0} Im = {1}", Re, Im);

}

 

public static Complex operator + (Complex A, Complex B) {

return new Complex(A.Re + B.Re, A.Im + B.Im);

}

}

Для объектов класса Complex возможно использование следующего кода:

Complex A = new Complex(10.0, 20.0);

Complex B = new Complex(-5.0, 10.0);

Console.WriteLine(A); // Выводит Re = 10.0 Im = 20.0

Console.WriteLine(B); // Выводит Re = -5.0 Im = 10.0

Console.WriteLine(A + B); // Выводит Re = 5.0 Im = 30.0

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

Внесем некоторые изменения в класс Complex:

class Complex {

. . .

public static Complex operator + (Complex A, Complex B) {

return new Complex(A.Re + B.Re, A.Im + B.Im);

}

public static Complex operator + (Complex A, double B) {

return new Complex(A.Re + B, A.Im + B);

}

}

Новая перегруженная операция сложения позволяет прибавлять к комплексному числу вещественное число.

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

Рассмотрим следующий пример. Пусть в классе Complex перегружены операции true и false:

class Complex {

. . .

public static bool operator true (Complex A) {

return (A.Re > 0) || (A.Im > 0);

}

public static bool operator false (Complex A) {

return (A.Re == 0) && (A.Im == 0);

}

}

Теперь возможно написать такой код (обратите внимание на оператор if):

Complex A = new Complex(10.0, 20.0);

Complex B = new Complex(0, 0);

if (B)

Console.WriteLine("Number is not zero");

else

Console.WriteLine("Number is 0.0 + 0.0i");

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

public static implicit operator <целевой тип>(<привод. тип> <имя>)

public static explicit operator <целевой тип>(<привод. тип> <имя>)

Ключевое слово implicit используется при перегрузке неявного приведения типов, а ключевое слово explicit – при перегрузке операции явного приведения. Либо <целевой тип>, либо <приводимый тип> должены совпадать с типом того класса, где выполняется перегрузка операций.

Поместим две перегруженных операции приведения в класс Complex:

class Complex {

. . .

public static implicit operator Complex (double a) {

return new Complex(a, 0);

}

public static explicit operator double (Complex A) {

return Math.Sqrt(A.Re * A.Re + A.Im * A.Im);

}

}

 

Вот пример кода, использующего преобразование типов:

Complex A = new Complex(3.0, 4.0);

double x;

//Выполняем явное приведение типов

x = (double) A;

Console.WriteLine(x); //Выводит 5

 

double y = 10;

//Выполняем неявное приведение типов

A = y;

Console.WriteLine(A); //Выводит Re = 10 Im = 0

1.17. Делегаты

Делегат в языке C# исполняет роль указателя на метод. Делегат объявляется с использованием ключевого слова delegate. При этом указывается имя делегата и сигнатура инкапсулируемого метода. Модификаторы доступа при необходимости указываются перед ключевым словом delegate:

delegate double Function(double x);

public delegate void IntegerSub(int i);

Делегат – самостоятельный пользовательский тип, он может быть как вложен в другой пользовательский тип (класс, структуру), так и объявлен отдельно. Так как делегат – это пользовательский тип, то нельзя объявить два или более делегатов с одинаковыми именами, но разной сигнатурой.

После объявления делегата можно объявить переменные этого типа:

Function Y;

IntegerSub SomeSub;

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

Y1 = new Function(ClassName.MyStaticFunction);

Y1 = new Function(Obj1.MyInstanceFunction);

Y2 = new Function(Y1);

После того как делегат инициализирован, инкапсулированный в нем метод вызывается, указывая параметры метода непосредственно после имени переменной-делегата:

Y1(0.5);

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

class ArrayPrint {

public static void print(int[] A, PrintMethod P) {

foreach(int element in A)

P(element);

}

}

PrintMethod является делегатом, который определяет способ печати отдельного числа. Он описан следующим образом:

delegate void PrintMethod(int x);

Теперь можно написать класс, который работает с классом ArrayPrint и делегатом PrintMethod:

class MainClass {

public static void ConsolePrint(int i) {

Console.WriteLine(i);

}

 

public void FormatPrint(int i) {

Console.WriteLine("Element is {0}", i);

}

 

public static void Main() {

int[] A = {1, 20, 30};

 

PrintMethod D = new PrintMethod(MainClass.ConsolePrint);

ArrayPrint.print(A, D);

 

MainClass C = new MainClass();

D = new PrintMethod(C.FormatPrint);

ArrayPrint.print(A, D);

}

}

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

Element is 1

Element is 20

Element is 30

Обратите внимание, что в данном примере переменная D инициализировалась статическим методом, а затем методом объекта. Делегат не делает различий между экземплярными и статическими методами класса.

Ключевой особенностью делегатов является то, что они могут инкапсулировать не один метод, а несколько. Подобные делегаты называются групповыми делегатами. При вызове группового делегата срабатывает вся цепочка инкапсулированных в нем методов.

Групповой делегат объявляется таким же образом, как и обычный. Затем создается несколько объектов делегата, и все они связывается с некоторыми методами. После этого используются перегруженные версии операций + или += класса System.Delegate для объединения делегатов в один групповой делегат. Для объединения можно использовать статический метод System.Delegate.Combine(), который получает в качестве параметров два объекта делегата (или массив объектов-делегатов) и возвращает групповой делегат, являющийся объединением параметров.

Модифицируем код из предыдущего примера следующим образом:

class MainClass {

. . .

public static void Main() {

int[] A = {1, 20, 30};

PrintMethod first, second, result;

first = new PrintMethod(MainClass.ConsolePrint);

MainClass C = new MainClass();

second = new PrintMethod(C.FormatPrint);

result = first + second;

ArrayPrint.print(A, result);

}

}

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

Element is 1

Element is 20

Element is 30

Если требуется удалить некий метод из цепочки группового делегата, то используются перегруженные операции – или -= (или метод System.Delegate.Remove()). Если из цепочки удаляют последний метод, результатом будет значение null. Следующий код удаляет метод first из цепочки группового делегата result:

result -= first;

Любой пользовательский делегат можно рассматривать как класс-наследник класса System.MulticastDelegate, который, в свою очередь, наследуется от класса System.Delegate. Именно на уровне класса System.Delegate определены перегрузкий операций + и -, использовавшихся для создания групповых делегатов. Полезным также может оказаться экземплярный метод GetInvocationList(). Он возвращает массив объектов, составляющих цепочку вызова группового делегата.

1.18. События

События представляют собой способ описания связи одного объекта с другими по действиям. Родственным концепции событий является понятие функции обратного вызова.

Работу с событиями можно условно разделить на три этапа:

· объявление события (publishing);

· регистрация получателя события (subscribing);

· генерация события (raising).

Событие можно объявить в пределах класса, структуры или интерфейса. При объявлении события требуется указать делегат, описывающий процедуру обработки события. Синтаксис объявления события следующий:

event <имя делегата> <имя события>;

Ключевое слово event указывает на объявление события. Объявление события может предваряться модификаторами доступа.

Приведем пример класса с объявлением события:

//Объявление делегата для события

delegate void Proc(int val);

 

class CEventClass {

int data;

//Объявление события

event Proc OnWrongData;

. . .

}

Фактически, события являются полями типа делегатов. Объявление события транслируется компилятором в следующий набор объявлений в классе:

a. в классе объявляется private-поле с именем <имя события> и типом <имя делегата>;

b. в классе объявляются два метода с именами add_<имя события> и remove_<имя события> для добавления и удаления обработчиков события.

Методы для обслуживания события содержат код, добавляющий (add_*) или удаляющий (remove_*) процедуру обработки события в цепочку группового делегата, связанного с событием.

Для генерации события в требуемом месте кода помещается вызов в формате <имя события>(<фактические аргументы>). Предварительно можно проверить, назначен ли обработчик события. Генерация события может происходить в одном из методов того же класса, в котором объявлено событие. Генерировать в одном классе события других классов нельзя.

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

delegate void Proc(int val);

 

class CExampleClass {

int field;

 

public event Proc onErrorEvent;

 

public void setField(int i){

field = i;

if(i < 0) {

if(onErrorEvent != null) onErrorEvent(i);

}

}

}

Рассмотрим этап регистрации получателя события. Для того чтобы отреагировать на событие, его надо ассоциировать с обработчиком события. Обработчиком события может быть метод-процедура, совпадающий по типу с типом события (делегатом). Назначение и удаление обработчиков события выполняется при помощи перегруженных версий операторов += и -=. При этом в левой части указывается имя события, в правой части – объект делегата, созданного на основе требуемого метода-обработчика.

Используем предыдущий класс CExampleClass и продемонстрируем назначение и удаление обработчиков событий:

class MainClass {

public static void firstReaction(int i) {

Console.WriteLine("{0} is a bad value!", i);

}

 

public static void secondReaction(int i) {

Console.WriteLine("Are you stupid?");

}

 

public static void Main() {

CExampleClass c = new CExampleClass();

c.setField(200);

c.setField(-200); // Нет обработчиков, нет и реакции

// Если бы при генерации события в CExampleClass

// отсутствовала проверка на null, то предыдущая

// строка вызвала бы исключительную ситуацию

 

// Назначаем обработчик

c.onErrorEvent += new Proc(firstReaction);

 

// Теперь будет вывод "-10 is a bad value!"

c.setField(-10);

 

// Назначаем еще один обработчик

c.onErrorEvent += new Proc(secondReaction);

 

// Вывод: "-10 is a bad value!" и "Are you stupid?"

c.setField(-10);

}

}

Выше было указано, что методы добавления и удаления обработчиков события генерируются в классе автоматически. Если программиста по каким-либо причинам не устраивает подобный подход, он может описать собственную реализацию данных методов. Для этого при объявлении события указывается блок, содержащий секции add и remove:

event <имя делегата> <имя события> {

add { }

remove { }

};

Кроме этого, при наличии собственного кода для добавления/удаления обработчиков, требуется явно объявить поле-делегат для хранения списка методов обработки.

Исправим класс CExampleClass, использовав для события onErrorEvent секции add и remove:

class CExampleClass {

int field;

//Данное поле будет содержать список обработчиков

private Proc handlerList;

 

public event Proc onErrorEvent {

add {

Console.WriteLine("Handler added");

// Обработчик поступает как неявный параметр value

// Обратите внимание на приведение типов!

handlerList += (Proc) value;

}

remove {

Console.WriteLine("Handler removed");

handlerList -= (Proc) value;

}

}

 

public void setField(int i){

field = i;

if (i < 0) {

// Проверяем на null не событие, а скрытое поле

if (handlerList != null) handlerList(i);

}

}

}

В заключение отметим, что считается стандартным такой подход, при котором сигнатура делегата, отвечающего за обработку события, содержит параметр sender (типа object), указывающий на источник события, и объект класса System.EventArgs (или класса, производного от System.EventArgs). Задача второго параметра – инкапсулировать параметры обработчика события.

1.19. Интерфейсы

В языке C# запрещено множественное наследование классов. Тем не менее, в C# существует концепция, позволяющая имитировать множественное наследование. Эта концепция интерфейсов. Интерфейс представляет собой набор объявлений свойств, индексаторов, методов и событий. Класс или структура могут реализовывать определенный интерфейс. В этом случае они берут на себя обязанность предоставить полную реализацию элементов интерфейса (хотя бы пустыми методами). Можно сказать так: интерфейс – это контракт, пункты которого суть свойства, индексаторы, методы и события. Если пользовательский тип реализует интерфейс, он берет на себя обязательство выполнить этот контракт.

Объявление интерфейса схоже с объявлением класса. Для объявления интерфейса используется ключевое слово interface. Интерфейс содержит только заголовки методов, свойств и событий:

interface IBird {

// Метод

void Fly();

 

// Свойство

double Speed { get; set;}

}

Обратите внимание – в определении элементов интерфейса отсутствуют модификаторы уровня доступа. Считается, что все элементы интерфейса имеют public уровень доступа. Более точно, следующие модификаторы не могут использоваться при объявлении членов интерфейса: abstract, public, protected, internal, private, virtual, override, static. Для свойства, объявленного в интерфейсе, указываются только ключевые слова get и (или) set.

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

Чтобы показать, что класс реализовывает некий интерфейс, используется синтаксис <имя класса> : <имя реализовываемого интерфейса> при записи заголовка класса. Если класс является производным от некоторого базового класса, то имя базового класса указывается перед именем реализовываемого интерфейса: <имя класса> : <имя базового класса>, <имя интерфейса>. В простейшем случае, чтобы указать, что некий член класса соответствует элементу интерфейса, у них должны совпадать имена:

class CFalcon : IBird {

private double FS;

public void DoSomeThing() {

Console.WriteLine("Falcon Flys");

}

public void Fly() {

Console.WriteLine("Falcon Flys");

}

public double Speed {

get { return FS; }

set { FS = value; }

}

}

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

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

// Объявим переменную интерфейсного типа

IBird Bird;

 

// Инициализация объектом подходящего класса

Bird = new CFalcon();

Bird.Fly(); // Фактически вызывается CFalcon.Fly()

 

// Строка вызовет ошибку компиляции! В IBird нет такого метода

Bird.DoSomeThing();

Если необходимо проверить, поддерживает ли объект Obj некоего класса интерфейс Inter, то можно воспользоваться операцией is:

//Результа равен true, если Obj реализует Inter

if (Obj is Inter) . . .

Один класс может реализовывать несколько интерфейсов, при этом имена интерфейсов перечисляются после имени класса через запятую:

interface ISwimable {

void Swim();

}

 

class CDuck : IBird, ISwimable {

public void Fly() {

Console.WriteLine("Duck Flies");

}

public void Swim() {

Console.WriteLine("Duck Swims");

}

public double Speed {

get { return 0.0;}

set { }

}

}

Если класс реализует несколько интерфейсов, которые имеют элементы с совпадающими именами, или имя одного из членов класса совпадает с именем элемента интерфейса, то при записи члена класса требуется указать имя в виде <имя интерфейса>.<имя члена>. Указание модификаторов доступа при этом запрещается.

Подобно классам, интерфейсы могут наследоваться от других интерфейсов. При этом, в отличие от классов, наследование интерфейсов может быть множественным.

1.20. Структуры и перечисления

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

struct <имя структуры> {

<элементы структуры>

}

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

Рассмотрим пример структуры для представления комплексных чисел:

struct Complex {

public double Re, Im;

public Complex(double X, double Y) {

Re = X;

Im = Y;

}

public Complex Add(Complex Z) {

return new Complex(this.Re + Z.Re, this.Im + Z.Im);

}

}

Переменные структур определяются как обычные переменные примитивных типов. Однако следует иметь в виду, что поля таких переменных будут не инициализированны. При определении переменной можно вызвать конструктор без параметров – тогда поля получат значения по умолчанию своих типов. Также допустим вызов пользовательского конструктора:

// Поля Z1 не инициализированы, их надо установить

Complex Z1;

 

// Поля Z2 инициализированы значениями 0.0

Complex Z2 = new Complex();

 

// Поля Z3 инициализированы значениями 2.0, 3.0

Complex Z3 = new Complex(2.0, 3.0);

Доступ к элементам структуры осуществляется так же, как к элементам объекта класса:

Z1.Re = 10.0;

Z1.Im = 5.0;

Z2 = Z3.Add(Z1);

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

Перечисление – это тип, содержащий в качестве элементов именованные целочисленные константы. Рассмотрим синтаксис определения перечисления:

enum <имя перечисления> [: <тип перечисления>] {

<элемент перечисления 1> [= <значение элемента>],

. . .

<элемент перечисления N> [= <значение элемента>]

}

Перечисление может предваряться модификатором доступа. Если задан тип перечисления, то он определяет тип каждого элемента перечисления. Типами перечислений могут быть только byte, sbyte, short, ushort, int, uint, long или ulong. По умолчанию принимается тип int. Для элементов перечисления область видимости указать нельзя. Значением элемента перечисления должна быть целочисленная константа. Если для какого-либо элемента перечисления значение опущено, то в случае, если это первый элемент, он принимает значение 0, иначе элемент принимает значение на единицу большее предыдущего элемента. Заданные значения элементов перечисления могут повторяться.

Приведем примеры определения перечислений:

enum Seasons {

Winter,

Spring,

Summer,

Autumn

}

 

public enum ErrorCodes : byte {

First = 1,

Second = 2,

Fourth = 4

}

После описания перечисления можно объявить переменную соответствующего типа:

Seasons S;

ErrorCodes EC;

Переменной типа перечисления можно присваивать значения, как и обычной переменной:

S = Seasons.Spring;

Console.WriteLine(S); // Выводит на печать Spring

Перечисления фактически являются наследниками типа System.Enum. При компиляции проводится простая подстановка соответствующих значений для элементов перечислений.

1.21. Пространства имен

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

Синтаксис описания пространства имен следующий:

namespace <имя пространства имен> {

[<компоненты пространства имен>]

}

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

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

Для доступа к компонентам пространства имен используется синтаксис <имя пространства имен>.<имя компонента>. Для компилируемых входных файлов имя пространства имен по умолчанию (если в файле нет обрамляющего пространства имен) можно задать специальной опцией компилятора.

Для использования в программе некоего пространства имен служит команда using. Ее синтаксис следующий:

using <имя пространства имен>;

или

using [<имя псевдонима> =] <имя пространства>[.<имя типа>];

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

1.22. Генерация и обработка исключительных ситуаций

Опишем возможности по обработке и генерации исключительных ситуаций в языке C#.

Рассмотрим синтаксис генерации исключительной ситуации. Для генерации исключительной ситуации используется команда throw со следующим синтаксисом:

throw <объект класса исключительной ситуации>;

Обратите внимание: объект, указанный после throw, должен обязательно быть объектом класса исключительной ситуации. Таким классом является класс System.Exception и все его наследники.

Рассмотрим пример программы с генерацией исключительной ситуации:

using System;

class CExample {

private int fX;

public void setFx(int x) {

if (x > 0)

fX = x;

else

// Объект исключит. ситуации создается "на месте"

throw new Exception();

}

}

class MainClass {

public static void Main() {

CExample A = new CExample();

A.setFx(-3); // ИС генерируется, но не обрабатывается!

}

}

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

Класс System.Exception является стандартным классом для представления исключительных ситуаций. Основными членами данного класса является свойство только для чтения Message, содержащее строку с описанием ошибки, и перегруженный конструктор с одним параметром-строкой, записываемой в свойство Message. Естественно, библиотека классов .NET Framework содержит большое число разнообразных классов, порожденных от System.Exception и описывающих конкретные исключительные ситуации.

Пользователь может создать собственный класс для представления информации об исключительной ситуации. Единственным условием является прямое или косвенное наследование этого класса от класса System.Exception.

Модифицируем пример с генерацией исключительной ситуации, описав для исключительной ситуации собственный класс:

class MyException : Exception {

public int info;

}

class CExample {

private int fX;

public void setFx(int x) {

if (x > 0)

fX = x;

else {

MyException E = new MyException();

E.info = x;

throw E;

}

}

}

Опишем возможности по обработке исключительных ситуаций. Для перехвата исключительных ситуаций служит блок try – catch – finally. Синтаксис блока следующий:

try {

[<команды, способные вызвать исключительную ситуацию>]

}

[<один или несколько блоков catch>]

[finally {

<операторы из секции завершения> }]

Операторы из части finally (если она присутствует) выполняются всегда, вне зависимости от того, произошла исключительная ситуация или нет. Если один из операторов, расположенных в блоке try, вызвал исключительную ситуацию, управление немедленно передается на блоки catch. Синтаксис отдельного блока catch следующий:

catch [(<тип ИС> [<идентификатор объекта ИС>])] {

<команды обработки исключительной ситуации>

}

<идентификатор объекта ИС> – это некая временная переменная, которая может использоваться для извлечения информации из объекта исключительной ситуации. Отдельно описывать эту переменную нет необходимости.

Модифицируем программу, описанную выше, добавив в нее блок перехвата ошибки:

class MainClass

{

public static void Main()

{

CExample A = new CExample();

try {

Console.WriteLine("Эта строка печатается");

A.setFx(-3);

Console.WriteLine("Строка не печатается, если ошибка ");

}

catch (MyException ex) {

Console.WriteLine("Ошибка при параметре {0}", ex.Info);

}

finally {

Console.WriteLine("Строка печатается - блок finally");

}

}

}

Если используется несколько блоков catch, то обработка исключительных ситуаций должна вестись по принципу «от частного – к общему», так как после выполнения одного блока catch управление передается на часть finally (при отсутствии finally – на оператор после try – catch). Компилятор C# не позволяет разместить блоки catch так, чтобы предыдущий блок перехватывал исключительные ситуации, предназначенные последующим блокам:

try {

. . .

}

//Ошибка компиляции, так как MyException – наследник Exception

catch (Exception ex) {

Console.WriteLine("Общий перехват");

}

catch (MyException ex) {

Console.WriteLine("Эта строка не печатается никогда!");

}

Запись блока catch в форме catch (Exception) { } позволяет перехватывать все исключительные ситуации, генерируемые CLR. Если записать блок catch в форме catch { }, то такой блок будет обрабатывать любые исключительные ситуации, в том числе и не связанные с исполняющей средой.

1.23. НОВОВВЕДЕНИЯ В ЯЗЫКЕ C# 2.0

В ноябре 2005 года корпорация Microsoft представила вторую версию платформы .NET. Эта версия содержит изменения, коснувшиеся как технологий и подходов, применяемых в рамках платформы, так и языков программирования для платформы. В данном и следующем параграфе рассмотрим нововведения во второй версии языка C#.

1. Статические классы. При объявлении класса возможно указать модификатор static. Таким образом определяется статический класс, включающий только статические элементы.

public static class AppSettings {

public static string BaseDir { . . . }

public static string GetRelativeDir() { . . . }

}

Экземпляр статического класса не может быть создан или даже объявлен в программе. Все элементы класса доступны только с использованием имени класса.

2. Частичные типы. Хорошей практикой программирования считается помещать описание каждого пользовательского типа в отдельный файл. Однако иногда классы и структуры получаются настолько большими, что указанный подход становиться непрактичным. Особенно это справедливо при использовании средств автоматического генерирования кода. Частичные типы (partial types) позволяют классам, структурам и интерфейсам быть разбитыми на несколько фрагментов, описанных в отдельных файлах с исходным текстом.

Для объявления частичного типа используется модификатор partial. Рассмотрим пример частичного типа:

// Файл part1.cs

partial class BrokenClass {

private int someField;

private string anotherField;

}

 

// Файл part2.cs

partial class BrokenClass {

public BrokenClass() { }

public void Method() { }

}

Подчеркнем, что все фрагменты частичного типа должны быть доступны во время компиляции, так как «сборку» типа выполняет компилятор. Еще одно замечание касается использования модификаторов, применяемых к типу. Модификаторы доступа должны быть одинаковыми у всех фрагментов. Если же к одному из фрагментов применяется модификатор sealed или abstract, то эти модификаторы считаются примененными ко всем фрагментам, то есть к типу в целом.

3. Модификаторы доступа для get и set частей свойств и индексаторов. Как правило, в пользовательском типе свойства открыты, имеют модификатор доступа public. Однако иногда логика типа требует, чтобы у свойства были отдельные «привилегии» для чтения и записи значений. Например, чтение позволено всем, а запись – только из методов того типа, где объявлено свойство. В C# 2.0 разрешено при описании свойства или индексатора указывать модификаторы доступа для get и set частей:

class SomeClass {

 

public int Prop {

get { . . . }

private set { . . . }

}

}

При указании модификаторов для get и set частей действуют два правила. Во-первых, модификатор может быть только у одной части. Во-вторых, он должен «понижать» видимость части по сравнению с видимостью всего свойства.

4. Безымянные методы. Назначение безымянных методов (anonymous methods) – сократить объем кода, который должен написать разработчик при работе с событиями. Рассмотрим пример, в котором назначаются обработчики событий для объектов класса CExampleClass (подробнее – в параграфе, посвященном работе с событиями).

class MainClass {

public static void firstReaction(int i) {

Console.WriteLine("{0} is a bad value!", i);

}

public static void secondReaction(int i) {

Console.WriteLine("Are you stupid?");

}

 

public static void Main() {

CExampleClass c = new CExampleClass();

// Назначаем обработчик

c.onErrorEvent += new Proc(firstReaction);

// Назначаем еще один обработчик

c.onErrorEvent += new Proc(secondReaction);

 

}

}

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

class MainClass {

public static void Main() {

CExampleClass c = new CExampleClass();

// Назначаем обработчик

c.onErrorEvent += delegate(int i) {

Console.WriteLine("{0} is a bad value!", i); };

// Назначаем еще один обработчик

c.onErrorEvent += delegate {

Console.WriteLine("Are you stupid?"); };

}

}

Таким образом, код обработки события помещен непосредственно в место присваивания делегата событию onErrorEvent. Теперь компилятор сам создаст делегат, основываясь на указанной сигнатуре анонимного метода, поместит в этот делегат указатель на код анонимного метода, а сам объект делегата присвоит событию.

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

1. Список параметров делегата совместим с безымянным методом, если выполняется одно из двух условий:

a. безымянный метод не имеет параметров, а делегат не имеет out-параметров;

b. список параметров безымянного метода полностью совпадает со списком параметров делегата (число параметров, типы, модификаторы)

2. Тип, возвращаемый делегатом, совместим с типом безымянного метода, если выполняется одно из двух условий:

a. тип делегата – void, а безымянный метод не имеет оператора return или оператор return записан без последующего выражения;

b. выражение, записанное после 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[] MultAllBy(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[] s = Apply(a, delegate(double x){ return x*x;});

double[] doubles = MultAllBy(a, 2.0);

}

}

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

addButton.Click += new EventHandler(AddClick);

Apply(a, new Function(Math.Sin));

В C# 2.0 он может быть переписан в следующей короткой форме:

addButton.Click += AddClick;

Apply(a, Math.Sin);

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

5. Итераторы. Язык C# содержит удобную синтаксическую конструкцию foreach для перебора элементов пользовательского типа. Чтобы поддерживать перебор при помощи foreach, тип должен реализовывать интерфейс IEnumerable. Кодирование поддержки этого интерфейса упрощается с использованием итераторов. Итератор (iterator) – это блок кода, который порождает упорядоченную последовательность значений. Итератор отличает присутствие в блоке кода одного или нескольких операторов yield. Оператор yield return <выражение> возвращает следующее значение в последовательности, оператор yield break прекращает генерирование последовательности.

Итераторы могут использоваться в качестве тела функции, если тип возвращаемого значения функции – это тип, реализующий или наследованный от интерфейсов IEnumerator, IEnumerator<T>, IEnumerable, IEnumerable<T>.

Рассмотрим пример использования итераторов. Следующее приложение выводит на консоль таблицу умножения:

using System;

using System.Collections;

 

class Test {

static IEnumerable FromTo(int from, int to) {

while (from <= to) yield return from++;

}

 

static void Main() {

IEnumerable e = FromTo(1, 10);

foreach (int x in e) {

foreach (int y in e) {

Console.Write("{0,3} ", x * y);

}

Console.WriteLine();

}

}

}

6. Типы с поддержкой null-значений. Для разработчиков всегда была проблемой поддержка неопределенных, пустых значений в структурных типах. Иногда для указания на неопределенное значение использовалось дополнительное булево поле, иногда – некая специальная константа. Язык C# новой версии предлагает для решения этой проблемы типы с поддержкой null-значений.

Тип с поддержкой null-значений (далее для краткости – null-тип) объявляется с использованием модификатора ?, записанного непосредственно после имени типа. Например, для типа int соответствующий null-тип объявляется как int?. Null-типы могут быть объявлены только для структурных типов (примитивных или пользовательских). В null-типе присутствует специальный булевский индикатор HasValue, указывающий на наличие значения, и свойство Value, содержащее значение. Попытка прочитать значение Value при HasValue=false ведет к генерации исключения.

Приведем фрагмент кода, использующий null-типы.

int? x = 123;

int? y = null;

if (x.HasValue) Console.WriteLine(x.Value);

if (y.HasValue) Console.WriteLine(y.Value);

Существует возможность неявного приведения структурного типа в соответствующий null-тип. Кроме этого, любой переменной null-типа может быть присвоено значение null. Если для структурного типа S возможно приведение к структурному типу T, то соответствующая возможность имеется и для типов S? и T?. Также возможно неявное приведение типа S к типу T? и явное приведение S? к T. В последнем случае возможна генерации исключительной ситуации – если значение типа S? не определено.

int x = 10;

int? z = x; // неявное приведение int к int?

double? w = z; // неявное приведение int? к double?

double y = (double)z; // явное приведение int? к double

С поддержкой null-типов связано появление в C# новой операции ??. Результатом выражения a ?? b является a, если оно содержит некое значение, и b – в противном случае. Таким образом, b – это то значение, которое следует использовать, если a не определено. Тип результата выражения a ?? b определяется типом операнда b.

int? x = GetNullableInt();

int? y = GetNullableInt();

int? z = x ?? y;

int i = z ?? -1;

Операцию ?? можно применить и для ссылочных типов:

string s = GetStringValue();

Console.WriteLine(s ?? "Unspecified");

В этом фрагменте кода на консоль выводится значение строки s, или "Unspecified", если s=null.

1.24. ОБОБЩЕННЫЕ ТИПЫ (GENERICS)

Обобщенные (или параметризованные) типы (generics) позволяют при описании пользовательских классов, структур, интерфейсов, делегатов и методов указать как параметр тип данных для хранения и обработки.

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

class Stack {

object[] items;

int count;

public void Push(object item) {...}

public object Pop() {...}

}

Класс Stack универсален, он позволяет хранить произвольные объекты:

Stack stack = new Stack();

stack.Push(new Customer());

Customer c = (Customer)stack.Pop();

 

Stack stack2 = new Stack();

stack2.Push(3);

int i = (int)stack2.Pop();

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

Stack stack = new Stack(); // планируем сделать стек чисел

stack.Push(1);

stack.Push(2);

stack.Push("three"); // вставили не число, а строку

for (int i = 0, i < 3, i++)

// компилируется, но при выполнении на третьей итерации

// будет сгенерирована исключительная ситуация

int result = (int)stack.Pop();

Основной причиной появления обобщенных типов была необходимость устранения описанных недостатков универсальных классов.

Опишем класс Stack как обобщенный тип. Для этого используется следующий синтаксис: после имени класса в угловых скобках < и > указывается параметр типа. Этот параметр затем может использоваться при описании элементов класса Stack.

class Stack<T> {

T[] items;

int count;

public void Push(T item) {...}

public T Pop() {...}

}

При использовании обобщенного типа Stack требуется указать фактический тип вместо параметра T. В следующем фрагменте Stack применяется для хранения данных типа int.

Stack<int> stack = new Stack<int>();

stack.Push(3);

int x = stack.Pop();

Тип вида Stack<int> будем в дальнейшем называть сконструированным типом (constructed type). Обратите внимание: при работе с типом Stack<int> отпала необходимость в выполнении приведения типов при извлечении элементов из стека. Кроме этого, теперь компилятор отслеживает, чтобы в стек помещались только данные типа int. И еще одна менее очевидная особенность. Нет необходимости в упаковке и распаковке структурного элемента, а это приводит к увеличению быстродействия.

При объявлении обобщенного типа можно использовать несколько параметров. Приведем фрагмент описания класса для хранения пар «ключ-значение» с возможностью доступа к значению по ключу:

class Dict<K,V> {

public void Add(K key, V value) {...}

public V this[K key] {...}

}

Сконструированный тип для Dict<K,V> должен быть основан на двух конкретных типах:

Dict<string,Customer> dict = new Dict<string,Customer>();

dict.Add("Alex", new Customer());

Customer c = dict["Alex"];

Как правило, обобщенные типы не просто хранят данные указанного параметра, а еще и вызывают методы у объекта, чей тип указан как параметр. К примеру, в классе Dict<K,V> метод Add() может использовать метод CompareTo() для сравнения ключей:

class Dict<K,V> {

public void Add(K key, V value) {

. . .

if(key.CompareTo(x) < 0) {...} // Ошибка компиляции!

. . .

}

}

Так как тип параметра K может быть любым, у key можно вызывать только методы класса object, и приведенный выше код просто не компилируется. Конечно, проблему можно решить, используя приведение типов:

class Dict<K,V> {

public void Add(K key, V value) {

. . .

if(((IComparable)key).CompareTo(x) < 0) {...}

. . .

}

}

Недостаток данного решения – необходимость приведения типов. К тому же, если тип K не поддерживает интерфейс IComparable, то при работе программы будет сгенерировано исключение InvalidCastException.

В C# для каждого параметра обобщенного типа может быть задан список ограничений (constraints). Только тип, удовлетворяющий ограничениям, может быть применен для записи сконструированного типа.

Ограничения объявляются с использованием ключевого слова where, после которого указывается параметр, двоеточие и список ограничения. Элементом списка ограничения могут являться интерфейсы, класс (только один) и ограничение на конструктор. Для класса Dict<K,V> можно установить ограничение на параметр K, гарантирующее, что тип K реализует IComparable.

class Dict<K,V> where K: IComparable

{

public void Add(K key, V value) {

. . .

if(key.CompareTo(x) < 0) {...}

. . .

}

}

Компилятор будет проверять соблюдение ограничения при создании сконструированного типа. Кроме этого, отпадает необходимость в выполнении приведения типов в теле класса Dict<K,V>.

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

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) {...}

. . .

}

}

Смысл ограничений, наложенных на параметр E: он должен быть приводим к классу Entity и иметь public-конструктор без параметров.

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

В некоторых случаях достаточно параметризовать не весь пользовательский тип, а только конкретный метод. Обобщенные методы (generic methods) объявляются с использованием параметра типа в угловых скобках после имени метода.

void PushMultiple<T>(Stack<T> stack, params T[] values) {

foreach (T value in values) stack.Push(value);

}

Использование обобщенного метода PushMultiple<T> позволяет работать с любым сконструированным типом на основе Stack<T>.

Stack<int> stack = new Stack<int>();

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

Для обобщенных методов, подобных PushMultiple<T>, компилятор способен самостоятельно установить значение параметра типа на основе фактических параметров метода. Это позволяет записывать вызов метода без указания типа:

Stack<int> stack = new Stack<int>();

// Так как stack – объект Stack<int>, то используем тип int

PushMultiple(stack, 1, 2, 3, 4);

Как и при описании типов, обобщенные методы могут содержать ограничения на параметр-тип:

public T Max<T>(T val1, T val2) where T: IComparable {

T retVal = val2;

if (val2.CompareTo(val1) < 0) retVal = val1;

return retVal;

}

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

class MyCache<K, V> {

. . .

// Метод для поиска элемента по ключу

// Если элемент найден, то метод возвращается его

// Иначе метод возвращает значение по умолчанию для V

public V LookupItem(K key) {

V retVal;

if (ContainsKey(key))

retVal = GetValue(key);

else

retVal = default(V);

return retVal;

}

}

Имеющиеся в .NET Framework обобщенные типы для представления структур данных обсуждаются ниже.

 


2.1. МЕТАДАННЫЕ И МЕХАНИЗМ ОТРАЖЕНИЯ

При программировании для платформы .NET в любую сборку помещаются метаданные, которые являются описанием всех типов данных сборки и их членов. Работа с метаданными происходит при помощи специального механизма, называемого механизмом отражения (reflection).

Главные элементы, которые необходимы для использования возможностей отражения – это класс System.Type и типы из пространства имен System.Reflection.

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

1. Вызвать у объекта метод GetType(). Данный метод определен на уровне System.Object, а значит присутствует у любого объекта:

Foo A = new Foo(); //Foo – это некий класс

Type t = A.GetType();

2. Использовать статический метод Type.GetType(), которому передается текстовое имя интересующего нас типа:

Type t;

t = Type.GetType("Foo");

3. Использовать ключевое слово языка C# typeof:

Type t = typeof(Foo);

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

public class Foo {

private int Field1;

public float Field2;

private int Method1() {

Console.WriteLine("Method1 is called");

return 0;

}

public void Method2(string s, ref int i) {

Console.WriteLine("Method2 is called");

Console.WriteLine("First parameter is " + s);

Console.WriteLine("Second parameter is " + i);

}

public int Prop {

get { return Field1; }

set { Field1 = value; }

}

}

А теперь рассмотрим код, который выводит информацию об элементах типа Foo (для использования типов, подобных FieldInfo, необходимо подключить пространство имен System.Reflection).

Type t = typeof(Foo);

 

//Это общая информация

Console.WriteLine("Full name = " + t.FullName);

Console.WriteLine("Base is = " + t.BaseType);

Console.WriteLine("Is abstract = " + t.IsAbstract);

Console.WriteLine("Is sealed = " + t.IsSealed);

Console.WriteLine("Is class = " + t.IsClass);

Console.WriteLine("******************************");

 

//Сейчас пройдемся по полям

FieldInfo[] fi = t.GetFields();

foreach(FieldInfo f in fi)

Console.WriteLine("Field = " + f.Name);

Console.WriteLine("******************************");

 

//А теперь по свойствам

PropertyInfo[] pi = t.GetProperties();

foreach(PropertyInfo p in pi)

Console.WriteLine("Property = " + p.Name);

Console.WriteLine("******************************");

 

//С методами поработаем подробнее

MethodInfo[] mi = t.GetMethods();

foreach(MethodInfo m in mi) {

Console.WriteLine("Method Name = " + m.Name);

Console.WriteLine("Method Return Type = " + m.ReturnType);

 

//Изучим параметры метода

ParameterInfo[] pri = m.GetParameters();

foreach(ParameterInfo pr in pri) {

Console.WriteLine("Parameter Name = " + pr.Name);

Console.WriteLine("Type = " + pr.ParameterType);

}

Console.WriteLine("******************************");

}

Данный код выводит следующую информацию:

Full name = refl.Foo

Base is = System.Object

Is abstract = False

Is sealed = False

Is class = True

******************************

Field = Field2

******************************

Property = Prop

******************************

Method Name = GetHashCode

Method Return Type = System.Int32

******************************

Method Name = Equals

Method Return Type = System.Boolean

Parameter Name = obj

Parameter Type = System.Object

******************************

Method Name = ToString

Method Return Type = System.String

******************************

Method Name = Method2

Method Return Type = System.Void

Parameter Name = s

Parameter Type = System.String

Parameter Name = i

Parameter Type = System.Int32&

******************************

Method Name = get_Prop

Method Return Type = System.Int32

******************************

Method Name = set_Prop

Method Return Type = System.Void

Parameter Name = value

Parameter Type = System.Int32

******************************

Method Name = GetType

Method Return Type = System.Type

******************************

Обратите внимание, что была получена информация только об открытых членах класса Foo. Кроме этого, информация включала описание собственных и унаследованных элементов класса Foo. Пространство имен System.Reflection содержит специальное перечисление BindingFlags, которое позволяет управлять получаемой о типе информацией.

Таблица 7

Элементы перечисления BindingFlags

Элемент BindingFlags Описание
Default Поиск по умолчанию
IgnoreCase Поиск, не чувствительный к регистру
DeclaredOnly Игнорировать унаследованные члены
Instance Поиск экземплярных членов
Static Поиск статических членов
Public Поиск открытых членов
NonPublic Поиск внутренних членов
FlattenHierarchy Поиск статических членов, заданных в базовых типах

Методы класса Type, подобные GetFields(), имеют перегруженные версии, в которых одним из параметров выступает набор флагов BindingFlags. Изменим приведенный выше код, чтобы получать информацию об открытых и внутренних методах, определенных в самом классе Foo:

BindingFlags bf = BindingFlags.DeclaredOnly |

BindingFlags.Public | BindingFlags.NonPublic |

BindingFlags.Static | BindingFlags.Instance;

MethodInfo[] mi = t.GetMethods(bf);

// Далее по тексту примера...

Некоторые типы пространства System.Reflection, которые могут быть полезны при работе с метаданными, перечисдены в таблице 8.

Таблица 8

Избранные типы пространства имен System.Reflection

Тип Назначение
Assembly Класс для загрузки сборки, изучения ее состава и выполнения операций со сборкой
AssemblyName Класс для получения идентификационной информации о сборке
EventInfo Хранит информацию о событии
FieldInfo Хранит информацию о поле
MemberInfo Абстрактный класс для классов вида *Info
MethodInfo Хранит информацию о методе
Module Позволяет обратиться к модулю в многофайловой сборке
ParameterInfo Хранит информацию о параметре метода
PropertyInfo Хранит информацию о свойстве

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

Для демонстрации позднего связывания поместим класс Foo в динамическую библиотеку с именем FooLib.dll. Основное приложение будет загружать данную библиотеку и исследовать ее типы:

using System;

using System.Reflection;

using System.IO; //Нужно для FileNotFoundException

 

class MainClass {

public static void Main() {

Assembly A = null;

try {

//Используется текстовое имя без расширения

//Файл FooLib.dll находится в директории программы

A = Assembly.Load("FooLib");

} catch (FileNotFoundException e) {

Console.WriteLine(e.Message);

return;

}

foreach(Module M in A.GetModules())

foreach(Type T in M.GetTypes())

// Выводить особо нечего – одна строке Foo

Console.WriteLine(T.Name);

}

}

Позднее связывание не ограничивается загрузкой сборки и изучением состава ее элементов. При помощи позднего связывания можно создать объекты типов, определенных в сборке, а также работать с элементами созданных объектов (например, вызывать методы). Для создания определенного объекта используется метод CreateInstance() класса System.Activator. Существует несколько перегруженных версий данного метода. Можно использовать вариант, при котором в качестве параметра метода используется объект Type или строка-имя типа. Метод возвращает значение типа object. Класс MethodInfo имеет метод Invoke(), который позволяет вызвать метод объекта. Первый параметр метода Invoke() – это тот объект, у которого вызывается метод, второй параметр – массив объектов, представляющих параметры метода.

Представим листинг приложения, которое пытается интерактивно создавать объекты определенных типов и вызывать их методы (для простоты полностью отсутствует обработка исключительных ситуаций):

using System;

using System.Reflection;

using System.IO;

 

class MainClass {

public static void Main() {

//Просим пользователя ввести имя сборки и считываем ее

Console.Write("Enter the name of assembly: ");

string AssemblyName = Console.ReadLine();

Assembly A = Assembly.Load(AssemblyName);

 

//Перечисляем все типы в сборке

//(вернее, проходим по модулям сборки и ищем типы в них)

Console.WriteLine("Assembly {0} has types:", AssemblyName);

foreach(Module M in A.GetModules())

foreach(Type T in M.GetTypes())

Console.WriteLine(T.Name);

 

//Просим пользователя ввести имя типа

Console.Write("Enter namespace.typename: ");

string TypeName = Console.ReadLine();

 

//Запрашиваем указанный тип и создаем его экземпляр

Type UserType = A.GetType(TypeName);

object obj = Activator.CreateInstance(UserType);

 

//Перечисляем методы типа (с применением BindingFlags)

Console.WriteLine("Type {0} has methods:", TypeName);

BindingFlags bf = BindingFlags.DeclaredOnly |

BindingFlags.Public |

BindingFlags.NonPublic |

BindingFlags.Static |

BindingFlags.Instance;

foreach(MethodInfo m in UserType.GetMethods(bf))

Console.WriteLine("Method Name = " + m.Name);

 

//Просим пользователя ввести имя метода

Console.Write("Enter the name of method to call: ");

string MethodName = Console.ReadLine();

 

//Запрашиваем метод и выводим список его параметров

MethodInfo Method = UserType.GetMethod(MethodName);

Console.WriteLine("Method {0} has parameters:",

MethodName);

foreach(ParameterInfo pr in Method.GetParameters()) {

Console.WriteLine("Parameter Name = " + pr.Name);

Console.WriteLine("Parameter Type = " +

pr.ParameterType);

Console.WriteLine("****************************");

}

 

// Создаем пустой массив для фактических параметров

object[] paramArray =

new object[Method.GetParameters().Length];

//Вызываем метод

Method.Invoke(obj, paramArray);

}

}

2.2. ПОЛЬЗОВАТЕЛЬСКИЕ И ВСТРОЕННЫЕ АТРИБУТЫ

Помимо метаданных в платформе .NET представлена система атрибутов. Атрибуты позволяют определить дополнительную информацию, связанную с элементом метаданных и предоставляют механизм для обращения к этой информации в ходе выполнения программы. Следовательно, атрибуты позволяют динамически изменять поведение программы на основе анализа этой дополнительной информации.

Все атрибуты можно разделить на четыре группы:

1. Атрибуты, используемые компилятором. Информация, предоставляемая этими атрибутами, используется компилятором для генерации конечного кода (атрибуты выступают как своеобразные директивы компилятора).

2. Атрибуты, используемые средой исполнения.

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

4. Пользовательские атрибуты. Это атрибуты, созданные программистом.

Остановимся подробно на процессе создания пользовательского атрибута. Любой атрибут (в том числе и пользовательский) является классом. К классу атрибута предъявляются следующие требования: он должен быть потомком класса System.Attribute, имя класса должно заканчиваться суффиксом Attribute[2], атрибут должен иметь public-конструктор, на тип параметров конструктора атрибута, а также на тип его открытых полей и свойств наложены ограничения (тип может быть не произвольным, а только типом из определенного набора).

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

using System;

public class AuthorAttribute : Attribute {

private string fName;

private string fDate;

public AuthorAttribute(string name) {

fName = name;

}

public string Name {

get { return fName; }

}

public string CreationDate {

get { return fDate; }

set { fDate = value;}

}

}

Данный класс можно скомпилировать и поместить в отдельную сборку (оформленную в виде DLL) или отдельный модуль (.netmodule).

Далее мы можем применить созданный атрибут к произвольному классу:

using System;

//Применяем атрибут

//(Не забудьте добавить ссылку на сборку с атрибутом!)

[AuthorAttribute("Volosevich")]

class MainClass {

. . .

}

Рассмотрим синтаксис использования атрибутов подробнее. Атрибуты записываются в квадратных скобках. Можно записать несколько атрибутов через запятую в виде списка. Список атрибутов должен находиться перед тем элементом, к которому этот список применяется. Если возникает неоднозначность трактовки применения атрибута, то возможно использование специальных модификаторов – assembly, module, field, event, method, param, property, return, type. Например, запись вида [assembly: Имя_атрибута] означает применение атрибута к сборке. Если атрибут применяется к сборке или модулю, то он должен быть записан после секций импортирования using, но перед основным кодом. После имени атрибута указываются в круглых скобках параметры конструктора атрибута. Если конструктор атрибута не имеет параметров, круглые скобки можно не указывать. Для сокращения записи разрешено указывать имя атрибута без суффикса Attribute.

Наряду с параметрами конструктора при применении атрибута можно указать именованные параметры, предназначенные для задания значения открытого поля или свойства. При этом используется синтаксис Имя_поля_или_свойства = Значение-константа. Именованные параметры всегда записываются в конце списка параметров. Если некое поле или свойство инициализируется через параметр конструктора и через именованный параметр, то конечное значение элемента – это значение именованного параметра.

Таким образом, описанный нами ранее атрибут может быть использован для класса в следующем виде:

[Author("Volosevich", CreationDate = "18.03.2005")]

class MainClass {

. . .

}

Ранее упоминалось, что на тип параметров конструктора атрибута, а также на тип его открытых полей и свойств наложены определенный ограничения. Тип параметров, указываемых при использовании атрибута, ограничен следующим набором: типы bool, byte, char, short, int, long, float, double, string; тип System.Type; перечисления; тип object (фактическим параметром в этом случае должна быть константа одного из типов, перечисленного выше); одномерные массивы перечисленных выше типов. Этот набор ограничивает типы для открытых свойств и полей класса атрибута. Закрытые элементы атрибута могут иметь произвольный тип.

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

Таблица 9

Значения параметра конструктора класса AttributeUsageAttribute

Значение параметра Атрибут может применяться к …
AttributeTargets.All любому элементу, указанному далее
AttributeTargets.Assembly сборке
AttributeTargets.Class классу
AttributeTargets.Constructor конструктору
AttributeTargets.Delegate делегату
AttributeTargets.Enum перечислению
AttributeTargets.Event событию
AttributeTargets.Field полю
AttributeTargets.Interface интерфейсу
AttributeTargets.Method методу
AttributeTargets.Module модулю
AttributeTargets.Parameter параметру метода
AttributeTargets.Property свойству
AttributeTargets.ReturnValue возвращаемому значению функции
AttributeTargets.Struct структуре

Свойство AllowMultiple класса AttributeUsageAttribute определяет, может ли быть атрибут применен к программному элементу более одного раза. Тип данного свойства – bool, значение по умолчанию – false.

Свойство Inherited класса AttributeUsageAttribute определяет, будет ли атрибут проецироваться на потомков программного элемента. Тип данного свойства – bool, значение по умолчанию – true.

Используем возможности класса AttributeUsageAttribute при описании пользовательского атрибута:

// Атрибут Author можно применить к классу или методу,

// причем несколько раз

[AttributeUsage(AttributeTargets.Class |

AttributeTargets.Method,

AllowMultiple = true)]

public class AuthorAttribute : Attribute {

. . .

}

Опишем возможности получения информации об атрибутах. Для этой цели можно использовать метод GetCustomAttribute() класса System.Attribute. Имеется несколько перегруженных версий данного метода. Мы рассмотрим только одну из версий:

public static Attribute GetCustomAttribute(MemberInfo element,

Type attributeType)

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

using System;

 

[Author("Volosevich", CreationDate = "18.03.2005")]

class SomeClass {

. . .

}

 

class MainClass {

public static void Main() {

Attribute A = Attribute.GetCustomAttribute(

typeof(SomeClass),

typeof(AuthorAttribute));

if (A != null)

Console.WriteLine(((AuthorAttribute)A).Name);

}

}

В данном примере к классу SomeClass был применен пользовательский атрибут AuthorAttribute. Затем в методе другого класса этот атрибут был прочитан, и из него извлеклась информация.

Следует иметь в виду, что объект, соответствующий классу атрибута, создается исполняющей средой только в тот момент, когда из атрибута извлекается информация. Задание атрибута перед некоторым элементом к созданию объекта не приводит. Количество созданных экземпляров атрибута равно количеству запросов к данным атрибута[3].

Метод Attribute.GetCustomAttributes() позволяет получить все атрибуты некоторого элемента в виде массива объектов. Одна из перегруженных версий данного метода описана следующим образом:

public static Attribute[] GetCustomAttributes(MemberInfo element);

Модифицируем предыдущий пример, использовав GetCustomAttributes:

using System;

 

[Author("Volosevich"), Author("Ivanov")]

class SomeClass {

. . .

}

 

class MainClass {

public static void Main() {

Attribute[] A;

A = Attribute.GetCustomAttributes(typeof(SomeClass));

for(int i = 0; i < A.Length; i++)

if(A[i] is AuthorAttribute)

Console.WriteLine(((AuthorAttribute)A[i]).Name);

}

}

Платформа .NET предоставляет для использования обширный набор атрибутов, некоторые из которых представлены в таблице 10:

Таблица 10

Некоторые атрибуты, применяемые в .NET Framework

Имя атрибута Область применения Описание
AttributeUsage Класс Задает область применения класса-атрибута
Conditional Метод Компилятор может игнорировать вызовы помеченного метода при заданном условии
DllImport Метод Указывает DLL, содержащую реализацию метода
MTAThread Метод (Main) Для приложения используется модель COM Multithreaded apartment
NonSerialized Поле Указывает, что поле не будет сериализовано
Obsolete Любая, исключая assembly, module, param, return Информирует, что в будущих реализациях данный элемент может отсутствовать
ParamArray Параметр Позволяет одиночному параметру быть обработанным как набор параметров params
Serializable Класс, структура, перечисление, делегат Указывает, что все поля типа могут быть сериализованы
STAThread Метод (Main) Для приложения используется модель COM Single-threaded apartment
StructLayout Класс, структура Задает схему размещения данных класса или структуры в памяти (Auto, Explicit, Sequential)
ThreadStatic Статическое поле В каждом потоке будет использоваться собственная копия данного статического поля

Атрибут DllImport предназначен для импортирования функций из библиотек динамической компоновки, написанных на «родном» языке процессора. В следующем примере данный атрибут используется для импортирования системной функции MessageBoxA():

using System;

using System.Runtime.InteropServices;

 

class MainClass {

[DllImport("user32.dll")]

public static extern int MessageBoxA(int h, string m,

string c, int type);

public static void Main() {

MessageBoxA(0, "Hello World", "nativeDLL", 0);

}

}

Обратите внимание, что для использования атрибута DllImport требуется подключить пространство имен System.Runtime.InteropServices. Кроме это, необходимо объявить импортируемую функцию статической и пометить ее модификатором extern. Атрибут DllImport допускает использование дополнительных параметров, подробное описание которых можно найти в документации.

Исполняемая среда .NET выполняет корректную передачу параметров примитивных типов между управляемым и неуправляемым кодом. Для правильной передачи параметров-структур требуется использование специального атрибута StructLayout при объявлении пользовательского типа, соответствующего структуре. Например, пусть выполняется экспорт системной функции GetLocalTime():

[DllImport("kernel32.dll")]

public static extern void GetLocalTime(SystemTime st);

В качестве параметра используется объект класса SystemTime. Этот класс должен быть описан следующим образом:

[StructLayout(LayoutKind.Sequential)]

public class SystemTime {

public ushort wYear;

public ushort wMonth;

public ushort wDayOfWeek;

public ushort wDay;

public ushort wHour;

public ushort wMinute;

public ushort wSecond;

public ushort wMilliseconds;

}

Атрибут StructLayout указывает, что поля объекта должны быть расположены в памяти в точности так, как это записано в объявлении класса (LayoutKind.Sequential). В противном случае при работе с системной функцией возможно возникновение ошибок.

Атрибут Conditional (описан в пространстве имен System.Diagnostics) может быть применен к любому методу, возвращающему значение void. Параметром атрибута является строковый литерал. Применение атрибута указывает компилятору, что вызовы помеченного метода следует опускать, если в проекте не определен строковый литерал. Задание литерала производится либо директивой препроцессора #define в начале текста программы (#define D), либо параметром компилятора /define:<symbol list>, либо в диалогах настройки интегрированной среды.

Рассмотрим пример использования атрибута Conditional:

using System;

using System.Diagnostics;

 

class MainClass {

[Conditional("D")]

public static void SomeDebugFunc() {

Console.WriteLine("SomeDebugFunc");

}

public static void Main() {

SomeDebugFunc();

Console.WriteLine("Hello!");

}

}

Если в проекте не определен параметр D (а так, естественно, и будет по умолчанию), вызова метода SomeDebugFunc() не произойдет. В принципе, подобный эффект достигается использованием директив препроцессора:

public static void Main() {



<== предыдущая лекция | следующая лекция ==>
ВВЕДЕНИЕ | 


Карта сайта Карта сайта укр


Уроки php mysql Программирование

Онлайн система счисления Калькулятор онлайн обычный Инженерный калькулятор онлайн Замена русских букв на английские для вебмастеров Замена русских букв на английские

Аппаратное и программное обеспечение Графика и компьютерная сфера Интегрированная геоинформационная система Интернет Компьютер Комплектующие компьютера Лекции Методы и средства измерений неэлектрических величин Обслуживание компьютерных и периферийных устройств Операционные системы Параллельное программирование Проектирование электронных средств Периферийные устройства Полезные ресурсы для программистов Программы для программистов Статьи для программистов Cтруктура и организация данных


 


Не нашли то, что искали? Google вам в помощь!

 
 

© life-prog.ru При использовании материалов прямая ссылка на сайт обязательна.

Генерация страницы за: 0.159 сек.