Главная страница
Навигация по странице:

  • Вызов методов с помощью средства отражения

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

  • Получение типов из компоновочных файлов

  • Полная автоматизация получения информации о типах

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

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

  • Использование встроенных атрибутов В C определено три встроенных атрибута: AttributeUsage, Conditional и Obsolete. Рассмотрим их по порядку. Атрибут AttributeUsage

  • Справочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией


    Скачать 5.05 Mb.
    НазваниеСправочник по C# Герберт Шилдт ббк 32. 973. 26018 75 Ш57 удк 681 07 Издательский дом "Вильямс" Зав редакцией
    АнкорC #.pdf
    Дата08.12.2017
    Размер5.05 Mb.
    Формат файлаpdf
    Имя файлаC #.pdf
    ТипСправочник
    #10795
    страница29 из 52
    1   ...   25   26   27   28   29   30   31   32   ...   52
    Второй формат вызова метода 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
    , то при использовании устаревшего элемента программы будет сгенерировано не предупреждение, а сообщение об ошибке. Нетрудно догадаться, что разница между этими двумя форматами состоит в том, что программа с ошибкой не может быть скомпилирована в выполняемую программу.

    Полный справочник по
    1   ...   25   26   27   28   29   30   31   32   ...   52


    написать администратору сайта