Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
Скачать 5.05 Mb.
|
Второй формат вызова метода GetMethods() Второй формат вызова метода GetMethods() позволяет задать различные флаги, которые фильтруют возвращаемые методы. Этот формат таков: MethodInfo[] GetMethods(BindingFlags flags ) Эта версия получает только те методы, которые соответствуют заданному критерию. BindingFlags — это перечисление. Ниже описаны наиболее употребительные его значения: Глава 17. Динамическая идентификация типов, отражение и атрибуты 459 Значение Описание DeclaredOnly Считывание только тех методов, которые определены в заданном классе. Унаследованные методы в результат не включаются Instance Считывание методов экземпляров Nonpublic Считывание нe- public -методов Public Считывание public -методов static Считывание static -методов Два или больше задаваемых флагов можно объединять с помощью оператора ИЛИ. С флагами Public или Nonpublic необходимо устанавливать флаги Instance или Static . В противном случае метод GetMethods() не возвратит ни одного метода. Одно из основных применений 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 ob , object[] args ) Здесь параметр ob — это ссылка на объект, для которого вызывается нужный метод. Для static -методов параметр ob должен содержать значение null . Любые аргументы, которые необходимо передать вызываемому методу, указываются в массиве args . Если метод вызывается без аргументов, параметр args должен иметь null -значение. При этом длина массива args должна совпадать с количеством аргументов, передаваемых методу. Следовательно, если необходимо передать два аргумента, массив args должен состоять из двух элементов, а не, например, из трех или четырех. 460 Часть I. Язык C# Для вызова нужного метода достаточно вызвать метод Invoke() для экземпляра класса MethodInfo , полученного в результате вызова метода GetMethods() . Эта процедура демонстрируется следующей программой: // Вызов методов с использованием средства отражения. using System; using System.Reflection; class MyClass { int x; int y; public MyClass(int i, int j) { x = i; y = 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; y = 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}, значение y: {1}", x, y); } } class InvokeMethDemo { public 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(); Глава 17. Динамическая идентификация типов, отражение и атрибуты 461 // Вызываем каждый метод. 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( "Результат вызова метода sum равен " + 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 и y."); } else if(m.Name.CompareTo("show")==0) { m.Invoke(reflectOb, null); } } } } Результаты выполнения этой программы таковы: Вызов методов, определенных в MyClass Результат вызова метода sum равен 30 14 находится между x и y. Внутри метода set(int, int). Значение x: 9, значение y: 18 Внутри метода set(double, double). Значение x: 1, значение y: 23 Значение x: 1, значение y: 23 Обратите внимание на то, как организуется вызов методов. Сначала получаем список методов. Затем в цикле fоreach извлекаем информацию о параметрах каждого метода. После этого, используя последовательность 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); } 462 Часть I. Язык C# 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() . Если окажется, что рассматривается версия set(int, int) , в массив args загружаются int -аргументы и вызывается метод set() . В противном случае для вызова метода set() используются аргументы типа double Получение конструкторов типа В предыдущем примере продемонстрирована возможность вызова методов с использованием средстве отражения, однако такой подход не имеет преимуществ по сравнению с непосредственным вызовом методов (в данном случае класса MyClass ), поскольку объект типа MyClass создается явным образом. Другими словами, проще вызывать эти методы обычным способом. Однако мощь отражения начинает проявляться в тех случаях, когда объект создается динамически во время работы программы. Для этого нужно сначала получить список конструкторов. Затем создать экземпляр типа, вызвав один из конструкторов. Этот механизм позволяет реализовать объект любого типа во время работы программы, не называя его в инструкции объявления. Чтобы получить конструкторы типа, вызовите метод GetConstructors() для объекта класса Туре . Один из наиболее употребительных форматов его вызова таков: ConstructorInfo[] GetConstructors() Он возвращает массив объектов типа ConstructorInfo , которые описывают эти конструкторы. Класс ConstructorInfо выведен из абстрактного класса MethodBase , который является производным от класса MemberInfo . Класс ConstructorInfo определяет также собственные члены. Из них нас интересует прежде всего метод GetParameters() , который возвращает список параметров, связанных с конструктором. Он работает подобно методу GetParameters() , определенному в описанном выше классе MethodInfо Получив информацию о конструкторе, можно с его помощью создать объект, вызвав метод Invoke() , определенный в классе ConstructorInfo . Формат вызова метода Invoke() в этом случае таков: object Invoke(object[] args ) Любые аргументы, которые необходимо передать конструктору, задаются с помощью массива args . Если конструктор вызывается без аргументов, параметр args должен иметь null -значение. При этом длина массива args должна в точности совпадать с количеством аргументов. Метод Invoke() возвращает ссылку на создаваемый объект. В следующей программе демонстрируется использование средства отражения для создания экземпляра класса MyClass : // Создание объекта с помощью средства отражения. using System; using System.Reflection; Глава 17. Динамическая идентификация типов, отражение и атрибуты 463 class MyClass { int x; int y; public MyClass(int i) { Console.WriteLine( "Создание объекта по формату MyClass(int). "); x = y = i; } public MyClass(int i, int j) { Console.WriteLine( "Создание объекта по формату MyClass(int, int). "); x = i; y = 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; y = 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}, значение y: {1}", x, y); } } class InvokeConsDemo { public static void Main() { Type t = typeof(MyClass); int val; // Получаем информацию о конструкторах. ConstructorInfo[] ci = t.GetConstructors(); Console.WriteLine("Имеются следующие конструкторы: "); 464 Часть I. Язык C# foreach(ConstructorInfo c in ci) { // Отображаем тип возвращаемого значения и имя. Console.Write(" " + t.Name + "{"); // Отображаем параметры. ParameterInfo[] pi = c.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.WriteLine( "\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] = 13; m.Invoke(reflectOb, args); } Глава 17. Динамическая идентификация типов, отражение и атрибуты 465 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( "Результат выполнения метода sum() равен " + 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 и y."); } else if(m.Name.CompareTo("show")==0) { m.Invoke(reflectOb, null); } } } } Результаты выполнения этой программы таковы: Имеются следующие конструкторы: MyClass(Int32 i) MyClass(Int32 i, Int32 j) Найден конструктор с двумя параметрами. Создание объекта по формату MyClass(int, int). Значение x: 10, значение y: 20 Вызов методов для объекта reflectOb. Результат выполнения метода sum() равен 30 14 находится между x и y. Внутри метода set(int, int). Значение x: 9, значение y: 18 Внутри метода set(double, double). Значение x: 1, значение y: 23 Значение x: 1, значение y: 23 Теперь разберемся, как используется средство отражения для создания объекта класса MyClass . Сначала с помощью следующей инструкции получаем список открытых конструкторов: ConstructorInfo[] ci = t.GetConstructors(); Затем в целях иллюстрации отображаем конструкторы, определенные в этом классе. После этого с помощью следующего кода просматриваем полученный список, чтобы найти конструктор, который принимает два аргумента: for(x=0; x < ci.Length; x++) { ParameterInfo[] pi = ci(x).GetParameters(); if(pi.Length == 2) break; } Если нужный конструктор обнаружен (как в данном случае), создаем объект, выполнив такую последовательность инструкций: 466 Часть I. Язык C# // Создаем объект. object[] consargs = new object[2]; consargs[0) = 10; consargs[1] = 20; object reflectOb = ci[x].Invoke(consargs); После обращения к методу Invoke() объект reflectOb будет ссылаться на объект класса MyClass В этом примере в целях упрощения предполагалось, что конструктор, который принимает два int -аргумента, — единственный среди всех конструкторов, определенных в классе MyClass . В реальных приложениях необходимо проверять тип каждого аргумента. Получение типов из компоновочных файлов В предыдущем примере с помощью средства отражения мы многое узнали о классе MyClass , но не все: мы не получили данные о самом типе MyClass . Несмотря на то что мы динамически извлекли из соответствующих объектов информацию о типе MyClass , мы исходили из того, что нам заранее было известно имя типа MyClass , и использовали инструкцию typeof для получения объекта класса Туре , для которого вызывались все методы средства отражения (напрямую или опосредованно). И хотя в некоторых ситуациях такой подход себя вполне оправдывает, возможности средства отражения проявляются в полной мере тогда, когда программа в состоянии определить необходимые типы посредством анализа содержимого других компоновочных файлов. Как было описано в главе 16, компоновочный файл включает информацию о классах, структурах и пр., которые он содержит. Интерфейс Reflection API позволяет загрузить компоновочный файл, извлечь информацию о нем и создать экземпляры любого из содержащихся в нем типов. Используя этот механизм, программа может проанализировать среду выполнения и заставить ее поработать в нужном направлении, не определяя явным образом “точки приложения” во время компиляции. Это чрезвычайно эффективное средство. Например, представьте себе программу, которая действует как браузер типов, отображая доступные в системе типы. Или представьте другое приложение, которое выполняло бы роль средства проектирования, позволяющего визуально связывать отдельные части программы, состоящей из различных типов, поддерживаемых системой. Если все данные о типе поддаются обнаружению, то не существует ограничений на применение средства отражения. Для получения информации о компоновочном файле сначала необходимо создать объект класса Assembly . Класс Assembly не определяет ни одного public - конструктора. Но объект класса Assembly можно создать, вызвав один из его методов. Например, воспользуемся методом LoadFrom() . Вот формат его использования: static Assembly LoadFrom(string имя_файла ) Здесь элемент имя_файла означает имя компоновочного файла. Создав объект класса Assembly , можно получить содержащуюся в нем информацию о типах с помощью метода GetTypes() . Формат его вызова таков: Туре[] GetTypes() Этот метод возвращает массив типов, содержащихся в компоновочном файле. Чтобы продемонстрировать получение информации о типах из компоновочного файла, нужно иметь два файла. Первый должен включать набор классов. Поэтому создадим файл MyClasses.cs с таким содержимым: // Этот файл содержит три класса. // Назовите его MyClasses.cs. Глава 17. Динамическая идентификация типов, отражение и атрибуты 467 using System; class MyClass { int x; int y; public MyClass(int i) { Console.WriteLine( "Создание объекта по формату MyClass(int). "); x = y = i; show(); } public MyClass(int i, int j) { Console.WriteLine( "Создание объекта по формату MyClassdnt, int). "); x = i; y = 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; y = 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}, значение y: {1}", x, y); } } class AnotherClass { string remark; public AnotherClass(string str) { remark = str; } 468 Часть I. Язык C# public void show() { Console.WriteLine(remark); } } class Demo { public static void Main() { Console.WriteLine("Это заглушка."); } } Этот файл содержит класс MyClass , который мы использовали в предыдущих примерах. Кроме того, сюда входит класс AnotherClass и еще один класс — Demo Таким образом, компоновочный файл, генерируемый программой, должен содержать три класса. Теперь скомпилируем этот файл, чтобы получить файл MyClasses.exe . Это и есть компоновочный файл, который мы будем опрашивать. Теперь рассмотрим программу, которая извлекает информацию о файле MyClasses.ехе /* Находим компоновочный файл, определяем типы и создаем объект, используя средство отражения. */ using System; using System.Reflection; class ReflectAssemblyDemo { public static void Main() { int val; // Загружаем компоновочный файл MyClasses.exe. Assembly asm = Assembly.LoadFrom("MyClasses.exe"); // Узнаем, какие типы содержит файл MyClasses.exe. Type[] 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 c in ci) { // Отображаем тип возвращаемого значения и имя. Console.Write(" " + t.Name + "("); // Отображаем параметры. ParameterInfo[] pi = c.GetParameters(); for(int i=0; i < pi.Length; i++) { Console.Write(pi[i].ParameterType.Name + Глава 17. Динамическая идентификация типов, отражение и атрибуты 469 " " + 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.WriteLine( "\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) { 470 Часть I. Язык C# val = (int) m.Invoke(reflectOb, null); Console.WriteLine( "Результат выполнения метода sum() равен " + 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 и y."); } 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). Значение x: 10, значение y: 20 Вызов методов для объекта reflectOb. Результат выполнения метода sum() равен 30 14 находится между x и y. Внутри метода set(int, int). Значение x: 9, значение y: 18 Внутри метода set(double, double). Значение x: 1, значение y: 23 Значение x: 1, значение y: 23 Как видно из результатов выполнения программы, были обнаружены все три класса, содержащиеся в файле MyClasses.exe . Первый из выявленных классов, в данном случае это MyClass , был использован для реализации объекта и выполнения его методов. Причем все это было проделано без использования информации о содержимом файла MyClasses.ехе Информация о типах, содержащихся в файле MyClasses.exe , извлекается с помощью следующей последовательности инструкций, которыми открывается метод Main() : // Загружаем компоновочный файл MyClasses.exe. Assembly asm = Assembly.LoadFrom("MyClasses.ехе"); // Узнаем, какие типы содержит файл MyClasses.exe. Туре[] alltypes = asm.GetTypes(); foreach(Type temp in alltypes) Console.WriteLine("Обнаружено: " + temp.Name); Глава 17. Динамическая идентификация типов, отражение и атрибуты 471 Эту последовательность инструкций можно использовать в случае, когда нужно динамически загрузить и опросить компоновочный файл. Кстати, компоновочный файл необязательно должен быть exe -файлом, Компоновочные файлы также можно найти среди файлов динамически подключаемой библиотеки (dynamic link library — DLL), которые имеют расширение dll . Например, файл MyClasses.cs можно скомпилировать с помощью такой командной строки: csc /t:library MyClasses.cs В результате выполнения этой команды мы получили бы выходной файл MyClasses.dll . При внесении программного кода в DLL-библиотеку не требуется создавать метод Main() . Для всех exe -файлов наличие такой точки входа, как метод Main() , обязательно. Поэтому класс Demo содержит заглушку для метода Main() . Для DLL-библиотек точки входа могут отсутствовать. Если вы захотите превратить MyClass в DLL-файл, вам придется изменить обращение к методу LoadFrom() следующим образом: Assembly asm = Assembly.LoadFrom("MyClasses.dll"); Полная автоматизация получения информации о типах Прежде чем завершить изучение темы отражения информации о типах, стоит рассмотреть еще один пример. Несмотря на то что в предыдущей программе нам удалось использовать класс MyClass без явного указания его имени в программе, все же мы предварительно знали содержимое класса MyClass . Например, нам были заранее известны имена его методов ( set() и sum() ). Однако с помощью средства отражения можно использовать тип, о котором нам предварительно ничего не известно. Для этого необходимо получить информацию, необходимую для создания объекта, и сгенерировать вызовы методов. Такой подход эффективен, например, в случае визуального средства проектирования, поскольку в нем используются типы, имеющиеся в системе. Чтобы понять, как динамически извлечь информацию о типе, рассмотрим следующий пример, в котором загружается компоновочный файл MyClasses.exe , создается объект класса MyClass , а затем вызываются все объявленные методы без каких бы то ни было предварительных сведений. // Использование класса MyClass без опоры на // предварительные данные о нем. using System; using System.Reflection; class ReflectAssemblyDemo { public 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(); // Используем первый обнаруженный конструктор. 472 Часть I. Язык C# ParameterInfo[] cpi = ci[0].GetParameters(); object reflectOb; if(cpi.Length > 0) { object[] consargs = new object[cpi.Length]; // Инициализируем аргументы. for(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(MethodInfo 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 и y."); else Console.WriteLine( "14 не находится между x и y."); } break; case 2: // два аргумента if((pi[0].ParameterType == typeof(int)) && Глава 17. Динамическая идентификация типов, отражение и атрибуты 473 (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). Значение x: 10, значение y: 10 Вызываем методы для объекта reflectOb. Вызов метода sum Результат равен 20 Вызов метода isBetween 14 не находится между x и y. Вызов метода set Внутри метода set(int, int). Значение x: 9, значение y: 18 Вызов метода set Внутри метода set(double, double). Значение x: 1, значение y: 23 Вызов метода show Значение x: 1, значение y: 23 Обратите внимание вот на что. Во-первых, программа получает (и использует) информацию только о тех методах, которые явно объявлены в классе MyClass . Эта фильтрация достигается благодаря использованию BindingFlags -формата вызова метода GetMethods() . Тем самым становится возможным отсев унаследованных методов. Во- вторых, отметьте, каким образом программа динамически получает количество параметров и тип значений, возвращаемых каждым методом. Количество параметров определяется с помощью switch -инструкции. В каждой case -ветви этой инструкции проверяется тип (типы) параметра (параметров) и тип возвращаемого методом значения. Затем на основе этой информации и организуется соответствующий вызов метода. 474 Часть I. Язык C# Атрибуты В 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 , предназначенного только для чтения. В этом свойстве хранится описание, связанное с атрибутом. В Глава 17. Динамическая идентификация типов, отражение и атрибуты 475 классе RemarkAttribute определен один открытый конструктор, который принимает строковый аргумент и присваивает его значение полю pri_remark . Этим, собственно, и ограничиваются функции атрибута RemarkAttribute , который совершенно готов к применению. Присоединение атрибута Определив класс атрибута, можно присоединить его к соответствующему элементу. Атрибут предшествует элементу, к которому он присоединяется, и задается путем заключения его конструктора в квадратные скобки. Например, вот как атрибут RemarkAttribute можно связать с классом: [RemarkAttribute("Этот класс использует атрибут.")] class UseAttrib { //... } При выполнении этого фрагмента кода создается объект класса RemarkAttribute , который содержит комментарий “Этот класс использует атрибут.”. Затем атрибут связывается с классом UseAttrib При связывании атрибута необязательно указывать суффикс Attribute . Например, предыдущее объявление класса можно было бы записать так: [Remark("Этот класс использует атрибут.")] class UseAttrib { // ... } Здесь используется только имя Remark . Несмотря на корректность использования этой короткой формы, при связывании атрибута все же безопаснее использовать его полное имя, поскольку это позволяет избежать возможной путаницы и неоднозначности. Получение атрибутов объекта После того как атрибут присоединен к элементу, другие части программы могут его извлечь. Для этого обычно используют один из двух методов. Первый — метод GetCustomAttributes() , который определен в классе MemberInfo и унаследован классом Туре . Он считывает список всех атрибутов, связанных с элементом, а формат его вызова таков: object[] GetCustomAttributes(bool searchBases ) Если аргумент searchBases имеет значение true , в результирующий список включаются атрибуты всех базовых классов по цепочке наследования. В противном случае будут включены только атрибуты, определенные заданным типом. Второй — метод GetCustomAttribute() , который определен в классе Attribute . Вот один из форматов его вызова: static Attribute GetCustomAttribute(MemberInfo mi , Type attribtype ) Здесь аргумент mi означает объект класса MemberInfo , описывающий элемент, для которого извлекается атрибут. Нужный атрибут указывается аргументом attribtype Этот метод используется в том случае, если известно имя атрибута, который нужно получить. Например, чтобы получить ссылку на атрибут RemarkAttribute , можно использовать такую последовательность: // Считываем RemarkAttribute. Type tRemAtt = typeof(RemarkAttribute); RemarkAttribute ra = (RemarkAttribute) Attribute.GetCustomAttribute(t, tRemAtt); 476 Часть I. Язык C# Имея ссылку на атрибут, можно получить доступ к его членам. Другими словами, информация, связанная с атрибутом, доступна программе, использующей элемент, к которому присоединен атрибут. Например, при выполнении следующей инструкции отображается значение поля remark : Console.WriteLine(ra.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 { public static void Main() { Type t = typeof(UseAttrib); Console.Write("Атрибуты в " + t.Name + ": "); object[] attribs = t.GetCustomAttributes(false); foreach(object o in attribs) { Console.WriteLine(o); } Console.Write("Remark: "); // Считываем атрибут RemarkAttribute. Type tRemAtt = typeof(RemarkAttribute); RemarkAttribute ra = (RemarkAttribute) Attribute.GetCustomAttribute(t, tRemAtt); Console.WriteLine(ra.remark); } } Результаты выполнения этой программы таковы: Атрибуты в UseAttrib: RemarkAttribute Remark: Этот класс использует атрибут. Глава 17. Динамическая идентификация типов, отражение и атрибуты 477 Сравнение позиционных и именованных параметров В предыдущем примере атрибут RemarkAttribute был инициализирован посредством передачи конструктору строки описания. В этом случае, т.е. при использовании обычного синтаксиса конструктора, параметр comment, принимаемый конструктором RemarkAttribute() , называется позиционным параметром. Возникновение этого термина объясняется тем, что аргумент метода связывается с параметром посредством своей позиции. Так работают в C# все методы и конструкторы. Но для атрибутов можно создавать именованные параметры и присваивать им начальные значения, используя их имена. Именованный параметр поддерживается либо открытым полем, либо свойством, которое не должно быть предназначено только для чтения. Такое поле или свойство автоматически можно использовать в качестве именованного параметра. При определении атрибута для элемента именованный параметр получает значение с помощью инструкции присваивания, которая содержится в конструкторе атрибута. Формат спецификации атрибута, включающей именованные параметры, таков: [ attrib ( список_позиционных_параметров , именованный_параметр_1 = value , именованиий_параметр_2 = value , ...)] Позиционные параметры (если они имеются) должны стоять в начале списка параметров. Затем каждому именованному параметру присваивается значение. Порядок следования именованных параметров не имеет значения. Именованным параметрам присваивать значения необязательно. Но в данном случае значения инициализации использованы по назначению. Чтобы понять, как используются именованные параметры, лучше всего рассмотреть пример. Перед вами версия определения класса 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("Этот класс использует атрибут.", supplement = "Это дополнительная информация.")] class UseAttrib { // ... } 478 Часть I. Язык C# Обратите особое внимание на то, как вызывается конструктор класса 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 { public static void Main() { Type t = typeof(UseAttrib); Console.Write("Атрибуты в " + t.Name + ": "); object[] attribs = t.GetCustomAttributes(false); foreach(object o in attribs) { Console.WriteLine(o); } // Считывание атрибута RemarkAttribute. Type tRemAtt = typeof(RemarkAttribute); RemarkAttribute ra = (RemarkAttribute) Attribute.GetCustomAttribute(t, tRemAtt); Console.Write("Remark: "); Console.WriteLine(ra.remark); Глава 17. Динамическая идентификация типов, отражение и атрибуты 479 Console.Write("Supplement: "); Console.WriteLine(ra.supplement); } } Вот результаты выполнения этой программы: Атрибуты в UseAttrib: RemarkAttribute Remark: Этот класс использует атрибут. Supplement: Это дополнительная информация. Как разъяснялось выше, в качестве именованного параметра можно также использовать свойство. Например, в следующей программе в класс атрибута RemarkAttribute добавляется int -свойство с именем priority // Использование свойства в качестве именованного // параметра атрибута. using System; using System.Reflection; [AttributeUsage(AttributeTargets.All)] public class RemarkAttribute : Attribute { string pri_remark; // Базовое поле для свойства remark. int pri_priority; // Базовое поле для свойства priority. public string supplement; // Это именованный параметр. public RemarkAttribute(string comment) { pri_remark = comment; supplement = "Данные отсутствуют"; } public string remark { get { return pri_remark; } } // Используем свойство в качестве именованного параметра. public int priority { get { return pri_priority; } set { pri_priority = value; } } } [RemarkAttribute( "Этот класс использует атрибут.", supplement = "Это дополнительная информация.", priority = 10)] class UseAttrib { // } class NamedParamDemo { 480 Часть I. Язык C# public static void Main() { Type t = typeof(UseAttrib); Console.Write("Атрибуты в " + t.Name + ": "); object[] attribs = t.GetCustomAttributes(false); foreach(object o in attribs) { Console.WriteLine(o); } // Считываем атрибут RemarkAttribute. Type tRemAtt = typeof(RemarkAttribute); RemarkAttribute ra = (RemarkAttribute) Attribute.GetCustomAttribute(t, tRemAtt); Console.Write("Remark: "); Console.WriteLine(ra.remark); Console.Write("Supplement: "); Console.WriteLine(ra.supplement); Console.WriteLine("Priority: " + ra.priority); } } Вот результаты выполнения этой программы: Атрибуты в UseAttrib: RemarkAttribute Remark: Этот класс использует атрибут. Supplement: Это дополнительная информация. Priority: 10 Обратите внимание на определение атрибута (перед определением класса UseAttrib ): [RemarkAttribute( "Этот класс использует атрибут.", supplement = "Это дополнительная информация.", priority = 10)] Задание именованных атрибутов supplement и priority не подчинено определенному порядку. Эти два присваивания можно поменять местами, и это никак не отразится на атрибуте в целом. Использование встроенных атрибутов В C# определено три встроенных атрибута: AttributeUsage , Conditional и Obsolete . Рассмотрим их по порядку. Атрибут AttributeUsage Как упоминалось выше, атрибут AttributeUsage определяет типы элементов, к которым можно применить атрибут. AttributeUsage — это еще одно имя для класса System.AttributeUsageAttribute . В классе AttributeUsage определен следующий конструктор: AttributeUsage(AttributeTargets item ) Глава 17. Динамическая идентификация типов, отражение и атрибуты 481 Здесь параметр item означает элемент или элементы, к которым может быть применен этот атрибут. Тип AttributeTargets — это перечисление, которое определяет следующие значения: All Assembly Class Constructor Delegate Enum Event Field Interface Method Module Parameter Property ReturnValue Struct Два или больше из этих значений можно объединить с помощью операции ИЛИ. Например, чтобы определить атрибут, применяемый только к полям и свойствам, используйте следующий вариант объединения значений перечисления AttributeTargets : AttributeTargets.Field | AttributeTargets.Property Конструктор класса AttributeUsage поддерживает два именованных параметра. Первый — это параметр AllowMultiple , который принимает значение типа bool , Если оно истинно, этот атрибут можно применить к одному элементу более одного раза. Второй — параметр Inherited , который также принимает значение типа bool . Если оно истинно, этот атрибут наследуется производными классами. В противном случае — не наследуется. По умолчанию оба параметра AllowMultiple и Inherited устанавливаются равными значению false Атрибут Conditional Атрибут Conditional, пожалуй, самый интересный из всех встроенных C#-атрибутов. Он позволяет создавать условные методы. Условный метод вызывается только в том случае, если соответствующий идентификатор определен с помощью директивы #define В противном случае вызов метода опускается. Таким образом, условный метод предлагает альтернативу условной компиляции на основе директивы #if Conditional — еще одно имя для класса System.Diagnostics.Conditional -Attribute . Чтобы использовать атрибут 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("Окончательная версия."); } public static void Main() { Test t = new Test(); 482 Часть I. Язык C# t.trial(); // Вызывается только в случае, если // идентификатор TRIAL определен. t.release(); // Вызывается только в случае, если // идентификатор RELEASE определен. } } Вот результаты выполнения этой программы: Пробная версия, не для распространения. Рассмотрим внимательно код этой программы, чтобы понять, почему получены такие результаты. Прежде всего следует отметить, что в программе определяется идентификатор TRIAL , и обратить ваше внимание на определение методов trial() и release() . В обоих случаях им предшествует атрибут Conditional , который используется в таком формате: [Conditional " symbol "] Здесь элемент symbol означает идентификатор, который определяет, будет ли выполнен этот метод. Этот атрибут можно использовать только для методов. Если соответствующий идентификатор определен, вызываемый метод выполняется. В противном случае метод не выполняется. Внутри метода Main() вызывается как метод trial() , так и метод release() Однако в программе определен только идентификатор TRIAL . Поэтому выполняется один метод trial() . Вызов же метода release() игнорируется. Если определить также и идентификатор RELEASE , выполнится и метод release() . Если при этом удалить определение идентификатора TRIAL , метод trial() вызван не будет. На условные методы налагается ряд ограничений. Они должны возвращать void - значение. Они должны быть членами класса, а не интерфейса. Их определение не может предварять ключевое слово override. Атрибут Obsolete Имя атрибута Obsolete представляет собой сокращение от имени класса System.ObsoleteAttribute . Этот атрибут позволяет отметить какой-либо элемент программы как устаревший. Формат его применения таков: [Obsolete(" message ")] Здесь параметр message содержит сообщение, которое будет отображено в случае компиляции соответствующего элемента программы. Рассмотрим короткий пример. // Демонстрация использования атрибута Obsolete. using System; class Test { [Obsolete("Лучше использовать метод myMeth2.")] static int myMeth(int a, int b) { return a / b; } // Улучшенная версия метода myMeth(). static int myMeth2(int a, int b) { return b == 0 ? 0 : a /b; } public static void Main() { Глава 17. Динамическая идентификация типов, отражение и атрибуты 483 // Предупреждение, отображаемое при выполнении // этой инструкции. Console.WriteLine("4 / 3 is " + Test.myMeth(4, 3)); // Здесь не будет никакого предупреждения. Console.WriteLine("4 / 3 is " + Test.myMeth2(4, 3)); } } Если при компиляции этой программы в методе Main() встретится вызов метода myMeth() , сгенерируется предупреждение, в котором пользователю будет предложено использовать вместо метода myMeth() метод myMeth2() Второй формат применения атрибута Obsolete выглядит так: [Obsolete(" message ", error )] Здесь параметр error имеет тип Boolean . Если его значение равно true , то при использовании устаревшего элемента программы будет сгенерировано не предупреждение, а сообщение об ошибке. Нетрудно догадаться, что разница между этими двумя форматами состоит в том, что программа с ошибкой не может быть скомпилирована в выполняемую программу. |