Проект к данной лекции Вы можете скачать здесь.
Атрибуты многократно упоминались в тексте этого курса. Всюду, где речь шла об объявлении сущностей, говорилось, что объявление может сопровождаться указанием атрибутов этой сущности. Но всякий раз говорилось, что атрибуты необязательны, и можно их не задавать. Пришла пора рассмотреть, что представляют собой атрибуты, где и когда их следует использовать. Атрибуты формально не относятся к программному коду. Это декларативная информация, некоторое описание, сопровождающее код. Информация, заданная атрибутом, преобразуется компилятором в метаинформацию, сопровождающую сборку. Метаинформация доступна исполняемой программе благодаря процессу, называемому отражением. В библиотеке CLR есть классы, позволяющие извлечь метаинформацию и анализировать ее. Результаты анализа, естественно, могут влиять на дальнейший ход выполнения программы.
Как и полагается в объектном программировании, атрибуты - это настоящие объекты, принадлежащие различным атрибутным классам. Прародителем всех атрибутных классов является класс с именем Attribute. Все атрибутные классы из Framework .Net являются потомками этого класса, и их имена заканчиваются суффиксом Attribute, который, впрочем, можно опускать при работе с атрибутами. Когда программист создает собственные атрибуты, то он, естественно, определяет собственный атрибутный класс, который также должен быть потомком прародителя атрибутов - класса Attribute и, для следования правилам хорошего тона оформления кода, иметь соответствующий суффикс в имени.
Где могут появляться атрибуты? Атрибуты могут задаваться для самых разных сущностей - классов, структур, интерфейсов, перечислений, делегатов, событий, полей и методов класса, параметров метода и возвращаемых значений. Атрибуты могут быть также заданы для всей сборки и модуля. Атрибуты задаются в точке объявления сущности. Синтаксически атрибут, связываемый с сущностью, заключается в квадратные скобки, непосредственно предшествуя описанию сущности. С одной сущностью могут быть связаны несколько атрибутов. О разных синтаксических формах задания атрибутов поговорим чуть позже, а сейчас рассмотрим первый пример использования атрибутов.
В лекции, посвященной перечислениям, подробно рассматривался частный, но важный случай перечислений, представляющий собой шкалы - строки битов. Для работы с перечислениями создан атрибутный класс FlagsAttribute. Появление этого атрибута у перечисления указывает компилятору, что перечисление задает шкалу. Как следствие, изменяется семантика работы методов ToString и Format для перечислений. Метод ToString для объекта перечисления, задающего комбинацию битов, возвращает в качестве результата число, если атрибут Flags не задан, и возвращает совокупность значений, заданных перечислением, если атрибут определен. Аналогичным образом меняется семантика метода Format.
Вернемся к примеру из лекции 3, где речь шла о фирме, принимающей программистов на работу. В этом примере был создан специальный класс Job для работы с кандидатами, претендующими на занятие вакансий, описание которого теперь приводить не буду. Свойства кандидатов задавались перечислением. Добавим теперь к этому перечислению атрибут Flags:
/// <summary>
/// Свойства претендентов на должность программиста,
/// описывающие знание технологий и языков программирования
/// </summary>
[Flags]
//[FlagsAttribute]
//[FlagsAttribute()]
public enum Prog_Properties
{
VB = 1, C_sharp = 2, C_plus_plus = 4,
Web = 8, Prog_1C = 16
}
Здесь приведены три формы записи атрибута. Синтаксически они разнятся, но по сути эквивалентны. Наиболее употребительной является первая краткая форма записи атрибута, где указывается имя класса с опущенным суффиксом. Во второй форме указывается полное имя класса. Третья - полная форма записи - показывает, что фактически вызывается конструктор класса, создающий объект. В случае вызова конструктора без параметров скобки можно опускать так же, как и суффикс в имени класса. Задавать многократно атрибут Flags для одной и той же сущности нельзя, так что две формы из трех закомментированы.
В общем случае атрибутный класс имеет параметры. В данном конкретном случае у атрибутного класса Flags нет параметров, поскольку важен лишь сам факт задания атрибута. Если атрибут задан, то компилятор при работе с перечислением выбирает одну реализацию методов ToString и Format, не задан - другую.
Рассмотрим знакомый по лекции 3 тест для работы с перечислением:
/// <summary>
/// Тестирование процесса приема на работу
/// </summary>
public void TestJob()
{
Prog_Properties pattern = Prog_Properties.C_sharp |
Prog_Properties.Web;
Console.WriteLine("Требования, заданные образцом:" +
" Знание языка С# и Web-технологии");
int n = 10;
Job mys = new Job(n);
mys.FormCands();
Prog_Properties[] cand = mys.GetCands();
//string[] strCand = mys.GetStrCands();
for (int i = 0; i < n; i++)
{
Console.WriteLine("Свойства кандидата[{0}] - {1}",
i, cand[i].ToString());
//Console.WriteLine(strCand[i]);
}
}
Обратите внимание на закомментированные строки:
string[] strCand = mys.GetStrCands(); Console.WriteLine(strCand[i]);
Ранее, когда атрибут Flags не задавался, я сам создавал реализацию метода ToString, учитывающую особенности работы со шкалами. Теперь при наличии атрибута Flags это становится излишним. Вот как выглядят результаты работы теста.
(рис 9.1) Перечисления и атрибут FlagsИтак, рассмотрен пример простого атрибутного класса. Атрибут Flags может быть задан только для классов, более того - для классов, задающих перечисления, более того - для перечислений, задающих шкалы. Конечно же, есть атрибуты, имеющие более широкое применение.
Рассмотрим, как устроен класс Attribute, потомками которого являются все атрибутные классы. Вот описание этого класса:
[SerializableAttribute] [ClassInterfaceAttribute(ClassInterfaceType.None)] [ComVisibleAttribute(true)] [AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = false)] public abstract class Attribute : _Attribute
Класс Attribute является абстрактным классом, наследником интерфейса _Attribute. Обратите внимание: атрибуты задаются и при объявлении класса Attribute, так же как и для любого другого класса. Как и положено, класс наследует все методы родительского класса object и определяет методы, заданные интерфейсом _Attribute. У класса определен конструктор без аргументов, ряд статических и динамических (экземплярных) методов. Есть у класса и одно-единственное свойство. Рассмотрим подробнее некоторые из методов класса.
Все рассматриваемые в этом разделе методы перегружены. Вот одна из реализаций метода GetCustomAttribute:
public static Attribute GetCustomAttribute( MemberInfo element, Type attributeType)
Метод возвращает ссылку на единственный атрибут, если таковой будет найден, и null - в противном случае. Если находится несколько атрибутов, удовлетворяющих условию поиска, то выбрасывается исключение. Так что этот метод применяется обычно к атрибутам, которые не допускают дублирования, то есть таких, для которых свойство AllowMultiple имеет значение false. Тип первого аргумента задается классом отражения из пространства System.Reflections. Первый аргумент задает элемент, с которым связан атрибут. Второй аргумент задает тип разыскиваемого атрибута. В данной реализации разыскиваются атрибуты, связанные с одним из членов класса - методом, полем, событием. У других реализаций этого метода первый аргумент может задавать другие классы отражения - сборку, класс и другие сущности, с которыми может связываться атрибут. В реализациях возможны и дополнительные аргументы, с чем встретимся в примерах.
Метод GetCustomAttributes возвращает массив найденных атрибутов, возможно, пустой. Метод перегружен, он может иметь те же аргументы, что и метод GetCustomAttribute, но в отличие от первого возвращает не один, а все найденные атрибуты. Во многих реализациях этого метода не задается тип разыскиваемого атрибута, так что возвращаются все атрибуты, связанные с заданным элементом, независимо от их типа.
Метод IsDefined работает более быстро, чем рассмотренные только что методы, он возвращает true, если искомый атрибут есть у элемента, и false - в противном случае.
Важное замечание: из рассматриваемых методов только метод GetCustomAttribute является уникальным и существует только у класса Attribute. Остальные два метода существуют в форме экземплярных методов у классов, связанных с процессом отражения, и могут быть вызваны, например, у объектов класса Type.
Рассмотрим, как, используя процесс отражения, получить атрибуты, заданные для некоторой сущности. Для определенности рассмотрим класс перечисления Prog_Properties, для которого задан атрибут Flags, и попробуем извлечь этот атрибут из объявления класса. Заодно продемонстрируем два способа получения
/// <summary>
/// Извлечение атрибутов класса
/// </summary>
public void TestTwoMeans()
{
Type clstype = typeof(Prog_Properties);
// Получить атрибуты перечисления,
//используя класс Attribute
Console.WriteLine("Класс Attribute!");
if (Attribute.IsDefined(clstype,
typeof(FlagsAttribute), false))
{
Attribute att = Attribute.GetCustomAttribute(clstype,
typeof(FlagsAttribute));
Console.WriteLine(
"У класса Prog_Properties есть атрибут {0}", att);
}
else
Console.WriteLine("У класса Prog_Properties нет атрибута Flags");
Attribute[] attrs = Attribute.GetCustomAttributes(
clstype, false);
foreach (Attribute attr in attrs)
Console.WriteLine(attr);
// Получить атрибуты перечисления,
//используя класс Type
Console.WriteLine("Класс Type!");
if(clstype.IsDefined(typeof(FlagsAttribute), false))
Console.WriteLine("У класса Prog_Properties есть атрибут Flags");
else
Console.WriteLine("У класса Prog_Properties нет атрибута Flags");
object[] attrsob = clstype.GetCustomAttributes(false);
foreach (Attribute attr in attrsob)
Console.WriteLine(attr);
}
Первым делом создается объект с именем clstype класса Type, содержащий тип анализируемого класса Prog_Properties. Далее вызов метода IsDefined позволяет определить, есть ли у класса атрибут Flags. Если таковой имеется, то вызывается статический метод GetCustomAttribute класса Attribute, которому передаются три аргумента - созданный объект clstype, тип разыскиваемого атрибута и булевский аргумент со значением false, указывающий, что не следует учитывать наследование атрибута. Метод возвращает в качестве результата объект класса FlagsAttribute.
Следующая задача состоит в том, чтобы определить полный список всех атрибутов, связанных с классом. Метод GetCustomAttributes класса Attribute прекрасно решает эту задачу, возвращая в качестве результата массив атрибутов, возможно, пустой. У метода на один аргумент меньше, поскольку ему не нужно передавать тип разыскиваемого атрибута.
Так же просто решаются эти задачи, если использовать работу с методами класса Type, что демонстрируется во второй части нашего примера. Отличия есть, но они незначительны. У класса Type нет метода GetCustomAttribute, так что получать одиночный атрибут сложнее. Второе отличие состоит в том, что метод GetCustomAttributes возвращает массив объектов класса object, а не Attribute, как в первом случае. На рис. 9.2 показаны результаты работы тестового примера.
(рис 9.2) Получение атрибутов класса
Рассмотрим, как устроены атрибутные классы - наследники класса Attribute. Эти классы имеют специфику, отличающую их от других классов. У них, помимо наследуемых от родителя, могут быть заданы свои поля и свойства. Атрибутные классы могут переопределять методы родителя.
Часть полей класса называются позиционными параметрами класса. К ним относят параметры, обязательные для задания. Фактические значения этих параметров всегда передаются конструктору класса в момент задания атрибута. Эти поля могут быть закрытыми.
Другие необязательные поля или свойства атрибутного класса называются именованными. Они должны быть не статическими, открытыми и допускать как чтение, так и запись. Если при объявлении атрибута требуется задать значение именованного параметра, то это значение передается также конструктору в виде пары < имя = значение>. Конструктору вначале передаются позиционные параметры в определенном порядке, а затем именованные параметры, порядок следования которых может быть произвольным. Наличие позиционных и именованных параметров и способ их передачи конструктору является синтаксическим отличием атрибутных классов.
Атрибуты этого класса задаются для большинства атрибутных классов. Будучи заданным для атрибутного класса, атрибут определяет, как могут использоваться атрибуты определяемого класса. Если взглянуть на приведенное ранее определение класса Attribute, то и для него задан атрибут AttributeUsage. Этот атрибут предваряет и определение самого класса AttributeUsageAttribute.
У класса AttributeUsage три параметра.
ValidOn - позиционный параметр. Его значения задаются перечислением AttributeTargets, представляющим шкалу. Значение может быть составным и задаваться с использованием побитовой логической операции < | >. Значение параметра определяет, для каких элементов программы может быть задан атрибут определяемого класса. Для класса Attribute значение этого параметра равно AttributeTargets.All, указывающее, что атрибуты могут появляться для всех возможных элементов. Для класса FlagsAttribute значение этого параметра равно AttributeTargets.Enum, указывающее, что атрибут, задающий флаги, может использоваться только для перечислений.Inherited - именованный параметр. Его значения true или false определяют, может ли атрибутный класс быть наследуемым.AllowMultiple - именованный параметр. Его значения true или false определяют, может ли с программным элементом связываться несколько экземпляров атрибута или он может быть задан только в одном экземпляре.Приведу примеры задания этого атрибута соответственно для классов Attribute, AttributeUsage и Flags:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = false)][AttributeUsageAttribute(AttributeTargets.Class, Inherited = true)][AttributeUsageAttribute(AttributeTargets.Enum, Inherited = false)]Как уже говорилось, в момент объявления сущности - элемента программы - можно задать атрибуты, связывая их с программным элементом. Синтаксическим признаком атрибута являются квадратные скобки, текст внутри которых можно рассматривать как вызов конструктора, создающего объект соответствующего атрибутного класса. Для одной сущности можно задать несколько атрибутов, принадлежащих как разным атрибутным классам, так и одному классу, если параметр AllowMultiple этого класса имеет значение true. Связывать атрибут с программным элементом можно только тогда, когда программный элемент соответствует цели атрибута, устанавливаемой параметром ValidOn и задаваемой перечислением AttributeTargets. Примеры подобного объявления атрибутов для программных элементов уже приводились.
Точные правила спецификации атрибутов и связывания их с сущностью следующие. Для связывания сущности с атрибутами можно задать одну или несколько атрибутных секций. Каждая атрибутная секция окаймляется квадратными скобками и содержит один атрибут или список атрибутов, элементы которого разделяются запятыми. Порядок следования атрибутных секций и порядок следования атрибутов в списке не играет значения. Так что спецификации атрибутов [A][B], [B][A], [A, B], и [B, A] эквивалентны.
Рассмотрим некоторые особенности связывания. Атрибуты можно задавать для сущностей, не объявляемых в тексте программы. Такими сущностями являются сборка ( assembly ) и модуль ( module ). Сборка создается для каждого проекта и, помимо программного кода, содержит метаинформацию, описывающую элементы проекта. Атрибуты - это часть метаинформации, содержащейся в манифесте сборки. Когда говорится, что в момент связывания атрибута с программным элементом конструктор атрибутного класса создает объект соответствующего класса, это некоторая метафора, поясняющая семантику. Реально объекты не создаются, а просто в манифест сборки добавляется соответствующая информация о данном программном элементе. Понятно, что манифест сборки в первую очередь описывает саму сборку, в том числе и атрибуты, характеризующие сборку. Как же задать эти атрибуты, ведь в программном тексте нет объявления сборки? Все очень просто - атрибуты, связанные со сборкой или с модулем, задаются в самом начале программного текста
любого класса, сразу после предложений using, еще до задания пространства имен. Синтаксически связывание атрибута со сборкой или с модулем достигается за счет того, что конструктору атрибута может предшествовать задание цели, отделяемое от конструктора двоеточием. Вот, например, как можно задать атрибуты для сборки и для модуля при объявлении знакомого нам класса Prog_Properties, который входит в проект ConsoleAttributes, содержащего примеры этой лекции:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
// Установить CLSCompliant атрибут как true
// Этот атрибут применим для сборок /target:assembly.
//Проверяет соответствие сборки общеязыковым спецификациям
[assembly: CLSCompliant(true)]
// Связывание атрибута Description с модулем проекта.
[module: Description("Описание модуля проекта ConsoleAttributes")]
namespace ConsoleAttributes
{
/// <summary>
/// Свойства претендентов на должность программиста,
/// описывающие знание технологий и языков программирования
/// </summary>
//[FlagsAttribute]
//[FlagsAttribute()]
[Flags]
public enum Prog_Properties
{
VB = 1, C_sharp = 2, C_plus_plus = 4,
Web = 8, Prog_1C = 16
}
}
Все атрибуты, связываемые со сборкой и модулем одного проекта и заданные при объявлении разных классов проекта, собираются вместе в манифесте сборки. Уточню, что модулем проекта является переносимый исполняемый файл проекта - PE-файл проекта.
При связывании атрибутов со сборкой и модулем задание цели обязательно, поскольку в программном коде нет объекта, с которым связывается атрибут. Но это не единственный случай, когда задание цели необходимо. Рассмотрим, например, связывание атрибута с методом и значением, возвращаемым этим методом:
[method: Description("Метод, позволяющий получить свойства кандидатов")]
[return: Description("Массив строк со свойствами кандидатов")]
public string[] GetStrCands() {}
Атрибут Description может связываться как с методом, так и с возвращаемым значением. Если он задан в единственном экземпляре и цель не задана, то атрибут по умолчанию связывается с наиболее общим случаем - с методом. Задание цели уточняет, с какой сущностью следует связать атрибут. Заметьте, цель можно задавать всегда, даже когда неоднозначность связывания не возникает.
Как связать атрибуты с модулем и сборкой, понятно. А как решить обратную задачу - выяснить, какие атрибуты связаны со сборкой и модулем? Для этого нужно уметь получать объекты класса Assembly и Module. Покажем, как это делается, извлекая информацию об атрибутах для сборки, модуля, метода и возвращаемого значения. Пример извлечения атрибутов для класса уже появлялся. Расширим его теперь на сущности других типов.
public void TestGetEntityAttributes()
{
//Получение атрибутов сборки и модуля
Type clstype = typeof(Prog_Properties);
Assembly projectAssembly = clstype.Assembly;
Module projectModule = clstype.Module;
Attribute[] assemblyAttributes =
Attribute.GetCustomAttributes(projectAssembly);
Console.WriteLine("Атрибуты сборки");
foreach (Attribute attr in assemblyAttributes)
Console.WriteLine(attr);
Attribute[] moduleAttributes =
Attribute.GetCustomAttributes(projectModule);
Console.WriteLine("Атрибуты модуля");
foreach (Attribute attr in moduleAttributes)
Console.WriteLine(attr);
}
Заметьте, что существуют классы Assembly и Module. Получить объекты этих классов, связанных с проектом, достаточно просто. Можно создать объект класса Type для одного из классов проекта, а затем использовать свойства Module и Assembly, возвращающие нужные нам объекты. Далее метод GetCustomAttributes, которому передаются в качестве первого аргумента объекты, задающие сборку или модуль, будет возвращать массив связанных с ними атрибутов. Уже отмечалось, что метод перегружен, но у него есть реализации, где первый аргумент принадлежит классам Assembly и Module. На рис. 9.3 показаны результаты работы этого теста.
(рис 9.3) Атрибуты сборки и модуляОбратите внимание, что у сборки по умолчанию установлено большое число атрибутов. У модуля таких устанавливаемых по умолчанию атрибутов нет, так что в данном случае у модуля появился только тот атрибут, который был установлен нами в тексте программы.
Покажем теперь, как, используя многообразные классы отражения, получить атрибуты метода класса и возвращаемого значения. Добавим в тестирующий метод TestGetEntityAttributes следующий код:
//Получение атрибутов метода и возвращаемого значения
Type typeJob = typeof(Job);
MethodInfo classMethod = typeJob.GetMethod("GetStrCands");
Attribute[] methodAttributes =
Attribute.GetCustomAttributes(classMethod);
Console.WriteLine("Атрибуты метода");
foreach (Attribute attr in methodAttributes)
Console.WriteLine(attr);
Console.WriteLine("Атрибуты вовращаемого значения");
ParameterInfo returnMethod = classMethod.ReturnParameter;
foreach(Attribute attr in returnMethod.GetCustomAttributes(false))
Console.WriteLine(attr);
Первым делом получен объект typeJob, задающий тип класса. С использованием метода GetMethod получен объект classMethod класса MethodInfo. Этот объект содержит подробную информацию о методе класса. В частности, можно получить атрибуты, связанные с методом. Это можно делать двояким способом, либо вызывая статический метод GetCustomAttributes класса Attribute, либо вызвать MethodInfo.
Созданный объект classMethod позволяет получить объект returnMethod класса ParameterInfo, содержащий информацию о значении, возвращаемом методом. Для получения атрибутов объектов classMethod и returnMethod используется в первом случае статический метод, во втором, для разнообразия, - GetCustomAttributes.
Для полноты картины рассмотрим, как получить не только сам атрибут, связанный с сущностью, но и параметры атрибута, если таковые имеются. У атрибута Description, приписанного в нашем примере к методу и к возвращаемому значению, есть параметр, задающий описание сущности. Это описание задавалось в момент объявления атрибута. Теперь постараемся извлечь его:
//Работа с атрибутом Description
Console.WriteLine("Получение параметров атрибута Description");
DescriptionAttribute des;
MemberInfo specMethod = typeJob.GetMethod("GetStrCands");
if (Attribute.IsDefined(specMethod, typeof(DescriptionAttribute)))
{
des = (DescriptionAttribute)Attribute.GetCustomAttribute(specMethod,
typeof(DescriptionAttribute));
Console.WriteLine(des.Description);
}
ParameterInfo retMethod = classMethod.ReturnParameter;
des = (DescriptionAttribute)Attribute.GetCustomAttribute(retMethod,
typeof(DescriptionAttribute));
Console.WriteLine(des.Description);
Главной особенностью этого фрагмента кода, на которую следует обратить внимание, является то, что для получения конкретного атрибута целесообразно использовать статический метод GetCustomAttribute, который есть только у класса Attribute. Важно также и то, что полученный атрибут нужно привести к заданному типу. Тогда соответствующий объект позволит добраться и до параметров атрибутов. Взгляните на результаты работы двух последних фрагментов кода.
(рис 9.4) Атрибуты метода и параметры
В FlagsAttribute находится в пространстве System, а класс DescriptionAttribute, используемый в наших примерах, находится в пространстве имен System.ComponentModel. Атрибуты активно применяются при построении самых разных
Встроенные атрибуты активно используют и программисты, создавая собственные классы, предназначенные для решения задач проблемной области. Встроенные атрибуты, примененные к собственным классам, представляют декларативный способ, позволяющий изменить семантику выполнения методов класса. Наш пример использования атрибута Flags для перечислений демонстрировал такую возможность.
Программист, создавая класс, связывает с ним встроенный атрибут. Компилятор извлекает метаинформацию, заданную атрибутом. Поскольку встроенные атрибуты компилятору известны, компилятор принимает решение о том, какой код построить, в зависимости от того, задан атрибут или нет, какие параметры у атрибута, если они у него есть. Рассмотрим в качестве примера несколько встроенных атрибутов, часто используемых программистами при создании собственных классов.
Если класс объявить с атрибутом [, то в него встраивается стандартный механизм сериализации. Необходимость в сериализации объектов зачастую возникает при работе с программной системой. Под сериализацией понимают процесс сохранения объектов в долговременной памяти (файлах). Под десериализацией понимают обратный процесс - восстановление состояния объектов, хранимого в долговременной памяти. Механизмы сериализации C# и Framework .Net поддерживают два формата сохранения данных - в бинарном файле и XML- файле. В первом случае данные при сериализации преобразуются в бинарный поток символов, который автоматически при [NonSerialized], - эти поля сохраняться не будут.
Сериализация позволяет запомнить состояния системы объектов с возможностью последующего возвращения к этим состояниям. Сериализация необходима, когда
Еще одно важное применение сериализации - это обмен данными удаленных систем. При удаленном обмене данными предпочтительнее формат xml из-за открытого стандарта передачи данных в Интернете по soap-протоколу, из-за открытого стандарта на структуру xml-документов. Обмен данными становится достаточно простым даже для систем, построенным на разных платформах и в разных средах разработки.
Так же как и клонирование, сериализация может быть поверхностной, когда сериализуется единственный объект, и глубокой, когда, начиная с корневого объекта, сериализуется совокупность объектов, связанных взаимными ссылками (граф объектов). Глубокую сериализацию самому организовать непросто, так как она требует, как правило,
Стандартный механизм сериализации поддерживает, что крайне приятно, глубокую сериализацию. Если по каким-либо причинам стандартная сериализация нас не устраивает, то класс следует объявить наследником интерфейса ISerialzable, реализация методов которого позволит управлять процессом сериализации.
Атрибутный класс SerializableAttribute определен следующим образом:
[ComVisibleAttribute(true)] [AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Struct|AttributeTargets.Enum|AttributeTargets.Delegate, Inherited = false)] public sealed class SerializableAttribute : Attribute
Класс не может быть наследован. Атрибут сериализации может быть задан для таких сущностей, как класс, структура, перечисление и делегат.
В качестве примера рассмотрим два класса Teacher и Student, связанных взаимными ссылками и допускающих сериализацию объектов. Построим простую модель, в которой создаются объекты этих классов. В некоторый момент состояние этих объектов запоминается за счет использования механизма сериализации. Затем состояние объектов изменяется. А потом, с использованием Teacher:
[Serializable]
class Teacher
{
string name;
Student[] students;
public Teacher(string name, Student[] students)
{
this.name = name;
this.students = students;
}
public Student[] Students
{
get { return students; }
}
}
Для класса задан атрибут сериализации. У класса два поля: одно задает имя учителя, другое - его учеников. У класса есть конструктор с параметрами и свойство, позволяющее получить доступ к закрытому полю students.
Класс Student также устроен достаточно просто:
[Serializable]
class Student
{
string name;
Teacher teacher;
string topic;
public Student(string name, Teacher teacher)
{
this.name = name;
this.teacher = teacher;
}
public string Topic
{
get { return topic; }
set { topic = value; }
}
}
Для класса также задан атрибут сериализации. У класса три поля, которые задают имя студента, учителя и тему работы, выполняемой под руководством учителя. У класса есть конструктор с параметрами и свойство, позволяющее получить доступ к закрытому полю topic.
А теперь построим модель, в которой создаются и существуют объекты этих классов. Как обычно, в классе Testing построен соответствующий метод:
public void TestTeacherAndStudents()
{
string teacherName = "Иванов Л.Д.";
Teacher teacher;
Student[] students = new Student[3];
string[] studNames =
{"Зайцев Г.Ю.","Гулевич С.А.","Маргулис В.А."};
teacher = new Teacher(teacherName, students);
students[0] = new Student(studNames[0], teacher);
students[1] = new Student(studNames[1], teacher);
students[2] = new Student(studNames[2], teacher);
В нашей модели созданы объекты teacher и students, задающие учителя и его студентов. Зададим теперь темы работ студентов и сохраним это состояние объектов в бинарном файле.
students[0].Topic = "thema0";
students[1].Topic = "thema1";
students[2].Topic = "thema2";
Console.WriteLine("Темы студентов");
for (int i = 0; i < 3; i++)
Console.WriteLine("Студент {0}, тема: {1} ",
studNames[i], students[i].Topic);
Stream tasFile, satFile;
tasFile = File.Create("teach1.bin");
satFile = File.Create("teach2.bin");
BinaryFormatter bif = new BinaryFormatter();
bif.Serialize(tasFile, teacher);
bif.Serialize(satFile, students[0]);
tasFile.Close();
satFile.Close();
Console.WriteLine("Темы запомнили!");
Для полноты описания картины . Метод Serialize определен для класса только тогда, когда у класса есть соответствующий атрибут.
Представим себе, что после некоторого обсуждения темы работ решено было изменить:
Console.WriteLine("Меняем темы!");
students[0].Topic = "thema7";
students[1].Topic = "thema8";
students[2].Topic = "thema9";
teacher = null;
for (int i = 0; i < 3; i++)
Console.WriteLine("Студент {0}, тема: {1} ",
studNames[i], students[i].Topic);
Для полноты картины у студентов не стало учителя. Видимо, произошел конфликт. Но потом все уладилось, и нужно вернуться к исходному состоянию. В этом поможет
Console.WriteLine("Десериализация. Восстанавливаем состояние");
tasFile = File.Open("teach1.bin",FileMode.Open, FileAccess.Read);
bif = new BinaryFormatter();
teacher = (Teacher)bif.Deserialize(tasFile);
students = teacher.Students;
for (int i = 0; i < 3; i++)
Console.WriteLine("Студент {0}, тема: {1} ",
studNames[i], students[i].Topic);
}
Используя корневой объект, в данном случае teacher, восстанавливаем весь граф объектов, связанных с корнем. Но, заметьте, остальные объекта графа - это внутренняя структура, связанная с корнем. Восстановить объект students нужно явно, используя свойство Students объекта teacher. На рис. 9.5 показаны результаты работы тестового примера.
(рис 9.5) Сериализация объектов
Атрибутный класс ConditionalAttribute позволяет программисту создавать так называемые условно компилируемые классы и методы.
Вот как выглядит заголовок атрибутного класса:
[SerializableAttribute] [AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true)] [ComVisibleAttribute(true)] public sealed class ConditionalAttribute : Attribute
Как видно из описания, атрибут Conditional можно задавать для классов и методов. У класса ConditionalAttribute есть позиционный параметр, содержательно задающий параметр компиляции. Когда для некоторой сущности (класса или метода) задается атрибут Conditional, то конструктору класса передается строка с фактическим значением параметра. Его значение сравнивается со значением параметра
Только атрибутные классы могут быть условно компилируемыми. Для обычных классов атрибут Conditional задаваться не может. Если для условного атрибутного класса условие компиляции выполняется, то задание этого атрибута для некоторой сущности будет иметь эффект. В случае невыполнения условия компиляции задание атрибута игнорируется.
Если некоторый метод задан с атрибутом Conditional, то метод является условно компилируемым. Это означает, что в момент компиляции компилятор проверит выполнение условия компиляции, сравнив значения двух параметров, заданного в конфигурации и переданного конструктору атрибута Conditional. Если условие компиляции истинно (значения параметров совпадают), то метод будет компилироваться, а иначе не будет компилироваться. Вызов метода, для которого условие компиляции не выполняется, игнорируется, но не приводит к ошибке в период выполнения.
Рассмотрим класс, в котором заданы три метода с атрибутом
[Conditional("DEMO")]
void MainMethod()
{
Console.WriteLine("Это демо версия");
}
[Conditional("PROF")]
void MainMethod(string s)
{
Console.WriteLine("Это профессиональная версия");
}
[Conditional("DEBUG")]
void DebugMethod()
{
Console.WriteLine("Это отладочная версия");
}
У первых двух методов имена совпадают, но сигнатуры отличаются. Содержательно один из методов соответствует демоверсии, а второй - профессиональной версии системы. Третий метод содержательно задает метод, используемый только на этапе отладки. Все три метода являются условно компилируемыми, и выполнение условия компиляции зависит от значения константы, переданной конструктору атрибута Conditional, и
Все три метода закрыты и непосредственно клиентами не вызываются. Рассмотрим метод, доступный клиентам класса:
public void ClientMethod(string s)
{
MainMethod();
MainMethod(s);
DebugMethod();
}
В этом методе вызываются все три метода с условной компиляцией. Какие из этих трех методов реально будут работать, зависит от того, какая конфигурация проекта будет вызвана и какие параметры в ней заданы. По умолчанию строятся две конфигурации проекта - Debug и Release. Первая используется на этапе отладке, вторая - с включенным режимом оптимизации задает рабочую версию системы. В конфигурации Debug по умолчанию определены две константы DEBUG и TRACE. В конфигурации Release определена по умолчанию только вторая из этих констант. В каждой конфигурации на странице свойств проекта можно задать произвольное число собственных DEMO, а для конфигурации Release - PROF. На рис. 9.6 показана страница свойств проекта для конфигурации Debug с заданными значениями констант.
(рис 9.6) Константы условной компиляции конфигурации DebugРассмотрим теперь клиентский класс Testing и его метод, вызывающий исследуемые методы:
public void TestConditional()
{
Console.WriteLine("Demo or Profi?");
ClassDemo demo = new ClassDemo();
demo.ClientMethod("");
}
Запустим наш проект на выполнение в конфигурации Debug. В этой конфигурации определены константы DEBUG и DEMO, так что два метода из трех вызываемых должны сработать. Результаты можно увидеть на рис. 9.7.
(рис 9.7) Результаты работы в конфигурации DebugЕсли же запустить проект в конфигурации Release, где определена только одна константа PROF, то будет работать только "профессиональный метод", а вызов остальных двух методов будет проигнорирован.
Ничто не мешает создать собственный атрибутный класс и связывать сущности программы с атрибутами этого класса. Действия компилятора в этом случае сводятся к тому, что он информацию, заданную атрибутом, размещает как часть метаинформации проекта. Понятно, что компилятору ничего не известно о сути определяемого нами атрибутного класса, и поэтому включение атрибута никак не отражается на коде, создаваемом компилятором. Чтобы собственные атрибуты оказали влияние на код, необходимо включить процесс отражения. Это означает, что в какой-то части программы информация об атрибутах, сопровождающих сущности, должна извлекаться, анализироваться, и на этой основе должны приниматься те или иные решения.
Когда следует создавать собственные атрибутные классы? Если разрабатывается DLL - динамическая библиотека классов, подключаемая к различным проектам, то создание атрибутных классов, сопровождающих эту библиотеку, и связывание программных сущностей библиотеки с этими атрибутами может быть весьма полезным. Проекты, присоединяющие эту библиотеку, могут из анализа атрибутов извлечь полезную информацию, позволяющую корректно работать с классами библиотеки. В более общей ситуации некое хост-приложение работает с дополнительными приложениями (add in), используя отражение и информацию, извлекаемую из атрибутов для обеспечения корректного взаимодействия.
Еще одна причина создания атрибутов состоит в том, что атрибуты позволяют сохранять полезную информацию о приложении, играя роль документации. Часто использование атрибутов для этих целей полезнее, чем другие возможные способы - комментарии в тексте, тэги, файлы или базы данных.
Рассмотрим пример создания собственного атрибутного класса. Атрибуты этого класса позволят запоминать историю создания и модификации программных сущностей проекта. В атрибуте будет сохраняться информация об авторе, который создает или вносит изменения в код, дате внесения изменений и сопутствующий комментарий.
[AttributeUsage (AttributeTargets.All,
AllowMultiple = true, Inherited = true)]
class HistoryAttribute: Attribute
{
//позиционные параметры
string author;
string date;
// именованный параметр
public string comment;
Атрибут Usage заданный для нашего атрибутного класса History, указывает, что атрибут может быть задан для всех программных сущностей и может появляться в нескольких экземплярах. Поля этого класса содержат информацию, которую предполагается задавать в момент определения атрибута. Автор и дата относятся к обязательным параметрам. Именованный параметр comment может опускаться.
Дополним класс заданием конструктора и методами свойствами, осуществляющими доступ к закрытым полям класса:
public HistoryAttribute(string author, string date)
{
this.author = author; this.date = date;
}
public string Author
{
get { return author; }
}
public string Date
{
get { return date; }
}
}
Теперь атрибутный класс полностью определен, и его можно использовать в процессе разработки программного проекта. В качестве модельного примера приведу класс, сохраняющий историю разработки:
[History("В. Биллиг", "30.03.2009",
comment = "Совместный проект. Будем работать!")]
[History("М. Дехтярь", "30.03.2009",
comment = "Проект: Наш класс")]
[History("И. Муссикаев", "30.03.2009",
comment = "Три автора")]
class OurClass
{
[History("И. Муссикаев", "3.04.2009",
comment = "Первый вариант алгоритма")]
[History("И. Муссикаев", "7.04.2009",
comment = "Улучшеный вариант")]
public void First()
{
}
[History("М. Дехтярь", "3.04.2009",
comment = "Первый вариант алгоритма")]
[History("М. Дехтярь", "7.04.2009",
comment = "Улучшение характеристик сложности")]
public void Second()
{
}
[History("В. Биллиг", "2.04.2009",
comment = "Первый вариант реализации алгоритма")]
[History("В. Биллиг", "5.04.2009",
comment = "Изменение спецификаций")]
public void Third()
{
}
}
Как видите, атрибут позволяет хранить вместе с кодом и историю внесения изменений. Процесс отражения позволит при необходимости извлечь историю разработки. Объяснение того, как это можно делать, уже дано. Поэтому просто приведу код, выполняющий эту работу:
public void TestOurClass()
{
//История разработки класса
Console.WriteLine("История разработки класса");
Type clstype = typeof(OurClass);
Attribute[] attrs;
HistoryAttribute history;
attrs = Attribute.GetCustomAttributes(clstype);
foreach(Attribute attr in attrs)
{
history = attr as HistoryAttribute;
if (history != null)
Console.WriteLine("автор: {0}", history.Author);
Console.WriteLine(" дата начала разработки: {0} ",
history.Date);
Console.WriteLine("комментарий: {0} ",
history.comment);
}
Console.WriteLine("История разработки методов");
MethodInfo[] methods = clstype.GetMethods();
foreach (MethodInfo method in methods)
{
attrs = Attribute.GetCustomAttributes(method);
foreach(Attribute attr in attrs)
{
history = attr as HistoryAttribute;
if (history != null)
Console.WriteLine("Метод: {0} ", method.Name);
Console.WriteLine("автор: {0}", history.Author);
Console.WriteLine(" дата начала разработки: {0}",
history.Date);
Console.WriteLine("комментарий: {0} ",
history.comment);
}
}
}
На рис. 9.8 приведены результаты работы этого метода.
(рис 9.8) История разработкиОдним из важных качеств кода, соответствующего требованиям промышленной разработки, является ясность понимания кода. Код должен быть понятным автору не только в момент его создания. Код должен оставаться понятным в процессе его сопровождения через длительное время после его создания, когда работать с программным продуктом придется разработчику, возможно, не знакомому с автором исходного кода.
В языке C# большое внимание уделяется средствам документирования кода. Разработчики кода могут комментировать код, используя специальный синтаксис, содержащий XML-текст. Комментарии, использующие этот синтаксис, называются документируемыми комментариями. Для поддержки работы с такими комментариями имеется специальный инструментарий - генератор документации, - позволяющий извлекать такие комментарии и строить автоматически файл документации - и браузер
объектов являются частью такого инструментария. Эти средства отображают информацию, заданную в тегах summary, param, и некоторых других тегах. Интеллектуальная подсказка крайне важна при работе клиентов с классом. Поэтому профессиональный стиль разработки требует, чтобы все открытые для клиентов и потомков сущности класса были снабжены документируемыми комментариями. Хотелось бы иметь как отдельный инструментарий и браузер отчетов, но в настоящее время он не входит в состав инструментария Visual Studio 2008.
В основе любого XML-текста лежит понятие тега. Текст имеет скобочную структуру. Тег задается парой - открывающим тегом и закрывающим тегом. В языке C# определен целый ряд стандартных тегов, имеющих фиксированный смысл и используемых в определенном контексте. Зная смысл тегов, компилятор проверяет соответствие контекста заданному тегу и выдает предупреждение, если соответствие нарушается. Теги включаются в состав документируемого комментария.
Синтаксис комментариев позволяет генератору создавать
/// Это однострочный комментарий /** * Это многострочный комментарий с ограничителями */
Первая форма соответствует однострочным комментариям, вторая допускает расположение комментария на нескольких строках. Оба комментария должны непосредственно предшествовать программной сущности, с которой они связаны. В роли сущностей могут выступать классы и все их частные случаи, а также поля, свойства, методы и события класса. Если аннотируемая сущность имеет атрибуты, то атрибуты рассматриваются как часть объявления сущности, и комментарии непосредственно предшествуют атрибутам. Заметьте, обычный строчный комментарий начинается с двух символов слеша, а документируемый - с трех, что позволяет компилятору находить документируемый комментарий. Для многострочных комментариев признаком документируемого комментария являются две звездочки, идущие за слешем, в отличие от одной звездочки обычного комментария.
Комментарии в отличие от атрибутов не включаются в метаинформацию, и, следовательно, не могут быть извлечены в процессе отражения. Но они могут быть включены в файл документации проекта. Стандартные теги имеют предопределенный смысл и функциональность. Программист при желании может задавать собственные теги. Единственное, что от него требуется в этом случае, - это соблюдение общих синтаксических правил, предъявляемых к XML-тексту. В табл. 9.1 приведен набор стандартных тегов C# с пояснением их смысла.
| Тег | Смысл |
|---|---|
<c> |
Для тела тега - текста, размещенного между открывающим и закрывающим тегом, используется шрифт, применяемый для кода. |
<code> |
Аналогичен тегу <c>. Обычно первый тег применяется для кратких текстов, второй для длинных текстов из нескольких строк. |
<example> |
Применяется, когда в комментариях встречается пример кода. Обычно сочетается с тегом <code> |
<exception> * |
Обычно связывается с методом и описывает исключения, которые могут быть выброшены в процессе работы метода. Синтаксис: <exception cref = "
Выполняется проверка, существует ли указанный |
<include>*
Stop!!! |
Для больших проектов может существовать общий XML-файл с документацией. Тег include позволяет ссылаться на нужное место в этом файле. Синтаксис: <
Выполняется проверка, существует ли указанный файл и путь к тегу. |
<list> |
Позволяет внутри комментария задавать структуру списка или таблицы, что улучшает читаемость отчета документации. |
< |
Позволяет внутри комментария задавать абзацы с той же целью улучшения читаемости отчета. |
<param>* |
Позволяет комментировать параметры метода. Обычно строится автоматически вместе с тегом summary, когда документируемый комментарий вставляется перед методом. Текст описания отображается в подсказке и в браузере объектов.
Выполняется проверка, существует ли указанный параметр у комментируемого метода. |
<paramref> |
Позволяет внутри комментария указать, что некоторое слово текста является параметром (ссылкой на параметр метода), - это позволяет в отчете выделить это слово. |
<permission>* |
Позволяет документировать безопасность доступа, задавая ссылку на поле или метод, доступный для вызова в данном окружении. |
< |
Задает описание программной сущности. Обычно это некоторое дополнение - ремарка к основному описанию, задаваемому тегом summary. Но иногда тег используется вместо тега summary. Следует иметь в виду, что в этом случае описание, заданное тегом, не отображается в подсказке , хотя и отображается в браузере объектов. |
<returns> |
Тег returns позволяет описать значение, возвращаемое методом. Когда документируемый комментарий вставляется перед началом метода, достаточно набрать три слеша - признак начала документируемого комментария, как автоматически в комментарий добавляются тег summary и теги param и returns, если метод имеет параметры и возвращаемое значение, отличное от void. |
<see>* |
Позволяет внутри описания (в теге summary ) сделать ссылку на поле или метод. Синтаксис:<see cref = "member" |/>Выполняется проверка существования элемента, заданного ссылкой. Пример: ///<summary>
///Метод Move перемещает точку на плоскости в новое положение.
///В отличие от метода < see cref = "Offset" />, выполняющего сдвиг точки.
///</summary>
public void Move(int x, int y) {…}
|
<seealso>* |
Аналогичен тегу <see>. Создает в файле документации специальный раздел "смотри также". |
<summary> |
Задает описание полей и методов. Вставляется автоматически, как только заданы три слеша, начинающие документирующий комментарий. Описание, заданное тегом, показывается в подсказке и в браузере объектов. Если строится файл документации, то требуется задание этого тега для всех public полей и методов. В противном случае выдаются предупреждение, что элемент не имеет комментария, что нарушает правила стиля профессионального программирования. |
<typeparam>* |
Позволяет описать параметр типа для summary, param, returns автоматически добавляется в документирующий комментарий.
Выполняется проверка, что соответствующий параметр действительно есть у класса или метода. |
<typeparamref> |
Задает ссылку на параметр типа. |
<value>* |
Позволяет описать метод-свойство. |
В примерах данного курса документируемые комментарии создавались для многих открытых сущностей класса, что отвечает правилам стиля профессионального программирования. Иногда требования стиля не соблюдались по причине увеличения объема текста, затрудняющего его восприятие. Для программного текста увеличение объема не имеет принципиального значения, поскольку текст комментария всегда может быть свернут, не мешая восприятию программного кода. Но, когда программный код копируется и становится частью текста книги, свертка не действует, и комментарий появляется в развернутом виде, мешая иногда восприятию программного кода. По этой причине в примерах документируемые комментарии построены далеко не всюду, где они должны появляться в соответствии с правилами стиля. Тем не менее было много вариантов использования основных тегов - summary, param, returns. Приводить примеры применения всех тегов нет особой необходимости. В табл. 9.1 дано
достаточно подробное описание их назначения. Ограничусь одним примером использования тега exception, позволяющего описать причину исключения, которое может возникнуть в ходе работы метода класса. Давайте слегка модифицируем уже известный нам пример одного из методов класса OurClass:
/* * *
<summary>
Третий метод. Лучший из всех методов.
</summary>
* */
/// <exception cref = "OurClassException">
/// Исключение возникает после дождика в четверг!
/// Оно будет перехвачено и обработано.
/// </exception>
[History("В. Биллиг", "2.04.2009",
comment = "Первый вариант реализации алгоритма")]
[History("В. Биллиг", "5.04.2009",
comment = "Изменение спецификаций")]
public void Third()
{
const string message = "Дождь в четверг!";
const string result = "Хорошо собирать грибы!";
try
{
bool rainInThursday = true;
if (rainInThursday) throw (new OurClassException(message));
}
catch (OurClassException e)
{
Console.WriteLine(e.Message);
Console.WriteLine(result);
}
}
У метода два атрибута и два тега. Тег summary размещен в многострочном комментарии, тег exception - в однострочном. В теге exception задан параметр cref, задающий имя класса исключений. Подобный параметр имеется во многих тегах, где он задает имя некоторой сущности. Такие теги являются тегами с проверкой - проверяется существование сущности, заданной параметром cref. Для класса OurClass создан, как положено, OurClassException. Поэтому проверка, проводимая для тега exception, закончится успехом, обнаружив этот класс.
Приведу пример фрагмента XML отчета, связанного с рассматриваемым методом:
<member name="M:ConsoleAttributes.OurClass.Third">
<summary>Третий метод. Лучший из всех методов. </summary>
<exception cref="T:ConsoleAttributes.OurClassException">
Исключение возникает после дождика в четверг!
Оно будет перехвачено и обработано.
</exception>
</member>
Заметьте, вся информация из тегов попала в отчет. Благодаря проверке приводится полная информация, включающая имя пространства имен, имя класса и имя сущности. Однобуквенные идентификаторы, предшествующие описанию сущности, характеризуют ее тип ( M - метод, T - исключение, F - поле и так далее).
Атрибуты и теги играют важную роль в процессе разработки и сопровождения программной системы. Они являются формально не обязательными, декларативными элементами, связанными с программной сущностью, но включать их в программный текст необходимо. Встроенные атрибуты и теги существенно упрощают разработку. Но во многих случаях полезно и нужно создавать собственные классы атрибутов и использовать собственные теги в процессе создания отчета по документации.
Программный текст должен быть не только хорошим. Он должен быть хорошо документирован. Без тегов и атрибутов в этом случае не обойтись.
Person, Student, Car, Firm. Задайте стандартные атрибуты для классов, полей, методов, параметров и возвращаемых значений методов класса. Постройте классы PersonReflection и другие, где, используя стандартные классы отражения из пространства имен Reflection, получите информацию о сущностях построенных классов. Постройте соответствующий интерфейс Windows-проекта, позволяющий получать информацию о классе. Можно ли построить универсальный класс TReflection с родовым параметром T, позволяющий получать информацию о произвольном классе T?Person, Student, Car, Firm собственными атрибутами. Постройте процесс отражения, получая информацию об атрибутированных сущностях классов.Person, Student, Car, Firm. Классы должны включать события и предусматривать обработку исключительных ситуаций. Задайте в каждом классе стандартные теги всех возможных видов. Постройте соответствующий интерфейс Windows-проекта, позволяющий получать полный отчет о классе и частные отчеты о деталях класса - полях класса, методов класса, событиях, исключительных ситуациях.Проект к данной лекции Вы можете скачать здесь.
Атрибуты многократно упоминались в тексте этого курса. Всюду, где речь шла об объявлении сущностей, говорилось, что объявление может сопровождаться указанием атрибутов этой сущности. Но всякий раз говорилось, что атрибуты необязательны, и можно их не задавать. Пришла пора рассмотреть, что представляют собой атрибуты, где и когда их следует использовать. Атрибуты формально не относятся к программному коду. Это декларативная информация, некоторое описание, сопровождающее код. Информация, заданная атрибутом, преобразуется компилятором в метаинформацию, сопровождающую сборку. Метаинформация доступна исполняемой программе благодаря процессу, называемому отражением. В библиотеке CLR есть классы, позволяющие извлечь метаинформацию и анализировать ее. Результаты анализа, естественно, могут влиять на дальнейший ход выполнения программы.
Как и полагается в объектном программировании, атрибуты - это настоящие объекты, принадлежащие различным атрибутным классам. Прародителем всех атрибутных классов является класс с именем Attribute. Все атрибутные классы из Framework .Net являются потомками этого класса, и их имена заканчиваются суффиксом Attribute, который, впрочем, можно опускать при работе с атрибутами. Когда программист создает собственные атрибуты, то он, естественно, определяет собственный атрибутный класс, который также должен быть потомком прародителя атрибутов - класса Attribute и, для следования правилам хорошего тона оформления кода, иметь соответствующий суффикс в имени.
Где могут появляться атрибуты? Атрибуты могут задаваться для самых разных сущностей - классов, структур, интерфейсов, перечислений, делегатов, событий, полей и методов класса, параметров метода и возвращаемых значений. Атрибуты могут быть также заданы для всей сборки и модуля. Атрибуты задаются в точке объявления сущности. Синтаксически атрибут, связываемый с сущностью, заключается в квадратные скобки, непосредственно предшествуя описанию сущности. С одной сущностью могут быть связаны несколько атрибутов. О разных синтаксических формах задания атрибутов поговорим чуть позже, а сейчас рассмотрим первый пример использования атрибутов.
В лекции, посвященной перечислениям, подробно рассматривался частный, но важный случай перечислений, представляющий собой шкалы - строки битов. Для работы с перечислениями создан атрибутный класс FlagsAttribute. Появление этого атрибута у перечисления указывает компилятору, что перечисление задает шкалу. Как следствие, изменяется семантика работы методов ToString и Format для перечислений. Метод ToString для объекта перечисления, задающего комбинацию битов, возвращает в качестве результата число, если атрибут Flags не задан, и возвращает совокупность значений, заданных перечислением, если атрибут определен. Аналогичным образом меняется семантика метода Format.
Вернемся к примеру из лекции 3, где речь шла о фирме, принимающей программистов на работу. В этом примере был создан специальный класс Job для работы с кандидатами, претендующими на занятие вакансий, описание которого теперь приводить не буду. Свойства кандидатов задавались перечислением. Добавим теперь к этому перечислению атрибут Flags:
/// <summary>
/// Свойства претендентов на должность программиста,
/// описывающие знание технологий и языков программирования
/// </summary>
[Flags]
//[FlagsAttribute]
//[FlagsAttribute()]
public enum Prog_Properties
{
VB = 1, C_sharp = 2, C_plus_plus = 4,
Web = 8, Prog_1C = 16
}
Здесь приведены три формы записи атрибута. Синтаксически они разнятся, но по сути эквивалентны. Наиболее употребительной является первая краткая форма записи атрибута, где указывается имя класса с опущенным суффиксом. Во второй форме указывается полное имя класса. Третья - полная форма записи - показывает, что фактически вызывается конструктор класса, создающий объект. В случае вызова конструктора без параметров скобки можно опускать так же, как и суффикс в имени класса. Задавать многократно атрибут Flags для одной и той же сущности нельзя, так что две формы из трех закомментированы.
В общем случае атрибутный класс имеет параметры. В данном конкретном случае у атрибутного класса Flags нет параметров, поскольку важен лишь сам факт задания атрибута. Если атрибут задан, то компилятор при работе с перечислением выбирает одну реализацию методов ToString и Format, не задан - другую.
Рассмотрим знакомый по лекции 3 тест для работы с перечислением:
/// <summary>
/// Тестирование процесса приема на работу
/// </summary>
public void TestJob()
{
Prog_Properties pattern = Prog_Properties.C_sharp |
Prog_Properties.Web;
Console.WriteLine("Требования, заданные образцом:" +
" Знание языка С# и Web-технологии");
int n = 10;
Job mys = new Job(n);
mys.FormCands();
Prog_Properties[] cand = mys.GetCands();
//string[] strCand = mys.GetStrCands();
for (int i = 0; i < n; i++)
{
Console.WriteLine("Свойства кандидата[{0}] - {1}",
i, cand[i].ToString());
//Console.WriteLine(strCand[i]);
}
}
Обратите внимание на закомментированные строки:
string[] strCand = mys.GetStrCands(); Console.WriteLine(strCand[i]);
Ранее, когда атрибут Flags не задавался, я сам создавал реализацию метода ToString, учитывающую особенности работы со шкалами. Теперь при наличии атрибута Flags это становится излишним. Вот как выглядят результаты работы теста.
(рис 9.1) Перечисления и атрибут FlagsИтак, рассмотрен пример простого атрибутного класса. Атрибут Flags может быть задан только для классов, более того - для классов, задающих перечисления, более того - для перечислений, задающих шкалы. Конечно же, есть атрибуты, имеющие более широкое применение.
Рассмотрим, как устроен класс Attribute, потомками которого являются все атрибутные классы. Вот описание этого класса:
[SerializableAttribute] [ClassInterfaceAttribute(ClassInterfaceType.None)] [ComVisibleAttribute(true)] [AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = false)] public abstract class Attribute : _Attribute
Класс Attribute является абстрактным классом, наследником интерфейса _Attribute. Обратите внимание: атрибуты задаются и при объявлении класса Attribute, так же как и для любого другого класса. Как и положено, класс наследует все методы родительского класса object и определяет методы, заданные интерфейсом _Attribute. У класса определен конструктор без аргументов, ряд статических и динамических (экземплярных) методов. Есть у класса и одно-единственное свойство. Рассмотрим подробнее некоторые из методов класса.
Все рассматриваемые в этом разделе методы перегружены. Вот одна из реализаций метода GetCustomAttribute:
public static Attribute GetCustomAttribute( MemberInfo element, Type attributeType)
Метод возвращает ссылку на единственный атрибут, если таковой будет найден, и null - в противном случае. Если находится несколько атрибутов, удовлетворяющих условию поиска, то выбрасывается исключение. Так что этот метод применяется обычно к атрибутам, которые не допускают дублирования, то есть таких, для которых свойство AllowMultiple имеет значение false. Тип первого аргумента задается классом отражения из пространства System.Reflections. Первый аргумент задает элемент, с которым связан атрибут. Второй аргумент задает тип разыскиваемого атрибута. В данной реализации разыскиваются атрибуты, связанные с одним из членов класса - методом, полем, событием. У других реализаций этого метода первый аргумент может задавать другие классы отражения - сборку, класс и другие сущности, с которыми может связываться атрибут. В реализациях возможны и дополнительные аргументы, с чем встретимся в примерах.
Метод GetCustomAttributes возвращает массив найденных атрибутов, возможно, пустой. Метод перегружен, он может иметь те же аргументы, что и метод GetCustomAttribute, но в отличие от первого возвращает не один, а все найденные атрибуты. Во многих реализациях этого метода не задается тип разыскиваемого атрибута, так что возвращаются все атрибуты, связанные с заданным элементом, независимо от их типа.
Метод IsDefined работает более быстро, чем рассмотренные только что методы, он возвращает true, если искомый атрибут есть у элемента, и false - в противном случае.
Важное замечание: из рассматриваемых методов только метод GetCustomAttribute является уникальным и существует только у класса Attribute. Остальные два метода существуют в форме экземплярных методов у классов, связанных с процессом отражения, и могут быть вызваны, например, у объектов класса Type.
Рассмотрим, как, используя процесс отражения, получить атрибуты, заданные для некоторой сущности. Для определенности рассмотрим класс перечисления Prog_Properties, для которого задан атрибут Flags, и попробуем извлечь этот атрибут из объявления класса. Заодно продемонстрируем два способа получения
/// <summary>
/// Извлечение атрибутов класса
/// </summary>
public void TestTwoMeans()
{
Type clstype = typeof(Prog_Properties);
// Получить атрибуты перечисления,
//используя класс Attribute
Console.WriteLine("Класс Attribute!");
if (Attribute.IsDefined(clstype,
typeof(FlagsAttribute), false))
{
Attribute att = Attribute.GetCustomAttribute(clstype,
typeof(FlagsAttribute));
Console.WriteLine(
"У класса Prog_Properties есть атрибут {0}", att);
}
else
Console.WriteLine("У класса Prog_Properties нет атрибута Flags");
Attribute[] attrs = Attribute.GetCustomAttributes(
clstype, false);
foreach (Attribute attr in attrs)
Console.WriteLine(attr);
// Получить атрибуты перечисления,
//используя класс Type
Console.WriteLine("Класс Type!");
if(clstype.IsDefined(typeof(FlagsAttribute), false))
Console.WriteLine("У класса Prog_Properties есть атрибут Flags");
else
Console.WriteLine("У класса Prog_Properties нет атрибута Flags");
object[] attrsob = clstype.GetCustomAttributes(false);
foreach (Attribute attr in attrsob)
Console.WriteLine(attr);
}
Первым делом создается объект с именем clstype класса Type, содержащий тип анализируемого класса Prog_Properties. Далее вызов метода IsDefined позволяет определить, есть ли у класса атрибут Flags. Если таковой имеется, то вызывается статический метод GetCustomAttribute класса Attribute, которому передаются три аргумента - созданный объект clstype, тип разыскиваемого атрибута и булевский аргумент со значением false, указывающий, что не следует учитывать наследование атрибута. Метод возвращает в качестве результата объект класса FlagsAttribute.
Следующая задача состоит в том, чтобы определить полный список всех атрибутов, связанных с классом. Метод GetCustomAttributes класса Attribute прекрасно решает эту задачу, возвращая в качестве результата массив атрибутов, возможно, пустой. У метода на один аргумент меньше, поскольку ему не нужно передавать тип разыскиваемого атрибута.
Так же просто решаются эти задачи, если использовать работу с методами класса Type, что демонстрируется во второй части нашего примера. Отличия есть, но они незначительны. У класса Type нет метода GetCustomAttribute, так что получать одиночный атрибут сложнее. Второе отличие состоит в том, что метод GetCustomAttributes возвращает массив объектов класса object, а не Attribute, как в первом случае. На рис. 9.2 показаны результаты работы тестового примера.
(рис 9.2) Получение атрибутов класса
Рассмотрим, как устроены атрибутные классы - наследники класса Attribute. Эти классы имеют специфику, отличающую их от других классов. У них, помимо наследуемых от родителя, могут быть заданы свои поля и свойства. Атрибутные классы могут переопределять методы родителя.
Часть полей класса называются позиционными параметрами класса. К ним относят параметры, обязательные для задания. Фактические значения этих параметров всегда передаются конструктору класса в момент задания атрибута. Эти поля могут быть закрытыми.
Другие необязательные поля или свойства атрибутного класса называются именованными. Они должны быть не статическими, открытыми и допускать как чтение, так и запись. Если при объявлении атрибута требуется задать значение именованного параметра, то это значение передается также конструктору в виде пары < имя = значение>. Конструктору вначале передаются позиционные параметры в определенном порядке, а затем именованные параметры, порядок следования которых может быть произвольным. Наличие позиционных и именованных параметров и способ их передачи конструктору является синтаксическим отличием атрибутных классов.
Атрибуты этого класса задаются для большинства атрибутных классов. Будучи заданным для атрибутного класса, атрибут определяет, как могут использоваться атрибуты определяемого класса. Если взглянуть на приведенное ранее определение класса Attribute, то и для него задан атрибут AttributeUsage. Этот атрибут предваряет и определение самого класса AttributeUsageAttribute.
У класса AttributeUsage три параметра.
ValidOn - позиционный параметр. Его значения задаются перечислением AttributeTargets, представляющим шкалу. Значение может быть составным и задаваться с использованием побитовой логической операции < | >. Значение параметра определяет, для каких элементов программы может быть задан атрибут определяемого класса. Для класса Attribute значение этого параметра равно AttributeTargets.All, указывающее, что атрибуты могут появляться для всех возможных элементов. Для класса FlagsAttribute значение этого параметра равно AttributeTargets.Enum, указывающее, что атрибут, задающий флаги, может использоваться только для перечислений.Inherited - именованный параметр. Его значения true или false определяют, может ли атрибутный класс быть наследуемым.AllowMultiple - именованный параметр. Его значения true или false определяют, может ли с программным элементом связываться несколько экземпляров атрибута или он может быть задан только в одном экземпляре.Приведу примеры задания этого атрибута соответственно для классов Attribute, AttributeUsage и Flags:
[AttributeUsageAttribute(AttributeTargets.All, Inherited = true, AllowMultiple = false)][AttributeUsageAttribute(AttributeTargets.Class, Inherited = true)][AttributeUsageAttribute(AttributeTargets.Enum, Inherited = false)]Как уже говорилось, в момент объявления сущности - элемента программы - можно задать атрибуты, связывая их с программным элементом. Синтаксическим признаком атрибута являются квадратные скобки, текст внутри которых можно рассматривать как вызов конструктора, создающего объект соответствующего атрибутного класса. Для одной сущности можно задать несколько атрибутов, принадлежащих как разным атрибутным классам, так и одному классу, если параметр AllowMultiple этого класса имеет значение true. Связывать атрибут с программным элементом можно только тогда, когда программный элемент соответствует цели атрибута, устанавливаемой параметром ValidOn и задаваемой перечислением AttributeTargets. Примеры подобного объявления атрибутов для программных элементов уже приводились.
Точные правила спецификации атрибутов и связывания их с сущностью следующие. Для связывания сущности с атрибутами можно задать одну или несколько атрибутных секций. Каждая атрибутная секция окаймляется квадратными скобками и содержит один атрибут или список атрибутов, элементы которого разделяются запятыми. Порядок следования атрибутных секций и порядок следования атрибутов в списке не играет значения. Так что спецификации атрибутов [A][B], [B][A], [A, B], и [B, A] эквивалентны.
Рассмотрим некоторые особенности связывания. Атрибуты можно задавать для сущностей, не объявляемых в тексте программы. Такими сущностями являются сборка ( assembly ) и модуль ( module ). Сборка создается для каждого проекта и, помимо программного кода, содержит метаинформацию, описывающую элементы проекта. Атрибуты - это часть метаинформации, содержащейся в манифесте сборки. Когда говорится, что в момент связывания атрибута с программным элементом конструктор атрибутного класса создает объект соответствующего класса, это некоторая метафора, поясняющая семантику. Реально объекты не создаются, а просто в манифест сборки добавляется соответствующая информация о данном программном элементе. Понятно, что манифест сборки в первую очередь описывает саму сборку, в том числе и атрибуты, характеризующие сборку. Как же задать эти атрибуты, ведь в программном тексте нет объявления сборки? Все очень просто - атрибуты, связанные со сборкой или с модулем, задаются в самом начале программного текста
любого класса, сразу после предложений using, еще до задания пространства имен. Синтаксически связывание атрибута со сборкой или с модулем достигается за счет того, что конструктору атрибута может предшествовать задание цели, отделяемое от конструктора двоеточием. Вот, например, как можно задать атрибуты для сборки и для модуля при объявлении знакомого нам класса Prog_Properties, который входит в проект ConsoleAttributes, содержащего примеры этой лекции:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
// Установить CLSCompliant атрибут как true
// Этот атрибут применим для сборок /target:assembly.
//Проверяет соответствие сборки общеязыковым спецификациям
[assembly: CLSCompliant(true)]
// Связывание атрибута Description с модулем проекта.
[module: Description("Описание модуля проекта ConsoleAttributes")]
namespace ConsoleAttributes
{
/// <summary>
/// Свойства претендентов на должность программиста,
/// описывающие знание технологий и языков программирования
/// </summary>
//[FlagsAttribute]
//[FlagsAttribute()]
[Flags]
public enum Prog_Properties
{
VB = 1, C_sharp = 2, C_plus_plus = 4,
Web = 8, Prog_1C = 16
}
}
Все атрибуты, связываемые со сборкой и модулем одного проекта и заданные при объявлении разных классов проекта, собираются вместе в манифесте сборки. Уточню, что модулем проекта является переносимый исполняемый файл проекта - PE-файл проекта.
При связывании атрибутов со сборкой и модулем задание цели обязательно, поскольку в программном коде нет объекта, с которым связывается атрибут. Но это не единственный случай, когда задание цели необходимо. Рассмотрим, например, связывание атрибута с методом и значением, возвращаемым этим методом:
[method: Description("Метод, позволяющий получить свойства кандидатов")]
[return: Description("Массив строк со свойствами кандидатов")]
public string[] GetStrCands() {}
Атрибут Description может связываться как с методом, так и с возвращаемым значением. Если он задан в единственном экземпляре и цель не задана, то атрибут по умолчанию связывается с наиболее общим случаем - с методом. Задание цели уточняет, с какой сущностью следует связать атрибут. Заметьте, цель можно задавать всегда, даже когда неоднозначность связывания не возникает.
Как связать атрибуты с модулем и сборкой, понятно. А как решить обратную задачу - выяснить, какие атрибуты связаны со сборкой и модулем? Для этого нужно уметь получать объекты класса Assembly и Module. Покажем, как это делается, извлекая информацию об атрибутах для сборки, модуля, метода и возвращаемого значения. Пример извлечения атрибутов для класса уже появлялся. Расширим его теперь на сущности других типов.
public void TestGetEntityAttributes()
{
//Получение атрибутов сборки и модуля
Type clstype = typeof(Prog_Properties);
Assembly projectAssembly = clstype.Assembly;
Module projectModule = clstype.Module;
Attribute[] assemblyAttributes =
Attribute.GetCustomAttributes(projectAssembly);
Console.WriteLine("Атрибуты сборки");
foreach (Attribute attr in assemblyAttributes)
Console.WriteLine(attr);
Attribute[] moduleAttributes =
Attribute.GetCustomAttributes(projectModule);
Console.WriteLine("Атрибуты модуля");
foreach (Attribute attr in moduleAttributes)
Console.WriteLine(attr);
}
Заметьте, что существуют классы Assembly и Module. Получить объекты этих классов, связанных с проектом, достаточно просто. Можно создать объект класса Type для одного из классов проекта, а затем использовать свойства Module и Assembly, возвращающие нужные нам объекты. Далее метод GetCustomAttributes, которому передаются в качестве первого аргумента объекты, задающие сборку или модуль, будет возвращать массив связанных с ними атрибутов. Уже отмечалось, что метод перегружен, но у него есть реализации, где первый аргумент принадлежит классам Assembly и Module. На рис. 9.3 показаны результаты работы этого теста.
(рис 9.3) Атрибуты сборки и модуляОбратите внимание, что у сборки по умолчанию установлено большое число атрибутов. У модуля таких устанавливаемых по умолчанию атрибутов нет, так что в данном случае у модуля появился только тот атрибут, который был установлен нами в тексте программы.
Покажем теперь, как, используя многообразные классы отражения, получить атрибуты метода класса и возвращаемого значения. Добавим в тестирующий метод TestGetEntityAttributes следующий код:
//Получение атрибутов метода и возвращаемого значения
Type typeJob = typeof(Job);
MethodInfo classMethod = typeJob.GetMethod("GetStrCands");
Attribute[] methodAttributes =
Attribute.GetCustomAttributes(classMethod);
Console.WriteLine("Атрибуты метода");
foreach (Attribute attr in methodAttributes)
Console.WriteLine(attr);
Console.WriteLine("Атрибуты вовращаемого значения");
ParameterInfo returnMethod = classMethod.ReturnParameter;
foreach(Attribute attr in returnMethod.GetCustomAttributes(false))
Console.WriteLine(attr);
Первым делом получен объект typeJob, задающий тип класса. С использованием метода GetMethod получен объект classMethod класса MethodInfo. Этот объект содержит подробную информацию о методе класса. В частности, можно получить атрибуты, связанные с методом. Это можно делать двояким способом, либо вызывая статический метод GetCustomAttributes класса Attribute, либо вызвать MethodInfo.
Созданный объект classMethod позволяет получить объект returnMethod класса ParameterInfo, содержащий информацию о значении, возвращаемом методом. Для получения атрибутов объектов classMethod и returnMethod используется в первом случае статический метод, во втором, для разнообразия, - GetCustomAttributes.
Для полноты картины рассмотрим, как получить не только сам атрибут, связанный с сущностью, но и параметры атрибута, если таковые имеются. У атрибута Description, приписанного в нашем примере к методу и к возвращаемому значению, есть параметр, задающий описание сущности. Это описание задавалось в момент объявления атрибута. Теперь постараемся извлечь его:
//Работа с атрибутом Description
Console.WriteLine("Получение параметров атрибута Description");
DescriptionAttribute des;
MemberInfo specMethod = typeJob.GetMethod("GetStrCands");
if (Attribute.IsDefined(specMethod, typeof(DescriptionAttribute)))
{
des = (DescriptionAttribute)Attribute.GetCustomAttribute(specMethod,
typeof(DescriptionAttribute));
Console.WriteLine(des.Description);
}
ParameterInfo retMethod = classMethod.ReturnParameter;
des = (DescriptionAttribute)Attribute.GetCustomAttribute(retMethod,
typeof(DescriptionAttribute));
Console.WriteLine(des.Description);
Главной особенностью этого фрагмента кода, на которую следует обратить внимание, является то, что для получения конкретного атрибута целесообразно использовать статический метод GetCustomAttribute, который есть только у класса Attribute. Важно также и то, что полученный атрибут нужно привести к заданному типу. Тогда соответствующий объект позволит добраться и до параметров атрибутов. Взгляните на результаты работы двух последних фрагментов кода.
(рис 9.4) Атрибуты метода и параметры
В FlagsAttribute находится в пространстве System, а класс DescriptionAttribute, используемый в наших примерах, находится в пространстве имен System.ComponentModel. Атрибуты активно применяются при построении самых разных
Встроенные атрибуты активно используют и программисты, создавая собственные классы, предназначенные для решения задач проблемной области. Встроенные атрибуты, примененные к собственным классам, представляют декларативный способ, позволяющий изменить семантику выполнения методов класса. Наш пример использования атрибута Flags для перечислений демонстрировал такую возможность.
Программист, создавая класс, связывает с ним встроенный атрибут. Компилятор извлекает метаинформацию, заданную атрибутом. Поскольку встроенные атрибуты компилятору известны, компилятор принимает решение о том, какой код построить, в зависимости от того, задан атрибут или нет, какие параметры у атрибута, если они у него есть. Рассмотрим в качестве примера несколько встроенных атрибутов, часто используемых программистами при создании собственных классов.
Если класс объявить с атрибутом [, то в него встраивается стандартный механизм сериализации. Необходимость в сериализации объектов зачастую возникает при работе с программной системой. Под сериализацией понимают процесс сохранения объектов в долговременной памяти (файлах). Под десериализацией понимают обратный процесс - восстановление состояния объектов, хранимого в долговременной памяти. Механизмы сериализации C# и Framework .Net поддерживают два формата сохранения данных - в бинарном файле и XML- файле. В первом случае данные при сериализации преобразуются в бинарный поток символов, который автоматически при [NonSerialized], - эти поля сохраняться не будут.
Сериализация позволяет запомнить состояния системы объектов с возможностью последующего возвращения к этим состояниям. Сериализация необходима, когда
Еще одно важное применение сериализации - это обмен данными удаленных систем. При удаленном обмене данными предпочтительнее формат xml из-за открытого стандарта передачи данных в Интернете по soap-протоколу, из-за открытого стандарта на структуру xml-документов. Обмен данными становится достаточно простым даже для систем, построенным на разных платформах и в разных средах разработки.
Так же как и клонирование, сериализация может быть поверхностной, когда сериализуется единственный объект, и глубокой, когда, начиная с корневого объекта, сериализуется совокупность объектов, связанных взаимными ссылками (граф объектов). Глубокую сериализацию самому организовать непросто, так как она требует, как правило,
Стандартный механизм сериализации поддерживает, что крайне приятно, глубокую сериализацию. Если по каким-либо причинам стандартная сериализация нас не устраивает, то класс следует объявить наследником интерфейса ISerialzable, реализация методов которого позволит управлять процессом сериализации.
Атрибутный класс SerializableAttribute определен следующим образом:
[ComVisibleAttribute(true)] [AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Struct|AttributeTargets.Enum|AttributeTargets.Delegate, Inherited = false)] public sealed class SerializableAttribute : Attribute
Класс не может быть наследован. Атрибут сериализации может быть задан для таких сущностей, как класс, структура, перечисление и делегат.
В качестве примера рассмотрим два класса Teacher и Student, связанных взаимными ссылками и допускающих сериализацию объектов. Построим простую модель, в которой создаются объекты этих классов. В некоторый момент состояние этих объектов запоминается за счет использования механизма сериализации. Затем состояние объектов изменяется. А потом, с использованием Teacher:
[Serializable]
class Teacher
{
string name;
Student[] students;
public Teacher(string name, Student[] students)
{
this.name = name;
this.students = students;
}
public Student[] Students
{
get { return students; }
}
}
Для класса задан атрибут сериализации. У класса два поля: одно задает имя учителя, другое - его учеников. У класса есть конструктор с параметрами и свойство, позволяющее получить доступ к закрытому полю students.
Класс Student также устроен достаточно просто:
[Serializable]
class Student
{
string name;
Teacher teacher;
string topic;
public Student(string name, Teacher teacher)
{
this.name = name;
this.teacher = teacher;
}
public string Topic
{
get { return topic; }
set { topic = value; }
}
}
Для класса также задан атрибут сериализации. У класса три поля, которые задают имя студента, учителя и тему работы, выполняемой под руководством учителя. У класса есть конструктор с параметрами и свойство, позволяющее получить доступ к закрытому полю topic.
А теперь построим модель, в которой создаются и существуют объекты этих классов. Как обычно, в классе Testing построен соответствующий метод:
public void TestTeacherAndStudents()
{
string teacherName = "Иванов Л.Д.";
Teacher teacher;
Student[] students = new Student[3];
string[] studNames =
{"Зайцев Г.Ю.","Гулевич С.А.","Маргулис В.А."};
teacher = new Teacher(teacherName, students);
students[0] = new Student(studNames[0], teacher);
students[1] = new Student(studNames[1], teacher);
students[2] = new Student(studNames[2], teacher);
В нашей модели созданы объекты teacher и students, задающие учителя и его студентов. Зададим теперь темы работ студентов и сохраним это состояние объектов в бинарном файле.
students[0].Topic = "thema0";
students[1].Topic = "thema1";
students[2].Topic = "thema2";
Console.WriteLine("Темы студентов");
for (int i = 0; i < 3; i++)
Console.WriteLine("Студент {0}, тема: {1} ",
studNames[i], students[i].Topic);
Stream tasFile, satFile;
tasFile = File.Create("teach1.bin");
satFile = File.Create("teach2.bin");
BinaryFormatter bif = new BinaryFormatter();
bif.Serialize(tasFile, teacher);
bif.Serialize(satFile, students[0]);
tasFile.Close();
satFile.Close();
Console.WriteLine("Темы запомнили!");
Для полноты описания картины . Метод Serialize определен для класса только тогда, когда у класса есть соответствующий атрибут.
Представим себе, что после некоторого обсуждения темы работ решено было изменить:
Console.WriteLine("Меняем темы!");
students[0].Topic = "thema7";
students[1].Topic = "thema8";
students[2].Topic = "thema9";
teacher = null;
for (int i = 0; i < 3; i++)
Console.WriteLine("Студент {0}, тема: {1} ",
studNames[i], students[i].Topic);
Для полноты картины у студентов не стало учителя. Видимо, произошел конфликт. Но потом все уладилось, и нужно вернуться к исходному состоянию. В этом поможет
Console.WriteLine("Десериализация. Восстанавливаем состояние");
tasFile = File.Open("teach1.bin",FileMode.Open, FileAccess.Read);
bif = new BinaryFormatter();
teacher = (Teacher)bif.Deserialize(tasFile);
students = teacher.Students;
for (int i = 0; i < 3; i++)
Console.WriteLine("Студент {0}, тема: {1} ",
studNames[i], students[i].Topic);
}
Используя корневой объект, в данном случае teacher, восстанавливаем весь граф объектов, связанных с корнем. Но, заметьте, остальные объекта графа - это внутренняя структура, связанная с корнем. Восстановить объект students нужно явно, используя свойство Students объекта teacher. На рис. 9.5 показаны результаты работы тестового примера.
(рис 9.5) Сериализация объектов
Атрибутный класс ConditionalAttribute позволяет программисту создавать так называемые условно компилируемые классы и методы.
Вот как выглядит заголовок атрибутного класса:
[SerializableAttribute] [AttributeUsageAttribute(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple=true)] [ComVisibleAttribute(true)] public sealed class ConditionalAttribute : Attribute
Как видно из описания, атрибут Conditional можно задавать для классов и методов. У класса ConditionalAttribute есть позиционный параметр, содержательно задающий параметр компиляции. Когда для некоторой сущности (класса или метода) задается атрибут Conditional, то конструктору класса передается строка с фактическим значением параметра. Его значение сравнивается со значением параметра
Только атрибутные классы могут быть условно компилируемыми. Для обычных классов атрибут Conditional задаваться не может. Если для условного атрибутного класса условие компиляции выполняется, то задание этого атрибута для некоторой сущности будет иметь эффект. В случае невыполнения условия компиляции задание атрибута игнорируется.
Если некоторый метод задан с атрибутом Conditional, то метод является условно компилируемым. Это означает, что в момент компиляции компилятор проверит выполнение условия компиляции, сравнив значения двух параметров, заданного в конфигурации и переданного конструктору атрибута Conditional. Если условие компиляции истинно (значения параметров совпадают), то метод будет компилироваться, а иначе не будет компилироваться. Вызов метода, для которого условие компиляции не выполняется, игнорируется, но не приводит к ошибке в период выполнения.
Рассмотрим класс, в котором заданы три метода с атрибутом
[Conditional("DEMO")]
void MainMethod()
{
Console.WriteLine("Это демо версия");
}
[Conditional("PROF")]
void MainMethod(string s)
{
Console.WriteLine("Это профессиональная версия");
}
[Conditional("DEBUG")]
void DebugMethod()
{
Console.WriteLine("Это отладочная версия");
}
У первых двух методов имена совпадают, но сигнатуры отличаются. Содержательно один из методов соответствует демоверсии, а второй - профессиональной версии системы. Третий метод содержательно задает метод, используемый только на этапе отладки. Все три метода являются условно компилируемыми, и выполнение условия компиляции зависит от значения константы, переданной конструктору атрибута Conditional, и
Все три метода закрыты и непосредственно клиентами не вызываются. Рассмотрим метод, доступный клиентам класса:
public void ClientMethod(string s)
{
MainMethod();
MainMethod(s);
DebugMethod();
}
В этом методе вызываются все три метода с условной компиляцией. Какие из этих трех методов реально будут работать, зависит от того, какая конфигурация проекта будет вызвана и какие параметры в ней заданы. По умолчанию строятся две конфигурации проекта - Debug и Release. Первая используется на этапе отладке, вторая - с включенным режимом оптимизации задает рабочую версию системы. В конфигурации Debug по умолчанию определены две константы DEBUG и TRACE. В конфигурации Release определена по умолчанию только вторая из этих констант. В каждой конфигурации на странице свойств проекта можно задать произвольное число собственных DEMO, а для конфигурации Release - PROF. На рис. 9.6 показана страница свойств проекта для конфигурации Debug с заданными значениями констант.
(рис 9.6) Константы условной компиляции конфигурации DebugРассмотрим теперь клиентский класс Testing и его метод, вызывающий исследуемые методы:
public void TestConditional()
{
Console.WriteLine("Demo or Profi?");
ClassDemo demo = new ClassDemo();
demo.ClientMethod("");
}
Запустим наш проект на выполнение в конфигурации Debug. В этой конфигурации определены константы DEBUG и DEMO, так что два метода из трех вызываемых должны сработать. Результаты можно увидеть на рис. 9.7.
(рис 9.7) Результаты работы в конфигурации DebugЕсли же запустить проект в конфигурации Release, где определена только одна константа PROF, то будет работать только "профессиональный метод", а вызов остальных двух методов будет проигнорирован.
Ничто не мешает создать собственный атрибутный класс и связывать сущности программы с атрибутами этого класса. Действия компилятора в этом случае сводятся к тому, что он информацию, заданную атрибутом, размещает как часть метаинформации проекта. Понятно, что компилятору ничего не известно о сути определяемого нами атрибутного класса, и поэтому включение атрибута никак не отражается на коде, создаваемом компилятором. Чтобы собственные атрибуты оказали влияние на код, необходимо включить процесс отражения. Это означает, что в какой-то части программы информация об атрибутах, сопровождающих сущности, должна извлекаться, анализироваться, и на этой основе должны приниматься те или иные решения.
Когда следует создавать собственные атрибутные классы? Если разрабатывается DLL - динамическая библиотека классов, подключаемая к различным проектам, то создание атрибутных классов, сопровождающих эту библиотеку, и связывание программных сущностей библиотеки с этими атрибутами может быть весьма полезным. Проекты, присоединяющие эту библиотеку, могут из анализа атрибутов извлечь полезную информацию, позволяющую корректно работать с классами библиотеки. В более общей ситуации некое хост-приложение работает с дополнительными приложениями (add in), используя отражение и информацию, извлекаемую из атрибутов для обеспечения корректного взаимодействия.
Еще одна причина создания атрибутов состоит в том, что атрибуты позволяют сохранять полезную информацию о приложении, играя роль документации. Часто использование атрибутов для этих целей полезнее, чем другие возможные способы - комментарии в тексте, тэги, файлы или базы данных.
Рассмотрим пример создания собственного атрибутного класса. Атрибуты этого класса позволят запоминать историю создания и модификации программных сущностей проекта. В атрибуте будет сохраняться информация об авторе, который создает или вносит изменения в код, дате внесения изменений и сопутствующий комментарий.
[AttributeUsage (AttributeTargets.All,
AllowMultiple = true, Inherited = true)]
class HistoryAttribute: Attribute
{
//позиционные параметры
string author;
string date;
// именованный параметр
public string comment;
Атрибут Usage заданный для нашего атрибутного класса History, указывает, что атрибут может быть задан для всех программных сущностей и может появляться в нескольких экземплярах. Поля этого класса содержат информацию, которую предполагается задавать в момент определения атрибута. Автор и дата относятся к обязательным параметрам. Именованный параметр comment может опускаться.
Дополним класс заданием конструктора и методами свойствами, осуществляющими доступ к закрытым полям класса:
public HistoryAttribute(string author, string date)
{
this.author = author; this.date = date;
}
public string Author
{
get { return author; }
}
public string Date
{
get { return date; }
}
}
Теперь атрибутный класс полностью определен, и его можно использовать в процессе разработки программного проекта. В качестве модельного примера приведу класс, сохраняющий историю разработки:
[History("В. Биллиг", "30.03.2009",
comment = "Совместный проект. Будем работать!")]
[History("М. Дехтярь", "30.03.2009",
comment = "Проект: Наш класс")]
[History("И. Муссикаев", "30.03.2009",
comment = "Три автора")]
class OurClass
{
[History("И. Муссикаев", "3.04.2009",
comment = "Первый вариант алгоритма")]
[History("И. Муссикаев", "7.04.2009",
comment = "Улучшеный вариант")]
public void First()
{
}
[History("М. Дехтярь", "3.04.2009",
comment = "Первый вариант алгоритма")]
[History("М. Дехтярь", "7.04.2009",
comment = "Улучшение характеристик сложности")]
public void Second()
{
}
[History("В. Биллиг", "2.04.2009",
comment = "Первый вариант реализации алгоритма")]
[History("В. Биллиг", "5.04.2009",
comment = "Изменение спецификаций")]
public void Third()
{
}
}
Как видите, атрибут позволяет хранить вместе с кодом и историю внесения изменений. Процесс отражения позволит при необходимости извлечь историю разработки. Объяснение того, как это можно делать, уже дано. Поэтому просто приведу код, выполняющий эту работу:
public void TestOurClass()
{
//История разработки класса
Console.WriteLine("История разработки класса");
Type clstype = typeof(OurClass);
Attribute[] attrs;
HistoryAttribute history;
attrs = Attribute.GetCustomAttributes(clstype);
foreach(Attribute attr in attrs)
{
history = attr as HistoryAttribute;
if (history != null)
Console.WriteLine("автор: {0}", history.Author);
Console.WriteLine(" дата начала разработки: {0} ",
history.Date);
Console.WriteLine("комментарий: {0} ",
history.comment);
}
Console.WriteLine("История разработки методов");
MethodInfo[] methods = clstype.GetMethods();
foreach (MethodInfo method in methods)
{
attrs = Attribute.GetCustomAttributes(method);
foreach(Attribute attr in attrs)
{
history = attr as HistoryAttribute;
if (history != null)
Console.WriteLine("Метод: {0} ", method.Name);
Console.WriteLine("автор: {0}", history.Author);
Console.WriteLine(" дата начала разработки: {0}",
history.Date);
Console.WriteLine("комментарий: {0} ",
history.comment);
}
}
}
На рис. 9.8 приведены результаты работы этого метода.
(рис 9.8) История разработкиОдним из важных качеств кода, соответствующего требованиям промышленной разработки, является ясность понимания кода. Код должен быть понятным автору не только в момент его создания. Код должен оставаться понятным в процессе его сопровождения через длительное время после его создания, когда работать с программным продуктом придется разработчику, возможно, не знакомому с автором исходного кода.
В языке C# большое внимание уделяется средствам документирования кода. Разработчики кода могут комментировать код, используя специальный синтаксис, содержащий XML-текст. Комментарии, использующие этот синтаксис, называются документируемыми комментариями. Для поддержки работы с такими комментариями имеется специальный инструментарий - генератор документации, - позволяющий извлекать такие комментарии и строить автоматически файл документации - и браузер
объектов являются частью такого инструментария. Эти средства отображают информацию, заданную в тегах summary, param, и некоторых других тегах. Интеллектуальная подсказка крайне важна при работе клиентов с классом. Поэтому профессиональный стиль разработки требует, чтобы все открытые для клиентов и потомков сущности класса были снабжены документируемыми комментариями. Хотелось бы иметь как отдельный инструментарий и браузер отчетов, но в настоящее время он не входит в состав инструментария Visual Studio 2008.
В основе любого XML-текста лежит понятие тега. Текст имеет скобочную структуру. Тег задается парой - открывающим тегом и закрывающим тегом. В языке C# определен целый ряд стандартных тегов, имеющих фиксированный смысл и используемых в определенном контексте. Зная смысл тегов, компилятор проверяет соответствие контекста заданному тегу и выдает предупреждение, если соответствие нарушается. Теги включаются в состав документируемого комментария.
Синтаксис комментариев позволяет генератору создавать
/// Это однострочный комментарий /** * Это многострочный комментарий с ограничителями */
Первая форма соответствует однострочным комментариям, вторая допускает расположение комментария на нескольких строках. Оба комментария должны непосредственно предшествовать программной сущности, с которой они связаны. В роли сущностей могут выступать классы и все их частные случаи, а также поля, свойства, методы и события класса. Если аннотируемая сущность имеет атрибуты, то атрибуты рассматриваются как часть объявления сущности, и комментарии непосредственно предшествуют атрибутам. Заметьте, обычный строчный комментарий начинается с двух символов слеша, а документируемый - с трех, что позволяет компилятору находить документируемый комментарий. Для многострочных комментариев признаком документируемого комментария являются две звездочки, идущие за слешем, в отличие от одной звездочки обычного комментария.
Комментарии в отличие от атрибутов не включаются в метаинформацию, и, следовательно, не могут быть извлечены в процессе отражения. Но они могут быть включены в файл документации проекта. Стандартные теги имеют предопределенный смысл и функциональность. Программист при желании может задавать собственные теги. Единственное, что от него требуется в этом случае, - это соблюдение общих синтаксических правил, предъявляемых к XML-тексту. В табл. 9.1 приведен набор стандартных тегов C# с пояснением их смысла.
| Тег | Смысл |
|---|---|
<c> |
Для тела тега - текста, размещенного между открывающим и закрывающим тегом, используется шрифт, применяемый для кода. |
<code> |
Аналогичен тегу <c>. Обычно первый тег применяется для кратких текстов, второй для длинных текстов из нескольких строк. |
<example> |
Применяется, когда в комментариях встречается пример кода. Обычно сочетается с тегом <code> |
<exception> * |
Обычно связывается с методом и описывает исключения, которые могут быть выброшены в процессе работы метода. Синтаксис: <exception cref = "
Выполняется проверка, существует ли указанный |
<include>*
Stop!!! |
Для больших проектов может существовать общий XML-файл с документацией. Тег include позволяет ссылаться на нужное место в этом файле. Синтаксис: <
Выполняется проверка, существует ли указанный файл и путь к тегу. |
<list> |
Позволяет внутри комментария задавать структуру списка или таблицы, что улучшает читаемость отчета документации. |
< |
Позволяет внутри комментария задавать абзацы с той же целью улучшения читаемости отчета. |
<param>* |
Позволяет комментировать параметры метода. Обычно строится автоматически вместе с тегом summary, когда документируемый комментарий вставляется перед методом. Текст описания отображается в подсказке и в браузере объектов.
Выполняется проверка, существует ли указанный параметр у комментируемого метода. |
<paramref> |
Позволяет внутри комментария указать, что некоторое слово текста является параметром (ссылкой на параметр метода), - это позволяет в отчете выделить это слово. |
<permission>* |
Позволяет документировать безопасность доступа, задавая ссылку на поле или метод, доступный для вызова в данном окружении. |
< |
Задает описание программной сущности. Обычно это некоторое дополнение - ремарка к основному описанию, задаваемому тегом summary. Но иногда тег используется вместо тега summary. Следует иметь в виду, что в этом случае описание, заданное тегом, не отображается в подсказке , хотя и отображается в браузере объектов. |
<returns> |
Тег returns позволяет описать значение, возвращаемое методом. Когда документируемый комментарий вставляется перед началом метода, достаточно набрать три слеша - признак начала документируемого комментария, как автоматически в комментарий добавляются тег summary и теги param и returns, если метод имеет параметры и возвращаемое значение, отличное от void. |
<see>* |
Позволяет внутри описания (в теге summary ) сделать ссылку на поле или метод. Синтаксис:<see cref = "member" |/>Выполняется проверка существования элемента, заданного ссылкой. Пример: ///<summary>
///Метод Move перемещает точку на плоскости в новое положение.
///В отличие от метода < see cref = "Offset" />, выполняющего сдвиг точки.
///</summary>
public void Move(int x, int y) {…}
|
<seealso>* |
Аналогичен тегу <see>. Создает в файле документации специальный раздел "смотри также". |
<summary> |
Задает описание полей и методов. Вставляется автоматически, как только заданы три слеша, начинающие документирующий комментарий. Описание, заданное тегом, показывается в подсказке и в браузере объектов. Если строится файл документации, то требуется задание этого тега для всех public полей и методов. В противном случае выдаются предупреждение, что элемент не имеет комментария, что нарушает правила стиля профессионального программирования. |
<typeparam>* |
Позволяет описать параметр типа для summary, param, returns автоматически добавляется в документирующий комментарий.
Выполняется проверка, что соответствующий параметр действительно есть у класса или метода. |
<typeparamref> |
Задает ссылку на параметр типа. |
<value>* |
Позволяет описать метод-свойство. |
В примерах данного курса документируемые комментарии создавались для многих открытых сущностей класса, что отвечает правилам стиля профессионального программирования. Иногда требования стиля не соблюдались по причине увеличения объема текста, затрудняющего его восприятие. Для программного текста увеличение объема не имеет принципиального значения, поскольку текст комментария всегда может быть свернут, не мешая восприятию программного кода. Но, когда программный код копируется и становится частью текста книги, свертка не действует, и комментарий появляется в развернутом виде, мешая иногда восприятию программного кода. По этой причине в примерах документируемые комментарии построены далеко не всюду, где они должны появляться в соответствии с правилами стиля. Тем не менее было много вариантов использования основных тегов - summary, param, returns. Приводить примеры применения всех тегов нет особой необходимости. В табл. 9.1 дано
достаточно подробное описание их назначения. Ограничусь одним примером использования тега exception, позволяющего описать причину исключения, которое может возникнуть в ходе работы метода класса. Давайте слегка модифицируем уже известный нам пример одного из методов класса OurClass:
/* * *
<summary>
Третий метод. Лучший из всех методов.
</summary>
* */
/// <exception cref = "OurClassException">
/// Исключение возникает после дождика в четверг!
/// Оно будет перехвачено и обработано.
/// </exception>
[History("В. Биллиг", "2.04.2009",
comment = "Первый вариант реализации алгоритма")]
[History("В. Биллиг", "5.04.2009",
comment = "Изменение спецификаций")]
public void Third()
{
const string message = "Дождь в четверг!";
const string result = "Хорошо собирать грибы!";
try
{
bool rainInThursday = true;
if (rainInThursday) throw (new OurClassException(message));
}
catch (OurClassException e)
{
Console.WriteLine(e.Message);
Console.WriteLine(result);
}
}
У метода два атрибута и два тега. Тег summary размещен в многострочном комментарии, тег exception - в однострочном. В теге exception задан параметр cref, задающий имя класса исключений. Подобный параметр имеется во многих тегах, где он задает имя некоторой сущности. Такие теги являются тегами с проверкой - проверяется существование сущности, заданной параметром cref. Для класса OurClass создан, как положено, OurClassException. Поэтому проверка, проводимая для тега exception, закончится успехом, обнаружив этот класс.
Приведу пример фрагмента XML отчета, связанного с рассматриваемым методом:
<member name="M:ConsoleAttributes.OurClass.Third">
<summary>Третий метод. Лучший из всех методов. </summary>
<exception cref="T:ConsoleAttributes.OurClassException">
Исключение возникает после дождика в четверг!
Оно будет перехвачено и обработано.
</exception>
</member>
Заметьте, вся информация из тегов попала в отчет. Благодаря проверке приводится полная информация, включающая имя пространства имен, имя класса и имя сущности. Однобуквенные идентификаторы, предшествующие описанию сущности, характеризуют ее тип ( M - метод, T - исключение, F - поле и так далее).
Атрибуты и теги играют важную роль в процессе разработки и сопровождения программной системы. Они являются формально не обязательными, декларативными элементами, связанными с программной сущностью, но включать их в программный текст необходимо. Встроенные атрибуты и теги существенно упрощают разработку. Но во многих случаях полезно и нужно создавать собственные классы атрибутов и использовать собственные теги в процессе создания отчета по документации.
Программный текст должен быть не только хорошим. Он должен быть хорошо документирован. Без тегов и атрибутов в этом случае не обойтись.
Person, Student, Car, Firm. Задайте стандартные атрибуты для классов, полей, методов, параметров и возвращаемых значений методов класса. Постройте классы PersonReflection и другие, где, используя стандартные классы отражения из пространства имен Reflection, получите информацию о сущностях построенных классов. Постройте соответствующий интерфейс Windows-проекта, позволяющий получать информацию о классе. Можно ли построить универсальный класс TReflection с родовым параметром T, позволяющий получать информацию о произвольном классе T?Person, Student, Car, Firm собственными атрибутами. Постройте процесс отражения, получая информацию об атрибутированных сущностях классов.Person, Student, Car, Firm. Классы должны включать события и предусматривать обработку исключительных ситуаций. Задайте в каждом классе стандартные теги всех возможных видов. Постройте соответствующий интерфейс Windows-проекта, позволяющий получать полный отчет о классе и частные отчеты о деталях класса - полях класса, методов класса, событиях, исключительных ситуациях.Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.