Skip to content

Latest commit

 

History

History
1948 lines (1691 loc) · 111 KB

c17.md

File metadata and controls

1948 lines (1691 loc) · 111 KB

ГЛАВА 17. Динамическая идентификация типов, рефлексия и атрибуты

В этой главе рассматриваются три эффективных сред­ ства: динамическая идентификация типов, рефлексия и атрибуты. Динамическая идентификация типов пред­ ставляет собой механизм, позволяющий определить тип данных во время выполнения программы. Рефлексия — это средство для получения сведений о типе данных. Используя эти сведения, можно конструировать и применять объекты во время выполнения. Это довольно эффективное средство, поскольку оно дает возможность расширять функции про­ граммы динамически, т.е. в процессе ее выполнения. Атри­ бут описывает характеристики определенного элемента программы на С#. Атрибуты можно, в частности, указать для классов, методов и полей. Во время выполнения про­ граммы разрешается опрашивать атрибуты для получения сведений о них. Для этой цели в атрибутах используется динамическая идентификация типов и рефлексия.

Динамическая идентификация типов

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

Для поддержки динамической идентификации типов в С# предусмотрены три ключевых слова: is, as и typeof. Каждое из этих ключевых слов рассматривается да­ лее по очереди.

Проверка типа с помощью оператора is

Конкретный тип объекта можно определить с помощью оператора is. Ниже при­ ведена его общая форма:

выражение is тип

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

Ниже приведен пример применения оператора is.

// Продемонстрировать применение оператора is.
using System;

class A {}

class В : A {}

class UseIs {
    static void Main() {
        A a = new A();
        В b = new В();

        if(a is A)
            Console.WriteLine("а имеет тип A");
        if(b is A)
            Console.WriteLine("b совместим с А, поскольку он производный от А");
        if(a is В)
            Console.WriteLine("Не выводится, поскольку а не производный от В");

        if(b is В)
            Console.WriteLine("В имеет тип В");
        if(a is object)
            Console.WriteLine("а имеет тип object");
    }
}

Вот к какому результату приводит выполнение этого кода.

а имеет тип А
b совместим с А, поскольку он производный от А
b имеет тип В
а имеет тип object

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

if(b is A)
    Console.WriteLine("b совместим с А, поскольку он производный от А");

Условный оператор if выполняется, поскольку b является объектом типа В, произ­ водным от типа А. Но обратное несправедливо. Так, если в строке кода

if(a is В)
    Console.WriteLine("Не выводится, поскольку а не производный от В");

условный оператор if не выполняется, поскольку а является объектом типа А, не про­ изводного от типа В. Поэтому а не относится к типу В.

Применение оператора as

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

выражение as тип

где выражение обозначает отдельное выражение, преобразуемое в указанный тип. Если исход такого преобразования оказывается удачным, то возвращается ссылка на тип, а иначе — пустая ссылка. Оператор as может использоваться только для преоб­ разования ссылок, идентичности, упаковки, распаковки.

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

// Использовать оператор is для предотвращения неправильного приведения типов.
using System;

class А {}

class В : А {}

class CheckCast {
    static void Main() {
        A a = new A();
        В b = new В();
        // Проверить, можно ли привести а к типу В.
        if(a is В) // если да, то выполнить приведение типов
            b = (В) а;
        else // если нет, то пропустить приведение типов
            b = null;

        if(b==null)
            Console.WriteLine("Приведение типов b = (В) HE допустимо.");
        else
            Console.WriteLine("Приведение типов b = (В) допустимо.");
    }
}

Эта программа дает следующий результат.

Приведение типов b = (В) НЕ допустимо.

Как следует из результата выполнения приведенной выше программы, тип объекта а не совместим с типом В, и поэтому его приведение к типу В не допустимо и предот­ вращается в условном операторе if. Но такую проверку приходится выполнять в два этапа. Сначала требуется убедиться в обоснованности операции приведения типов, а затем выполнить ее. Оба этапа могут быть объединены в один с помощью оператора as, как демонстрирует приведенная ниже программа.

// Продемонстрировать применение оператора as.
using System;

class A {}

class В : A {}

class CheckCast {
    static void Main() {
        A a = new A();
        В b = new В();

        b = a as В; // выполнить приведение типов, если это возможно

        if(b==null)
            Console.WriteLine("Приведение типов b = (В) НЕ допустимо.");
        else
            Console.WriteLine("Приведение типов b = (В) допустимо.");
    }
}

Эта программа дает прежний результат.

Приведение типов b = (В) НЕ допустимо.

В данном варианте программы в одном и том же операторе as сначала проверяется обоснованность операции приведения типов, а затем выполняется сама операция при­ ведения типов, если она допустима.

Применение оператора typeof

Несмотря на всю свою полезность, операторы as и is проверяют лишь совмести­ мость двух типов. Но зачастую требуется информация о самом типе. Для этой цели в С# предусмотрен оператор typeof. Он извлекает объект класса System.Туре для за­ данного типа. С помощью этого объекта можно определить характеристики конкрет­ ного типа данных. Ниже приведена общая форма оператора typeof:

typeof(тип)

где тип обозначает получаемый тип. Информация, описывающая тип, инкапсулиру­ ется в возвращаемом объекте класса Туре.

Получив объект класса Туре для заданного типа, можно извлечь информацию о нем, используя различные свойства, поля и методы, определенные в классе Туре. Класс Туре довольно обширен и содержит немало членов, поэтому его рассмотрение придется отложить до следующего раздела, посвященного рефлексии. Но в качестве краткого введения в этот класс ниже приведена программа, в которой используются три его свойства: FullName, IsClass и IsAbstract. Для получения полного имени типа служит свойство FullName. Свойство IsClass возвращает логическое значение true, если тип относится к классу. А свойство IsAbstract возвращает логическое зна­ чение true, если класс является абстрактным.

// Продемонстрировать применение оператора typeof.
using System;
using System.IO;

class UseTypeof {
    static void Main() {
    Type t = typeof(StreamReader);
    Console.WriteLine(t.FullName);
    if(t.IsClass) Console.WriteLine("Относится к классу.");
    if(t.IsAbstract) Console.WriteLine("Является абстрактным классом.");
    else Console.WriteLine("Является конкретным классом.");
    }
}

Эта программа дает следующий результат.

System.IO.StreamReader
Относится к классу.
Является конкретным классом.

В данной программе сначала извлекается объект класса Туре, описывающий тип StreamReader. Затем выводится полное имя этого типа данных и определяется его принадлежность к классу, а далее — к абстрактному или конкретному классу.

Рефлексия

Рефлексия — это средство, позволяющее получать сведения о типе данных. Термин рефлексия, или отражение, происходит от принципа действия этого средства: объект класса Туре отражает базовый тип, который он представляет. Для получения инфор­ мации о типе данных объекту класса Туре делаются запросы, а он возвращает (отра­ жает) обратно информацию, связанную с определяемым типом. Рефлексия является эффективным механизмом, поскольку она позволяет выявлять и использовать возмож­ ности типов данных, известные только во время выполнения.

Многие классы, поддерживающие рефлексию, входят в состав прикладного интер­ фейса .NET Reflection API, относящегося к пространству имен System.Reflection. Поэтому для применения рефлексии в код программы обычно вводится следующая строка.

using System.Reflection;

Класс System.Type - ядро подсистемы рефлексии

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

В классе MemberInfo определены приведенные ниже свойства, доступные только для чтения.

Свойство Описание
Type DeclaringType Тип класса или интерфейса, в котором объявляется отражаемый член
MemberTypes MemberTypesТип члена. Это значение обозначает, является ли член полем, методом, свойством, событием или конструктором
int MetadataToken Значение, связанное к конкретными метаданными
Module Module Объект типа Module, представляющий модуль (исполняемый файл), в котором находится отражаемый тип
string Name Имя типа
Type ReflectedType Тип отражаемого объекта

Следует иметь в виду, что свойство MemberType возвращает тип MemberTypes — перечисление, в котором определяются значения, обозначающие различные типы чле­ нов. К их числу относятся следующие.

MemberTypes.Constructor
MemberTypes.Method
MemberTypes.Field
MemberTypes.Event
MemberTypes.Property

Следовательно, тип члена можно определить, проверив свойство MemberType. Так, если свойство MemberType имеет значение MemberTypes.Method, то проверяемый член является методом.

В класс MemberInfo входят два абстрактных метода: GetCustomAttributes() и IsDefined(). Оба метода связаны с атрибутами. Первый из них получает список специальных атрибутов, имеющих отношение к вызывающему объекту, а второй уста­ навливает, определен ли атрибут для вызывающего метода. В версию .NET Framework Version 4.0 внедрен метод GetCustomAttributesData(), возвращающий сведения о специальных атрибутах. (Подробнее об атрибутах речь пойдет далее в этой главе.)

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

Метод Назначение
ConstructorInfo[] GetConstructors() Получает список конструкторов для заданного типа
EventInfo[] GetEvents() Получает список событий для заданного типа
FieldInfo[] GetFields() Получает список полей для заданного типа
Type[] GetGenericArguments() Получает список аргументов типа, связанных с закрыто сконструированным обобщенным типом, или же список параметров типа, если заданный тип определен как обобщенный. Для открыто сконструированного типа этот список может содержать как аргументы, так и параметры типа. (Более подробно обобщения рассматриваются в главе 18.)
MemberInfо[] GetMembers() Получает список членов для заданного типа
MethodInfo[] GetMethods() Получает список методов для заданного типа
PropertyInfo[] GetProperties() Получает список свойств для заданного типа

Далее приведен ряд наиболее часто используемых свойств, доступных только для чтения и определенных в классе Туре.

Свойство Назначение
Assembly Assembly Получает сборку для заданного типа
TypeAttributes Attributes Получает атрибуты для заданного типа
Type BaseType Получает непосредственный базовый тип для заданного типа
string FullName Получает полное имя заданного типа
bool IsAbstract Истинно, если заданный тип является абстрактным
bool IsArray Истинно, если заданный тип является массивом
bool IsClass Истинно, если заданный тип является классом
bool IsEnum Истинно, если заданный тип является перечислением
bool IsGenericParameter Истинно, если заданный тип является параметром обобщенного типа. (Более подробно обобщения рассматриваются в главе 18.)
bool IsGenericType Истинно, если заданный тип является обобщенным. (Более подробно обобщения рассматриваются в главе 18.)
string Namespace Получает пространство имен для заданного типа

Применение рефлексии

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

Прикладной интерфейс Reflection API весьма обширен и поэтому не может быть полностью рассмотрен в этой главе. Ведь для этого потребовалась бы целая книга! Но прикладной интерфейс Reflection API имеет ясную логическую структуру, а следова­ тельно, уяснив одну его часть, нетрудно понять и все остальное. Принимая во внима­ ние это обстоятельство, в последующих разделах демонстрируются четыре основных способа применения рефлексии: получение сведений о методах, вызов методов, кон­ струирование объектов и загрузка типов данных из сборок.

Получение сведений о методах

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

MethodInfo[] GetMethods()

Этот метод возвращает массив объектов класса MethodInfo, которые описывают методы, поддерживаемые вызывающим типом. Класс MethodInfo находится в про­ странстве имен System.Reflection.

Класс MethodInfo является производным от абстрактного класса MethodBase, ко­ торый в свою очередь наследует от класса MemberInfо. Это дает возможность пользо­ ваться всеми свойствами и методами, определенными в этих трех классах. Например, для получения имени метода служит свойство Name. Особый интерес вызывают два члена класса MethodInfo:ReturnType и GetParameters().

Возвращаемый тип метода находится в доступном только для чтения свойстве ReturnType, которое является объектом класса Туре.

Метод GetParameters() возвращает список параметров, связанных с анализируе­ мым методом. Ниже приведена его общая форма.

ParameterInfо[] GetParameters();

Сведения о параметрах содержатся в объекте класса ParameterInfо. В классе ParameterInfо определено немало свойств и методов, описывающих параметры. Особое значение имеют два свойства: Name — представляет собой строку, содержащую имя параметра, a ParameterType — описывает тип параметра, который инкапсули­ рован в объекте класса Туре.

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

// Анализ методов с помощью рефлексии.
using System;
using System.Reflection;

class MyClass {
	int x;
	int y;

	public MyClass(int i, int j) {
		x = i;
		у = j;
	}

	public int Sum() {
		return x+y;
	}

	public bool IsBetween(int i) {
		if(x < i && i < y) return true;
		else return false;
	}

	public void Set(int a, int b) {
		x = a;
		у = b;
	}

	public void Set(double a, double b) {
		x = (int) a;
		y = (int) b;
	}

	public void Show() {
		Console.WriteLine(" x: {0}, у: {1}", x, y);
	}
}

class ReflectDemo {
	static void Main() {
		Type t = typeof(MyClass); // получить объект класса Type,
								// представляющий класс MyClass

		Console.WriteLine("Анализ методов, определенных " +
						"в классе " + t.Name);
		Console.WriteLine();

		Console.WriteLine("Поддерживаемые методы: ");

		MethodInfo[] mi = t.GetMethods();

		// Вывести методы, поддерживаемые в классе MyClass.
		foreach(MethodInfo m in mi) {
			// Вывести возвращаемый тип и имя каждого метода.
			Console.Write(" " + m.ReturnType.Name + " " + m.Name + "(");

			// Вывести параметры.
			ParameterInfo[] pi = m.GetParameters();
			for(int i=0; i < pi.Length; i++) {
				Console.Write(pi[i].ParameterType.Name + " " + pi[i],Name);
				if(i+1 < pi.Length) Console.Write(", ");
			}
			Console.WriteLine(")");

			Console.WriteLine();
		}
	}
}

Эта программа дает следующий результат.

Анализ методов, определенных в классе MyClass

Поддерживаемые методы:
Int32 Sum()

Boolean IsBetween(Int32 i)

Void Set(Int32 a, Int32 b)

Void Set(Double a, Double b)

Void Show()

String ToString()

Boolean Equals(Object obj)

Int32 GetHashCode()

Type GetType()

Как видите, помимо методов, определенных в классе MyClass, в данной программе выводятся также методы, определенные в классе object, поскольку все типы данных в C# наследуют от класса object. Кроме того, в качестве имен типов указываются имена структуры .NET. Обратите также внимание на то, что метод Set() выводится дважды, поскольку он перегружается. Один из его вариантов принимает аргументы типа int, а другой — аргументы типа double.

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

Объект класса Туре, представляющий класс MyClass, создается в методе Main() в следующей строке кода.

Type t = typeof(MyClass); // получить объект класса Туре,
						// представляющий класс MyClass

Напомним, что оператор typeof возвращает объект класса Туре, представляющий конкретный тип данных (в данном случае — класс MyClass).

С помощью переменной t и прикладного интерфейса Reflection API в данной программе затем выводятся сведения о методах, поддерживаемых в классе MyClass. Для этого в приведенной ниже строке кода сначала выводится список соответствую­ щих методов.

MethodInfo[] mi = t.GetMethods();

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

foreach(MethodInfo m in mi) {
	// Вывести возвращаемый тип и имя каждого метода.
	Console.Write(" " + m.ReturnType.Name + " " + m.Name + "(");

	// Вывести параметры.
	ParameterInfo[] pi = m.GetParameters();
	for(int i=0; i < pi.Length; i++) {
		Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name);
		if(i+1 < pi.Length) Console.Write(", ");
	}
}

В этом фрагменте кода параметры, связанные с каждым методом, сначала создают­ ся с помощью метода GetParameters() и сохраняются в массиве pi. Затем в цикле for происходит обращение к элементам массива pi и выводится тип и имя каждо­ го параметра. Самое главное, что все эти сведения создаются динамически во время выполнения программы, не опираясь на предварительную осведомленность о классе MyClass.

Вторая форма метода GetMethods()

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

MethodInfo[] GetMethods(BindingFlags флажки)

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

Значение Описание
DeclaredOnly Извлекаются только те методы, которые определены в заданном классе. Унаследованные методы в извлекаемые сведения не включаются
Instance Извлекаются методы экземпляра
NonPublic Извлекаются методы, не являющиеся открытыми
Public Извлекаются открытые методы
Static Извлекаются статические методы

Два или несколько флажков можно объединить с помощью логической операции ИЛИ. Но как минимум флажок Instance или Static следует указывать вместе с флажком Public или NonPublic. В противном случае не будут извлечены сведения ни об одном из методов.

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

// Теперь получаются сведения только о тех методах,
// которые объявлены в классе MyClass.
MethodInfo[] mi = t.GetMethods(BindingFlags.DeclaredOnly |
	BindingFlags.Instance |
	BindingFlags.Public);

После этой замены программа дает следующий результат.

Анализ методов, определенных в классе MyClass

Поддерживаемые методы:
Int32 Sum()

Boolean IsBetween(Int32 i)

Void Set(Int32 a, Int32 b)

Void Set(Double a, Double b)

Void Show()

Как видите, теперь выводятся только те методы, которые явно определены в классе MyClass.

Вызов методов с помощью рефлексии

Как только методы, поддерживаемые определенным типом данных, становятся из­ вестны, их можно вызывать. Для этой цели служит метод Invoke(), входящий в со­ став класса MethodInfo. Ниже приведена одна из форм этого метода:

object Invoke(object obj, object[] parameters)

где obj обозначает ссылку на объект, для которого вызывается метод. Для вызова стати­ ческих методов (static) в качестве параметра obj передается пустое значение (null). Любые аргументы, которые должны быть переданы методу, указываются в массиве parameters. Если же аргументы не нужны, то вместо массива parameters указыва­ ется пустое значение (null). Кроме того, количество элементов массива parameters должно точно соответствовать количеству передаваемых аргументов. Так, если требует­ ся передать два аргумента, то массив parameters должен состоять из двух элементов, но не из трех или четырех. Значение, возвращаемое вызываемым методом, передается методу Invoke(), который и возвращает его.

Для вызова конкретного метода достаточно вызвать метод Invoke() для экзем­ пляра объекта типа MethodInfo, получаемого при вызове метода GetMethods(). Эта процедура демонстрируется в приведенном ниже примере программы.

// Вызвать методы с помощью рефлексии.
using System;
using System.Reflection;

class MyClass {
	int x;
	int y;

	public MyClass(int i, int j) {
		x = i;
		У = j;
	}

	public int Sum() {
		return x+y;
	}

	public bool IsBetween(int i) {
		if((x < i) && (i < y)) return true;
		else return false;
	}

	public void Set(int a, int b) {
		Console.Write("В методе Set(int, int). ");
		x = a;
		У = b;
		Show();
	}

	// Перегрузить метод Set.
	public void Set(double a, double b) {
		Console.Write("В методе Set(double, double). ");
		x = (int) a;
		у = (int) b;
		Show();
	}

	public void Show() {
		Console.WriteLine("Значение x: {0}, значение у: {1}", x, у);
	}
}

class InvokeMethDemo {
	static void Main() {
		Type t = typeof(MyClass);
		MyClass reflectOb = new MyClass(10, 20);
		int val;

		Console.WriteLine("Вызов методов, определенных в классе " + t.Name);
		Console.WriteLine();
		MethodInfo[] mi = t.GetMethods();

		// Вызвать каждый метод.
		foreach(MethodInfo m in mi) {
			// Получить параметры.
			ParameterInfo[] pi = m.GetParameters();

			if(m.Name.CompareTo("Set")==0 &&
					pi[0].ParameterType == typeof(int)) {
				object[] args = new object[2];
				args[0] = 9;
				args[1] = 18;
				m.Invoke(reflectOb, args);
			}
			else if(m.Name.CompareTo("Set")==0 &&
					pi[0].ParameterType == typeof(double)) {
				object[] args = new object[2];
				args[0] = 1.12;
				args[1] = 23.4;
				m.Invoke(reflectOb, args);
			}
			else if(m.Name.CompareTo("Sum")==0) {
				val = (int) m.Invoke(reflectOb, null);
				Console.WriteLine("Сумма равна " + val);
			}
			else if(m.Name.CompareTo("IsBetween")==0) {
				object[] args = new object[1];
				args[0] = 14;
				if((bool) m.Invoke(reflectOb, args))
				Console.WriteLine("Значение 14 находится между x и у");
			}
			else if(m.Name.CompareTo("Show")==0) {
				m.Invoke(reflectOb, null);
			}
		}
	}
}

Вот к какому результату приводит выполнение этой программы.

Вызов методов, определенных в классе MyClass

Сумма равна 30
Значение 14 находится между х и у
В методе Set(int, int). Значение х: 9, значение у: 18
В методе Set(double, double). Значение х: 1, значение у: 23
Значение х: 1, значение у: 23

Рассмотрим подробнее порядок вызова методов. Сначала создается список методов. Затем в цикле foreach извлекаются сведения об их параметрах. Далее каждый метод вызывается с указанием соответствующего типа и числа аргументов в последователь­ ном ряде условных операторов if/else. Обратите особое внимание на перегрузку ме­ тода Set() в приведенном ниже фрагменте кода.

if(m.Name.CompareTo("Set")==0 &&
		pi[0].ParameterType == typeof(int)) {
	object[] args = new object[2];
	args[0] = 9;
	args[1] = 18;
	m.Invoke(reflectOb, args);
}
else if(m.Name.CompareTo("Set")==0 &&
		pi[0].ParameterType == typeof(double)) {
	object[] args = new object[2];
	args[0] = 1.12;
	args[1] = 23.4;
	m.Invoke(reflectOb, args);
}

Если имя метода — Set, то проверяется тип первого параметра, чтобы выявить конкретный вариант этого метода. Так, если это метод Set(int, int), то его аргу­ менты загружаются в массив args. В противном случае используются аргументы типа double.

Получение конструкторов конкретного типа

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

Конструкторы конкретного типа получаются при вызове метода GetConstructors() для объекта класса Туре. Ниже приведена одна из наиболее ча­ сто используемых форм этого метода.

ConstructorInfо[] GetConstructors()

Метод GetConstructors() возвращает массив объектов класса ConstructorInfо, описывающих конструкторы.

Класс ConstructorInfo является производным от абстрактного класса MethodBase, который в свою очередь наследует от класса MemberInfо. В нем также определен ряд собственных методов. К их числу относится интересующий нас метод GetConstructors(), возвращающий список параметров, связанных с конструк­ тором. Этот метод действует таким же образом, как и упоминавшийся ранее метод GetParameters(), определенный в классе MethodInfo.

Как только будет обнаружен подходящий конструктор, для создания объекта вызы­ вается метод Invoke(), определенный в классе ConstructorInfo. Ниже приведена одна из форм этого метода.

object Invoke(object[] parameters)

Любые аргументы, которые требуется передать методу, указываются в масси­ ве parameters. Если же аргументы не нужны, то вместо массива parameters ука­ зывается пустое значение (null). Но в любом случае количество элементов массива parameters должно совпадать с количеством передаваемых аргументов, а типы аргу­ ментов — с типами параметров. Метод Invoke() возвращает ссылку на сконструиро­ ванный объект.

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

// Создать объект с помощью рефлексии.
using System;
using System.Reflection;

class MyClass {
	int x;
	int y;

	public MyClass(int i) {
		Console.WriteLine("Конструирование класса MyClass(int, int). ");
		x = у = i;
	}

	public MyClass(int i, int j) {
		Console.WriteLine("Конструирование класса MyClass(int, int). ");
		x = i;
		У = j;
		Show();
	}

	public int Sum() {
		return x+y;
	}

	public bool IsBetween(int i) {
		if((x < i) && (i < y)) return true;
		else return false;
	}

	public void Set(int a, int b) {
		Console.Write("В методе Set(int, int). ");
		x = a;
		У = b;
		Show();
	}

	// Перегрузить метод Set.
	public void Set(double a, double b) {
		Console.Write("В методе(double, double). ");
		x = (int) a;
		у = (int) b;
		Show();
	}

	public void Show() {
		Console.WriteLine("Значение x: {0}, значение у: {1}", x, у);
	}
}

class InvokeConsDemo {
	static void Main() {
		Type t = typeof(MyClass);
		int val;

		// Получить сведения о конструкторе.
		ConstructorInfо[] ci = t.GetConstructors();
		Console.WriteLine("Доступные конструкторы: ");

		foreach(ConstructorInfo с in ci) {
			// Вывести возвращаемый тип и имя.
			Console.Write(" " + t.Name + "(");

			// Вывести параметры.
			ParameterInfо[] pi = с.GetParameters();
			for(int i=0; i-< pi.Length; i++) {
				Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name);
				if(i+1 < pi.Length) Console.Write(", ");
			}

			Console.WriteLine (")");
		}
		Console.WriteLine();

		// Найти подходящий конструктор.
		int х;
		for(x=0; х < ci.Length; х++) {
			ParameterInfо[] pi = ci[х].GetParameters();
			if(pi.Length == 2) break;
		}

		if(x == ci.Length) {
			Console.WriteLine("Подходящий конструктор не найден.");
			return;
		}
		else
			Console.WriteLine("Найден конструктор с двумя параметрами.\n");

		// Сконструировать объект.
		object[] consargs = new object[2];
		consargs[0] = 10;
		consargs[1] = 20;
		object reflectOb = ci[x].Invoke(consargs);
		Console.WriteLine("\nВызов методов для объекта reflectOb.");
		Console.WriteLine();

		MethodInfo[] mi = t.GetMethods();
		// Вызвать каждый метод.
		foreach(MethodInfo m in mi) {
			// Получить параметры.
			ParameterInfо[] pi = m.GetParameters();
			if(m.Name.CompareTo("Set")==0 &&
			pi[0].ParameterType == typeof(int)) {
				// Это метод Set(int, int).
				object[] args = new object[2];
				args[0] = 9;
				args[1] = 18;
				m.Invoke(reflectOb, args);
			}
			else if(m.Name.CompareTo("Set")==0 &&
			pi[0].ParameterType == typeof(double)) {
				// Это метод Set(double, double).
				object[] args = new object[2];
				args[0] = 1.12;
				args[1] = 23.4;
				m.Invoke(reflectOb, args);
			}
			else if(m.Name.CompareTo("Sum")==0) {
				val = (int) m.Invoke(reflectOb, null);
				Console.WriteLine("Сумма равна " + val);
			}
			else if(m.Name.CompareTo("IsBetween")==0) {
				object[] args = new object[1];
				args[0] = 14;
				if((bool) m.Invoke(reflectOb, args))
				Console.WriteLine("Значение 14 находится между x и у");
			}
			else if(m.Name.CompareTo("Show")==0) {
				m.Invoke(reflectOb, null);
			}
		}
	}
}

Эта программа дает следующий результат.

Доступные конструкторы:
	MyClass(Int32 i)
	MyClass(Int32 i, Int32 j)

Найден конструктор с двумя параметрами.

Конструирование класса MyClass(int, int)
Значение х: 10, значение у: 20

Вызов методов для объекта reflectOb

Сумма равна 30
Значение 14 находится между х и у
В методе Set(int, int). Значение х: 9, значение у: 18
В методе Set(double, double). Значение х: 1, значение у: 23
Значение х: 1, значение у: 23

А теперь рассмотрим порядок применения рефлексии для конструирования объек­ та класса MyClass. Сначала получается перечень открытых конструкторов в следую­ щей строке кода.

ConstructorInfо[] ci = t.GetConstructors();

Затем для наглядности примера выводятся полученные конструкторы. После этого осуществляется поиск по списку конструктора, принимающего два аргумента, как по­ казано в приведенном ниже фрагменте кода.

for(x=0; х < ci.Length; х++) {
	ParameterInfo[] pi = ci[x].GetParameters();
	if(pi.Length == 2) break;
}

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

// Сконструировать объект.
object[] consargs = new object[2];
consargs[0] = 10;
consargs[1] = 20;
object reflectOb = ci[x].Invoke(consargs);

После вызова метода Invoke() переменная экземпляра reflectOb будет ссылаться на объект типа MyClass. А далее в программе выполняются соответствующие методы для экземпляра этого объекта.

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

Получение типов данных из сборок

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

Как следует из главы 16, сборка несет в себе сведения о типах классов, структур и прочих элементов данных, которые в ней содержатся. Прикладной интерфейс Reflection API позволяет загрузить сборку, извлечь сведения о ней и получить экзем­ пляры объектов любых открыто доступных в ней типов. Используя этот механизм, программа может выявлять свою среду и использовать те функциональные возмож­ ности, которые могут оказаться доступными без явного их определения во время ком­ пиляции. Это очень эффективный и привлекательный принцип. Представьте себе, например, программу, которая выполняет роль "браузера типов", отображая типы данных, доступные в системе, или же инструментальное средство разработки, позво­ ляющее визуально составлять программы из различных типов данных, поддерживае­ мых в системе. А поскольку все сведения о типах могут быть извлечены и проверены, то ограничений на применение рефлексии практически не существует.

Для получения сведений о сборке сначала необходимо создать объект класса Assembly. В классе Assembly открытый конструктор не определяется. Вместо этого объект класса Assembly получается в результате вызова одного из его методов. Так, для загрузки сборки по заданному ее имени служит метод LoadFrom(). Ниже при­ ведена его соответствующая форма:

static Assembly LoadFrom(string файл_сборки)

где файл_сборки обозначает конкретное имя файла сборки.

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

Туре[] GetTypes()

Этот метод возвращает массив типов, содержащихся в сборке.

Для того чтобы продемонстрировать порядок обнаружения типов в сборке, потре­ буются два исходных файла. Первый файл будет содержать ряд классов, обнаруживае­ мых в коде из второго файла. Создадим сначала файл MyClasses.cs, содержащий следующий код.

// Файл, содержащий три класса и носящий имя MyClasses.cs.
using System;

class MyClass {
	int x;
	int y;

	public MyClass(int i) {
		Console.WriteLine("Конструирование класса MyClass(int). ");
		x = у = i;
		Show();
	}

	public MyClass(int i, int j) {
		Console.WriteLine("Конструирование класса MyClass(int, int). ");
		x = i;
		у = j;
		Show();
	}

	public int Sum() {
		return x+y;
	}

	public bool IsBetween(int i) {
		if((x < i) && (i < y)) return true;
		else return false;
	}

	public void Set(int a, int b) {
		Console.Write("В методе Set(int, int). ");
		x = a;
		У = b;
		Show();
	}

	// Перегрузить метод Set.
	public void Set(double a, double b) {
		Console.Write("В методе Set(double, double). ");
		x = (int) a;
		y = (int) b;
		Show();
	}

	public void Show() {
		Console.WriteLine("Значение x: {0}, значение у: {1}", x, у);
	}
}

class AnotherClass {
	string msg;

	public AnotherClass(string str) {
		msg = str;
	}

	public void Show() {
		Console.WriteLine(msg);
	}
}

class Demo {
	static void Main() {
		Console.WriteLine("Это заполнитель.");
	}
}

Этот файл содержит класс MyClass, неоднократно использовавшийся в предыду­ щих примерах. Кроме того, в файл добавлены второй класс AnotherClass и третий класс Demo. Следовательно, сборка, полученная из исходного кода, находящегося в этом исходном файле, будет содержать три класса. Затем этот файл компилируется, и из него формируется исполняемый файл MyClasses.ехе. Именно эта сборка и будет опрашиваться программно.

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

/* Обнаружить сборку, определить типы и создать объект
	с помощью рефлексии. */
using System;
using System.Reflection;

class ReflectAssemblyDemo {
	static void Main() {
		int val;

		// Загрузить сборку MyClasses.exe.
		Assembly asm = Assembly.LoadFrom("MyClasses.exe");

		// Обнаружить типы, содержащиеся в сборке MyClasses.exe.
		Туре[] alltypes = asm.GetTypes();
		foreach(Type temp in alltypes)
			Console.WriteLine("Найдено: " + temp.Name);

		Console.WriteLine();

		// Использовать первый тип, в данном случае - класс MyClass.
		Type t = alltypes[0]; // использовать первый найденный класс
		Console.WriteLine("Использовано: " + t.Name);

		// Получить сведения о конструкторе.
		ConstructorInfo[] ci = t.GetConstructors();

		Console.WriteLine("Доступные конструкторы: ");
		foreach(ConstructorInfo с in ci) {
			// Вывести возвращаемый тип и имя.
			Console.Write(" " + t.Name + "(");

			// Вывести параметры.
			ParameterInfо[] pi = с.GetParameters();
			for(int i=0; i < pi.Length; i++) {
				Console.Write(pi[i].ParameterType.Name + " " + pi[i].Name);
				if(i+1 < pi.Length) Console.Write(", ");
			}
			Console.WriteLine(")");
		}
		Console.WriteLine();

		// Найти подходящий конструктор.
		int x;
		for(x=0; x < ci.Length; x++) {
			ParameterInfo[] pi = ci[x].GetParameters();
			if(pi.Length == 2) break;
		}

		if(x == ci.Length) {
			Console.WriteLine("Подходящий конструктор не найден.");
			return;
		}
		else
			Console.WriteLine("Найден конструктор с двумя параметрами.\n");

		// Сконструировать объект.
		object[] consargs = new object[2];
		consargs[0] = 10;
		consargs[1] = 20;
		object reflectOb = ci[x].Invoke(consargs);

		Console.Write.Line("/nВызов методов для объекта reflectOb.");
		Console.WriteLine();
		MethodInfo[] mi = t.GetMethods();

		// Вызвать каждый метод.
		foreach(MethodInfo m in mi) {
			// Получить параметры.
			ParameterInfo[] pi = m.GetParameters();

			if(m.Name.CompareTo("Set")==0 &&
					pi[0].ParameterType == typeof(int)) {
				// Это метод Set(int, int).
				object[] args = new object[2];
				args[0] = 9;
				args[1] = 18;
				m.Invoke(reflectOb, args);
			}
			else if(m.Name.CompareTo("Set")==0 &&
					pi[0].ParameterType == typeof(double)) {
				// Это метод Set(double, double).
				object[] args = new object[2];
				args[0] = 1.12;
				args[1] = 23.4;
				m.Invoke(reflectOb, args);
			}
			else if(m.Name.CompareTo("Sum")==0) {
				val = (int) m.Invoke(reflectOb, null);
				Console.WriteLine("Сумма равна " + val);
			}
			else if(m.Name.CompareTo("IsBetween")==0) {
				object[] args = new object[1];
				args[0] = 14;
				if((bool) m.Invoke(reflectOb, args))
					Console.WriteLine("Значение 14 находится между x и у");
			}
			else if(m.Name.CompareTo("Show")==0) {
				m.Invoke(reflectOb, null);
			}
		}
	}
}

При выполнении этой программы получается следующий результат.

Найдено: MyClass
Найдено: AnotherClass
Найдено: Demo

Использовано: MyClass

Доступные конструкторы:
	MyClass(Int32 i)
	MyClass(Int32 i, Int32 j)

Найден конструктор с двумя параметрами.

Конструирование класса MyClass(int, int)
Значение х: 10, значение у: 20

Вызов методов для объекта reflectOb

Сумма равна 30
Значение 14 находится между х и у
В методе Set(int, int). Значение х: 9, значение у: 18
В методе Set(double, double). Значение х: 1, значение у: 23
Значение х: 1, значение у: 23

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

Отдельные типы обнаруживаются в сборке MyClasses.ехе с помощью приведен­ ной ниже последовательности кода, находящегося в самом начале метода Маin().

// Загрузить сборку MyClasses.exe.
Assembly asm = Assembly.LoadFrom("MyClasses.ехе");

// Обнаружить типы, содержащиеся в сборке MyClasses.exe.
Туре[] alltypes = asm.GetTypes();
foreach(Type temp in alltypes)
	Console.WriteLine("Найдено: " + temp.Name);

Этой последовательностью кода можно пользоваться всякий раз, когда требуется динамически загружать и опрашивать сборку.

Но сборка совсем не обязательно должна быть исполняемым файлом с расшире­ нием .ехе. Сборки могут быть также в файлах динамически компонуемых библиотек (DLL) с расширением .dll. Так, если скомпилировать исходный файл MyClasses.cs в следующей командной строке:

csc /t:library MyClasses.es

то в итоге получится файл MyClasses.dll. Преимущество размещения кода в библи­ отеке DLL заключается, в частности, в том, что в этом случае метод Main() в исходном коде не нужен, тогда как всем исполняемым файлам требуется определенная точка входа, с которой должно начинаться выполнение программы. Именно поэтому класс Demo содержит метод Main() в качестве такой точки входа. А для библиотеки DLL ме­ тод Main() не требуется. Если же класс MyClass нужно превратить в библиотеку DLL, то в вызов метода LoadFrom() придется внести следующее изменение.

Assembly asm = Assembly.LoadFrom("MyClasses.dll");

Полностью автоматизированное обнаружение типов

Прежде чем завершить рассмотрение рефлексии, обратимся к еще одному поучи­ тельному примеру. Несмотря на то что в программе из предыдущего примера класс MyClass был полноценно использован без явного указания на его имя в программе, этот пример все же опирается на предварительную осведомленность о содержимом класса MyClass. Так, в программе были заранее известны имена методов Set и Sum из этого класса. Но с помощью рефлексии можно воспользоваться типом данных, ниче­ го не зная о нем заранее. С этой целью придется извлечь все сведения, необходимые для конструирования объекта и формирования вызовов соответствующих методов. Та­ кой подход может оказаться пригодным, например, при создании инструментально­ го средства визуального проектирования, поскольку он позволяет использовать типы данных, имеющиеся в системе.

Рассмотрим следующий пример, демонстрирующий полностью автоматизирован­ ное обнаружение типов. В этом примере сначала загружается сборка MyClasses.ехе, затем конструируется объект класса MyClass и далее вызываются все методы, объяв­ ленные в классе MyClass, причем о них ничего заранее неизвестно.

// Использовать класс MyClass, ничего не зная о нем заранее.
using System;
using System.Reflection;

class ReflectAssemblyDemo {
	static void Main() {
		int val;
		Assembly asm = Assembly.LoadFrom("MyClasses.exe");

		Type[] alltypes = asm.GetTypes();

		Type t = alltypes[0]; // использовать первый обнаруженный класс
		Console.WriteLine("Использовано: " + t.Name);

		ConstructorInfo[] ci = t.GetConstructors();

		// Использовать первый обнаруженный конструктор.
		ParameterInfо[] cpi = ci[0].GetParameters();
		object reflectOb;

		if(cpi.Length > 0) {
			object[] consargs = new object[cpi.Length];

			// Инициализировать аргументы.
			fox(int n=0; n < cpi.Length; n++)
				consargs[n] = 10 + n * 20;

			// Сконструировать объект.
			reflectOb = ci[0].Invoke(consargs);
		} else
			reflectOb = ci[0].Invoke(null);

		Console.WriteLine("\nВызов методов для объекта reflectOb.");
		Console.WriteLine();

		// Игнорировать наследуемые методы.
		MethodInfo[] mi = t.GetMethods(BindingFlags.DeclaredOnly |
									BindingFlags.Instance |
									BindingFlags.Public);

		// Вызвать каждый метод.
		foreach(Methodlnfo m in mi) {
			Console.WriteLine("Вызов метода {0} ", m.Name);

		// Получить параметры.
		ParameterInfo[] pi = m.GetParameters();

		// Выполнить методы.
		switch(pi.Length) {
			case 0: // аргументы отсутствуют
				if(m.ReturnType == typeof(int)) {
					val = (int) m.Invoke(reflectOb, null);
					Console.WriteLine("Результат: " + val);
				}
				else if(m.ReturnType == typeof(void)) {
					m.Invoke(reflectOb, null);
				}
				break;
			case 1: // один аргумент
				if(pi[0].ParameterType == typeof(int)) {
					object[] args = new object[1];
					args[0] = 14;
					if((bool) m.Invoke(reflectOb, args))
						Console.WriteLine("Значение 14 находится между x и у");
					else
						Console.WriteLine("Значение 14 не находится между х и у");
				}
				break;
			case 2: // два аргумента
				if((pi[0].ParameterType == typeof(int)) &&
						(pi[1].ParameterType == typeof (int))) {
					object[] args = new object[2];
					args[0] = 9;
					args[1] = 18;
					m.Invoke(reflectOb, args);
				}
				else if((pi[0].ParameterType == typeof(double)) &&
						(pi[1].ParameterType == typeof(double))) {
					object[] args = new object[2];
					args[0] = 1.12;
					args[1] = 23.4;
					m.Invoke(reflectOb, args);
				}
				break;
			}
		Console.WriteLine();
		}
	}
}

Эта программа дает следующий результат.

Использовано: MyClass
Конструирование класса MyClass(int).
Значение х: 10, значение у: 10

Вызов методов для объекта reflectOb.

Вызов метода Sum
Результат: 20

Вызов метода IsBetween
Значение 14 не находится между х и у

Вызов метода Set
В методе Set(int, int). Значение х: 9, значение у: 18

Вызов метода Set
В методе Set(double, double). Значение х: 1, значение у: 23

Вызов метода Show
Значение х: 1, значение у: 23

Эта программа работает довольно просто, но все же требует некоторых пояснений. Во-первых, получаются и используются только те методы, которые явно объявлены в классе MyClass. Для этой цели служит форма BindingFlags метода GetMethods(), чтобы воспрепятствовать вызову методов, наследуемых от объекта. И во-вторых, ко­ личество параметров и возвращаемый тип каждого метода получаются динамически, а затем определяются и проверяются в операторе switch. На основании этой инфор­ мации формируется вызов каждого метода.

Атрибуты

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

Основы применения атрибутов

Атрибут поддерживается классом, наследующим от класса System.Attribute. Поэтому классы атрибутов должны быть подклассами класса Attribute. В классе Attribute определены основные функциональные возможности, но далеко не все они нужны для работы с атрибутами. В именах классов атрибутов принято употреблять суффикс Attribute. Например, ErrorAttribute — это имя класса атрибута, опи­ сывающего ошибку.

При объявлении класса атрибута перед его именем указывается атрибут AttributeUsage. Этот встроенный атрибут обозначает типы элементов, к которым может применяться объявляемый атрибут. Так, применение атрибута может ограни­ чиваться одними методами.

Создание атрибута

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

	[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
	string pri_remark; // базовое поле свойства Remark

	public RemarkAttribute(string comment) {
		pri_remark = comment;
	}

	public string Remark {
		get {
			return pri_remark;
		}
	}
}

Проанализируем этот класс атрибута построчно.

Объявляемый атрибут получает имя RemarkAttribute. Его объявлению пред­ шествует встроенный атрибут AttributeUsage, указывающий на то, что атрибут RemarkAttribute может применяться ко всем типам элементов. С помощью встроен­ ного атрибута AttributeUsage можно сузить перечень элементов, к которым может присоединяться объявляемый атрибут. Подробнее о его возможностях речь пойдет далее в этой главе.

Далее объявляется класс RemarkAttribute, наследующий от класса Attribute. В классе RemarkAttribute определяется единственное закрытое поле pri_remark, поддерживающее одно открытое и доступное для чтения свойство Remark. Это свой­ ство содержит описание, связываемое с атрибутом. (Конечно, Remark можно было бы объявить как автоматически реализуемое свойство с закрытым аксессором set, но ради наглядности данного примера выбрано свойство, доступное только для чтения.) В данном классе определен также один открытый конструктор, принимающий стро­ ковый аргумент и присваивающий его свойству Remark. Этим пока что ограничивают­ ся функциональные возможности класса RemarkAttribute, готового к применению.

Присоединение атрибута

Как только класс атрибута будет определен, атрибут можно присоединить к эле­ менту. Атрибут указывается перед тем элементом, к которому он присоединяется, и для этого его конструктор заключается в квадратные скобки. В качестве примера ниже показано, как атрибут RemarkAttribute связывается с классом.

	[RemarkAttribute("В этом классе используется атрибут.")]
class UseAttrib {
	// ...
}

В этом фрагменте кода конструируется атрибут RemarkAttribute, содержащий комментарий "В этом классе используется атрибут." Данный атрибут затем связывается с классом UseAttrib.

Присоединяя атрибут, совсем не обязательно указывать суффикс Attribute. Например, приведенный выше класс может быть объявлен следующим образом.

	[Remark("В этом классе используется атрибут.")]
class UseAttrib {
	// ...
}

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

Получение атрибутов объекта

Как только атрибут будет присоединен к элементу, он может быть извлечен в дру­ гих частях программы. Для извлечения атрибута обычно используется один из двух методов. Первый метод, GetCustomAttributes(), определяется в классе MemberInfо и наследуется классом Туре. Он извлекает список всех атрибутов, присоединенных к элементу. Ниже приведена одна из его форм.

object[] GetCustomAttributes(bool наследование)

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

Второй метод, GetCustomAttribute(), определяется в классе Attribute. Ниже приведена одна из его форм:

static Attribute GetCustomAttribute(MemberInfo элемент, Type тип_атрибута)

где элемент обозначает объект класса MemberInfo, описывающий тот элемент, для ко­ торого создаются атрибуты, тогда как тип_атрибута — требуемый атрибут. Данный метод используется в том случае, если имя получаемого атрибута известно заранее, что зачастую и бывает. Так, если в классе UseAttrib имеется атрибут RemarkAttribute, то для получения ссылки на этот атрибут можно воспользоваться следующей после­ довательностью кода.

// Получить экземпляр объекта класса MemberInfо, связанного
// с классом, содержащим атрибут RemarkAttribute.
Type t = typeof(UseAttrib);

// Извлечь атрибут RemarkAttribute.
Type tRemAtt = typeof(RemarkAttribute);
RemarkAttribute ra = (RemarkAttribute)
	Attribute.GetCustomAttribute(t, tRemAtt);

Эта последовательность кода оказывается вполне работоспособной, поскольку класс MemberInfo является базовым для класса Туре. Следовательно, t — это экземпляр объекта класса MemberInfo.

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

Console.WriteLine(rа.Remark);

Ниже приведена программа, в которой все изложенные выше особенности приме­ нения атрибутов демонстрируются на примере атрибута RemarkAttribute.

// Простой пример применения атрибута.
using System;
using System.Reflection;

	[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
	string pri_remark; // базовое поле свойства Remark

	public RemarkAttribute(string comment) {
		pri_remark = comment;
	}

	public string Remark {
		get {
			return pri_remark;
		}
	}
}

	[RemarkAttribute("В этом классе используется атрибут.")]
class UseAttrib {
	// ...
}

class AttribDemo {
	static void Main() {
		Type t = typeof(UseAttrib);

		Console.Write("Атрибуты в классе " + t.Name + ": ");
		object[] attribs = t.GetCustomAttributes(false);
		foreach(object о in attribs) {
			Console.WriteLine(o);
		}

		Console.Write("Примечание: ");

		// Извлечь атрибут RemarkAttribute.
		Type tRemAtt = typeof(RemarkAttribute);
		RemarkAttribute ra = (RemarkAttribute)
			Attribute.GetCustomAttribute(t, tRemAtt);

		Console.WriteLine(ra.Remark);
	}
}

Эта программа дает следующий результат.

Атрибуты в классе UseAttrib: RemarkAttribute
Примечание: В этом классе используется атрибут.

Сравнение позиционных и именованных параметров

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

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

ПРИМЕЧАНИЕ Несмотря на то что именованные параметры атрибутов, по существу, подобны именован­ ным аргументам методов, они все же отличаются в деталях.

Именованный параметр поддерживается открытым полем или свойством, которое должно быть нестатическим и доступным только для записи. Любое поле или свой­ ство подобного рода может автоматически использоваться в качестве именованного параметра. Значение присваивается именованному параметру с помощью соответ­ ствующего оператора, расположенного в списке аргументов при вызове конструктора атрибута. Ниже приведена общая форма объявления атрибута, включая именованные параметры.

[attrib(список_позиционных_параметров,
	именованный_параметр_1 = значение,
	именованный_параметр_2 = значение, ...)]

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

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

	[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
	string pri remark; // базовое поле свойства Remark

	// Это поле можно использовать в качестве именованного параметра.
	public string Supplement;

	public RemarkAttribute(string comment) {
		pri_remark = comment;
		Supplement = "Отсутствует";
	}

	public string Remark {
		get {
			return pri_remark;
		}
	}
}

Как видите, поле Supplement инициализируется в конструкторе символьной стро­ кой "Отсутствует". Другого способа присвоить ему первоначальное значение в кон­ структоре не существует. Но поскольку поле Supplement является открытым в классе RemarkAttribute, его можно использовать в качестве именованного параметра, как показано ниже.

	[RemarkAttribute("В этом классе используется атрибут.",
		Supplement = "Это дополнительная информация.")]
class UseAttrib {
	// ...
}

Обратите особое внимание на вызов конструктора класса RemarkAttribute. В этом конструкторе первым, как и прежде, указывается позиционный параметр, а за ним через запятую следует именованный параметр Supplement, которому присваивается конкретное значение. И наконец, закрывающая скобка, ), завершает вызов конструкто­ ра. Таким образом, именованный параметр инициализируется в вызове конструктора. Этот синтаксис можно обобщить: позиционные параметры должны указываться в том порядке, в каком они определены в конструкторе, а именованные параметры — в про­ извольном порядке и вместе с присваиваемыми им значениями.

Ниже приведена программа, в которой демонстрируется применение поля Supplement в качестве именованного параметра атрибута.

// Использовать именованный параметр атрибута.
using System;
using System.Reflection;

[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
	string pri_remark; // базовое поле свойства Remark
	public string Supplement; // это именованный параметр

	public RemarkAttribute(string comment) {
		pri_remark = comment;
		Supplement = "Отсутствует";
	}

	public string Remark {
		get {
			return pri_remark;
		}
	}
}

[RemarkAttribute("В этом классе используется атрибут.",
Supplement = "Это дополнительная информация.")]
class UseAttrib {
	// ...
}

class NamedParamDemo {
	static void Main() {
		Type t = typeof(UseAttrib);

		Console.Write("Атрибуты в классе " + t.Name + ");
		object[] attribs = t.GetCustomAttributes(false);
		foreach(object о in attribs) {
			Console.WriteLine(o);
		}

		// Извлечь атрибут RemarkAttribute.
		Type tRemAtt = typeof(RemarkAttribute);
		RemarkAttrib.ute ra = (RemarkAttribute)
			Attribute.GetCustomAttribute(t, tRemAtt);

		Console.Write("Примечание: ");
		Console.WriteLine(ra.Remark);

		Console.Write("Дополнение: ") ;
		Console.WriteLine(ra.Supplement);
	}
}

При выполнении этой программы получается следующий результат.

Атрибуты в классе UseAttrib: RemarkAttribute
Примечание: В этом классе используется атрибут.
Дополнение: Это дополнительная информация.

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

Открытое и доступное только для чтения свойство может использоваться в качестве именованного параметра таким же образом, как и открытое поле. В качестве примера ниже показано, как автоматически реализуемое свойство Priority типа int вводится в класс RemarkAttribute.

// Использовать свойство в качестве именованного параметра атрибута.
using System;
using System.Reflection;

[AttributeUsage(AttributeTargets.All)]
public class RemarkAttribute : Attribute {
	string pri_remark; // базовое поле свойства Remark
	public string Supplement; // это именованный параметр

	public RemarkAttribute(string comment) {
		pri_remark = comment;
		Supplement = "Отсутствует";
		Priority = 1;
	}

	public string Remark {
		get {
			return pri_remark;
		}
	}
	// Использовать свойство в качестве именованного параметра.
	public int Priority { get; set; }
}

[RemarkAttribute("В этом классе используется атрибут.",
	Supplement = " Это дополнительная информация.",
	Priority = 10)]
class UseAttrib {
	// ...
}

class NamedParamDemo {
	static void Main() {
		Type t = typeof(UseAttrib);

		Console.Write("Атрибуты в классе " + t.Name + ");

		object[] attribs = t.GetCustomAttributes(false);
		foreach(object о in attribs) {
			Console.WriteLine(o);
		}

		// Извлечь атрибут RemarkAttribute.
		Type tRemAtt = typeof(RemarkAttribute);
		RemarkAttribute ra = (RemarkAttribute)
			Attribute.GetCustomAttribute(t, tRemAtt);

		Console.Write("Примечание: ");
		Console.WriteLine(ra.Remark);

		Console.Write("Дополнение: ");
		Console.WriteLine(ra.Supplement);

		Console.WriteLine("Приоритет: " + ra.Priority);
	}
}

Вот к какому результату приводит выполнение этого кода.

Атрибуты в классе UseAttrib: RemarkAttribute
Примечание: В этом классе используется атрибут.
Дополнение: Это дополнительная информация.
Приоритет: 10

В данном примере обращает на себя внимание порядок указания атрибутов перед классом UseAttrib, как показано ниже.

[RemarkAttribute("В этом классе используется атрибут.",
	Supplement = " Это дополнительная информация.",
Priority = 10)]
class UseAttrib {
	// ...
}

Именованные параметры атрибутов Supplement и Priority не обязательно ука­ зывать в каком-то определенном порядке. Порядок их указания можно свободно из­ менить, не меняя сами атрибуты.

И последнее замечание: тип параметра атрибута (как позиционного, так и имено­ ванного) должен быть одним из встроенных простых типов, object, Туре, перечисле­ нием или одномерным массивом одного из этих типов.

Встроенные атрибуты

В C# предусмотрено несколько встроенных атрибутов, но три из них имеют осо­ бое значение, поскольку они применяются в самых разных ситуациях. Это атрибуты AttributeUsage, Conditional и Obsolete, рассматриваемые далее по порядку.

Атрибут AttributeUsage

Как упоминалось ранее, атрибут AttributeUsage определяет типы элементов, к которым может быть применен объявляемый атрибут. AttributeUsage — это, по существу, еще одно наименование класса System.AttributeUsageAttribute. У него имеется следующий конструктор:

AttributeUsage(AttributeTargets validOn)

где validOn обозначает один или несколько элементов, к которым может быть приме­ нен объявляемый атрибут, тогда как AttributeTargets — перечисление, в котором определяются приведенные ниже значения.

Аll Assembly Class Constructor
Delegate Enum Event Field
GenericParameter Interface Method Module
Parameter Property ReturnValue Struct

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

AttributeTargets.Field | AttributeTargets.Property

В классе атрибута AttributeUsage поддерживаются два именованных параметра. Первым из них является параметр AllowMultiple, принимающий логическое значе­ ние. Если это значение истинно, то атрибут может быть применен к одному и тому же элементу неоднократно. Второй именованный параметр, Inherited, также принимает логическое значение. Если это значение истинно, то атрибут наследуется производны­ ми классами, а иначе он не наследуется. По умолчанию параметр AllowMultiple при­ нимает ложное значение (false), а параметр Inherited — истинное значение (true).

В классе атрибута AttributeUsage определяется также доступное только для чте­ ния свойство ValidOn. Оно возвращает значение типа AttributeTargets, определя­ ющее типы элементов, к которым можно применять объявляемый атрибут. По умол­ чанию используется значение AttributeTargets.All.

Атрибут Conditional

Атрибут Conditional представляет, вероятно, наибольший интерес среди всех встроенных атрибутов. Ведь он позволяет создавать условные методы, которые вызыва­ ются только в том случае, если с помощью директивы #define определен конкретный идентификатор, а иначе метод пропускается. Следовательно, условный метод служит альтернативой условной компиляции по директиве #if.

Conditional — это, по существу, еще одно наименование класса System. Diagnostics.ConditionalAttribute. Для применения атрибута Conditional в ис­ ходный код программы следует включить пространство имен System.Diagnostics.

Рассмотрим применение данного атрибута на следующем примере программы.

// Продемонстрировать применение встроенного атрибута Conditional.
#define TRIAL
using System;
using System.Diagnostics;

class Test {
	[Conditional("TRIAL")]
	void Trial() {
		Console.WriteLine("Пробная версия, не " +
		"предназначенная для распространения.");
	}

	[Conditional("RELEASE")]
	void Release() {
		Console.WriteLine("Окончательная рабочая версия.");
	}

	static void Main() {
		Test t = new Test();

		t.Trial(); //вызывается только в том случае, если
				// определен идентификатор TRIAL
		t.Release(); // вызывается только в том случае, если
				// определен идентификатор RELEASE
	}
}

Эта программа дает следующий результат.

Пробная версия, не предназначенная для распространения.

Рассмотрим эту программу подробнее, чтобы стал понятнее результат ее выпол­ нения. Прежде всего обратите внимание на то, что в этой программе определяется идентификатор TRIAL. Затем обратите внимание на определение методов Trial() и Release(). Каждому из них предшествует атрибут Conditional, общая форма ко­ торого приведена ниже:

[Conditional идентификатор]

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

Оба метода, Trial() и Release(), вызываются в методе Main(). Но поскольку определен один лишь идентификатор TRIAL, то выполняется только метод Trial(), тогда как метод Release() игнорируется. Если же определить идентификатор RELEASE, то метод Release() будет также выполняться. А если удалить определение идентификатора TRIAL, то метод Trial() выполняться не будет.

Атрибут Conditional можно также применить в классе атрибута, т.е. в классе, наследующем от класса Attribute. Так, если идентификатор определен, то атрибут применяется, когда он встречается в ходе компиляции. В противном случае он не при­ меняется.

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

Атрибут Obsolete

Атрибут Obsolete (сокращенное наименование класса System.ObsoleteAttribute) позволяет пометить элемент программы как устаревший. Ниже приведена общая фор­ ма этого атрибута:

[Obsolete("сообщение")]

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

// Продемонстрировать применение атрибута Obsolete.
using System;

class Test {

	[Obsolete("Лучше использовать метод MyMeth2.")]
	public static int MyMeth(int a, int b) {
		return a / b;
	}

	// Усовершенствованный вариант метода MyMeth.
	public static int MyMeth2(int a, int b) {
		return b == 0 ? 0 : a/b;
	}

	static void Main() {
		// Для этого кода выводится предупреждение.
		Console.WriteLine("4 / 3 равно " + Test.MyMeth(4, 3));

		// А для этого кода предупреждение не выводится.
		Console.WriteLine("4 / 3 равно " + Test.MyMeth2(4, 3));
	}
}

Когда по ходу компиляции программы в методе Main() встречается вызов метода MyMeth(), формируется предупреждение, уведомляющее пользователя о том, что ему лучше воспользоваться методом MyMeth2().

Ниже приведена вторая форма атрибута Obsolete:

[Obsolete("сообщение", ошибка)]

где ошибка обозначает логическое значение. Если это значение истинно (true), то при использовании устаревшего элемента формируется сообщение об ошибке компиля­ ции вместо предупреждения. Эта форма отличается тем, что программа, содержащая подобную ошибку, не будет скомпилирована в исполняемом виде.