
Объектно-ориентированное программирование.-6
.pdf
Формат: {0:P2} Цвет: Blue Фон: Black
75,81% // синий шрифт Формат: {0}
Цвет: Gray Фон: Red
Hello! // красный фон, серый шрифт Формат: 0x{0,4:X4}
Цвет: Yellow Фон: Blue
0x007B // синий фон, желтый шрифт
5.4.3.3. Задание правил наследования атрибутов
Последний параметр – флаг Inherited – определяет, может ли атрибут наследоваться производными классами и переопределенными членами. Его значение по умолчанию равно true. Если флаг установить в false, атрибут не будет наследоваться, в противном случае его действие зависит от значения флага AllowMultiple. Если Inherited установлен в true, а AllowMultiple – в false, установленный атрибут заменит унаследованный. Однако если и Inherited, и AllowMultiple установлены в true, атрибуты члена будут аккумулироваться.
Пример:
class TestAttribute : Attribute { public string Value; } [AttributeUsage(AttributeTargets.Class, Inherited = false)] class TestAttribute1 : TestAttribute { } [AttributeUsage(AttributeTargets.Class, Inherited = true)] class TestAttribute2 : TestAttribute { }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
class TestAttribute3 : TestAttribute { }
[TestAttribute1(Value = "Base1")] class BaseClass1 { } [TestAttribute2(Value = "Base2")] class BaseClass2 { } [TestAttribute3(Value = "Base3")] class BaseClass3 { }
class ChildClass1 : BaseClass1 { } class ChildClass21 : BaseClass2 { }
[TestAttribute2(Value = "Child2")] class ChildClass22 : BaseClass2 { } [TestAttribute3(Value = "Child3")] class ChildClass3 : BaseClass3 { }
static void GetAttributes(Type type)
{
TestAttribute[] attrs = (TestAttribute[])type.GetCustomAttributes(typeof(TestAttribute), true);
Console.WriteLine("Тип {0} имеет следующие атрибуты:", type.Name); foreach (var attr in attrs) Console.WriteLine(" {0} (Value =
{1})", attr.GetType().Name, attr.Value); Console.WriteLine();
}
411

static int Main()
{
GetAttributes(typeof(ChildClass1));
GetAttributes(typeof(ChildClass21));
GetAttributes(typeof(ChildClass22));
GetAttributes(typeof(ChildClass3)); return 0;
}
Обратите внимание, что при вызове метода GetCustomAttributes мы указали параметр inherit = true, иначе получили бы только атрибуты дочерних классов. Результат работы программы:
Тип ChildClass1 имеет следующие атрибуты:
Тип ChildClass21 имеет следующие атрибуты:
TestAttribute2 (Value = Base2)
Тип ChildClass22 имеет следующие атрибуты:
TestAttribute2 (Value = Child2)
Тип ChildClass3 имеет следующие атрибуты:
TestAttribute3 (Value = Child3)
TestAttribute3 (Value = Base3)
5.4.4.Стандартные классы атрибутов
Всправочной системе MSDN (на странице класса Attribute) приведена иерархия большинства из имеющихся классов атрибутов. Это, например:
• Уже знакомый нам атрибут System.AttributeUsageAttribute;
• Атрибут System.Diagnostics.ConditionalAttribute, используемый для определения условных методов;
• Атрибут System.ObsoleteAttribute, используемый для пометки типа или члена как устаревшего;
• Атрибут System.FlagsAttribute, указывающий, что перечисление может обрабатываться как битовое поле, которое является набором флагов (см.
п. 3.1.2.5);
• Атрибут System.Runtime.InteropServices.DllImportAttribute для вызова функций, экспортированных из неуправляемых динамических библиотек
(DLL) (см. § 5.5);
• Ряд атрибутов пространства имен System.Reflection для указания параметров сборки (автоматически задаются в файле AssemblyInfo.cs);
• и т.д.
Первые три имени атрибутов являются зарезервированными и не могут
412

использоваться при создании новых классов атрибутов.
Пример: Samples\5.4\5_4_4_stdattr.
5.4.4.1. Объявление условных методов
Условные методы похожи на методы, объявленные с директивами условной компиляции, но есть одно отличие:
#define X using System;
using System.Diagnostics;
namespace StandardAttributesSample
{
class Program
{
#if X
static void Method1()
{
Console.WriteLine("Метод 1");
}
#endif
#if Y
static void Method2()
{
Console.WriteLine("Метод 2");
}
#endif
[Conditional("X")] static void Method3()
{
Console.WriteLine("Метод 3");
}
[Conditional("Y")] static void Method4()
{
Console.WriteLine("Метод 4");
}
[Conditional("Y"), Conditional("X")] static void Method5()
{
Console.WriteLine("Метод 5");
}
static int Main()
{
Method1(); // ОК Method2(); // Ошибка Method3(); // ОК Method4(); // ОК Method5(); // ОК return 0;
}
}
}
Метод, заключенный в директиву условной компиляции, исключается из кода программы и не может быть вызван, если символ условной компиля-
413

ции не определен. Условный метод, описанный с атрибутом Conditional("<символ>"), где <символ> – это символ условной компиляции, может быть вызван в любом случае. Но его тело будет выполнено только в том случае, если этот символ определен:
Метод 1 Метод 3 Метод 5
Если указано несколько атрибутов Conditional, то метод будет выполнен, если определен хотя бы один из указанных символов (т.е. действует операция дизъюнкции).
На условные методы налагается ряд ограничений – это должны быть методы класса или структуры, имеющие тип возвращаемого значения void, не имеющие модификатора override и не являющиеся реализацией метода интерфейса.
5.4.4.2. Пометка типа или члена как устаревшего
Атрибут Obsolete помечает тип или член типа как устаревший. Позиционный параметр соответствует сообщению, которое получит пользователь от компилятора при попытке использовать такой тип или член:
[Obsolete("Класс Z устарел. Используйте вместо него класс NewZ")] class Z { }
class NewZ { } class ZZ
{
[Obsolete("Метод F1 устарел и будет исключен из класса в следующей версии библиотеки. Используйте вместо него метод F2")]
public static void F1() { } public static void F2() { }
}
static int Main()
{
Z z = new Z(); // Предупреждение ZZ.F1(); // Предупреждение return 0;
}
414

§ 5.5. Неуправляемый код
Рассмотрим три основных варианта применения неуправляемого кода в
.NET:
•Службы Platform Invocation Services, позволяющие коду .NET обра-
щаться к неуправляемым библиотекам DLL;
•Написание блоков небезопасного кода (unsafe code), позволяющее использовать в приложениях такие конструкции, как указатели;
•Организация взаимодействия с COM (COM interoperability). Данная тема не входит в список рассматриваемых вопросов. В принципе, технология COM, хотя пока еще достаточно широко используется, считается устаревшей (т.к. была разработана корпорацией Microsoft еще в 1993 году). На сегодняшний день Microsoft объявила рекомендуемой основой для создания приложений и компонентов под Windows платформу .NET.
5.5.1. Службы Platform Invocation Services
Службы Platform Invocation Services .NET (или PInvoke) позволяют управляемому коду работать с функциями и структурами, экспортированными из DLL. Мы можем создавать DLL с неуправляемым кодом с помощью компиляторов .NET (неуправляемые DLL), либо с помощью других языков программирования.
Поскольку компилятор не имеет доступа к исходному коду DLL, мы должны указать ему сигнатуру встроенного метода, информацию о любых возвращаемых значениях, а также способы преобразования параметров для
DLL.
Пример: Samples\5.5\5_5_1_pinvoke.
5.5.1.1. Объявление импортируемой функции
Рассмотрим объявление на языке C# импорта функций из динамических библиотек с неуправляемым кодом. Для этого используется атрибут
System.Runtime.InteropServices.DllImportAttribute. Общий синтаксис следую-
щий:
"["DLLImport("<имя DLL>" [, <именованные параметры>])"]" [<модификатор доступа>] static extern <тип возвращаемого значения> <имя метода>(<список формальных аргументов>);
415

Перечислим некоторые именованные параметры:
•bool BestFitMapping – включает или отключает поведение наилучшего сопоставления при преобразовании знаков Unicode в знаки ANSI (по умолча-
нию true).
•CharSet CharSet – задает кодировку строк при вызове импортируемого метода (доступны константы Ansi – по умолчанию, Unicode, Auto).
•bool ExactSpelling – определяет, требуется ли точное совпадение имени импортируемой функции, или будет происходить поиск отдельных версий для кодировок ANSI и Unicode (по умолчанию false).
Почему так много внимания уделяется кодировке строк? Все функции API Windows, параметрами которых являются строки, имеют две версии – для кодировок ANSI (1 байт на символ, имена таких функций заканчиваются на A) и для кодировок Unicode (заканчиваются на W). Например, смотрим определение функции MessageBox (см. заголовочный файл winuser.h):
WINUSERAPI int WINAPI MessageBoxA( HWND hWnd ,
LPCSTR lpText, LPCSTR lpCaption, UINT uType);
WINUSERAPI int WINAPI MessageBoxW( HWND hWnd ,
LPCWSTR lpText, LPCWSTR lpCaption, UINT uType);
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA #endif // !UNICODE
Здесь второй параметр – текст сообщения, третий – заголовок окна сообщения. Поэтому перед службой PInvoke стоит два вопроса при вызове импортируемой функции. Во-первых, искать ли в DLL функцию точно с таким именем, как это указано, либо добавлять суффикс A или W. Во-вторых, нужно ли преобразовывать строковые параметры из кодировки Unicode (которую имеют все строки в языке C#) в кодировку ANSI (какая это именно будет кодировка – зависит от языка локализации Windows).
Типы остальных параметров надо подбирать так, чтобы у них совпадал размер в байтах. Например, первый параметр типа HWND – указатель на окно, показывающее сообщение. Любой указатель в Win32 занимает 4 байта, поэтому при описании импортируемой функции можно использовать тип int
или uint.
416

•CallingConvention CallingConvention – указывает соглашение о вызове импортируемой функции. Доступны константы Winapi (по умолчанию),
Cdecl, StdCall, ThisCall, FastCall.
В языке C++ есть несколько модификаторов для указания соглашения о вызове (__cdecl, __stdcall, __fastcall и т.п.). Они влияют на способ управления параметрами при вызове функции (в каком порядке они передаются, кто их снимает со стека – вызывающий метод или вызываемый, и т.п.), на преобразование имени функции после компиляции и т.д.
В других языках есть схожие по назначению модификаторы. Таким образом, если мы импортируем функцию из библиотеки, написанной на одном из этих языков, использование неправильного параметра приведет к краху при вызове импортируемой функции. Для функции MessageBox видим, что при описании использовался модификатор WINAPI (в принципе, для ОС Windows он соответствует __stdcall).
•string EntryPoint – позволяет задать имя импортируемой функции, если оно не совпадает с именем, описанным в коде C#.
•bool PreserveSig – управляет типом значения, возвращаемого импортируемой функцией (по умолчанию true).
Многие функции Windows API возвращают в результате своей работы значение HRESULT, которое либо сигнализирует об успешном выполнении функции (значение 0), либо об ошибке (ненулевой код ошибки Windows). Программист должен сам обрабатывать ошибки, и если он забыл это сделать, то дальнейшее функционирование программы может стать нестабильным. Если данный флаг установить в значение false, то возвращаемым значением импортируемой функции станет void, а если при вызове функция вернет ненулевое значение, среда CLR сгенерирует исключение с сообщением об ошибке.
Пример:
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "MessageBoxA")] static extern int MessageBoxA1(int hwnd, string msg, string caption, uint flags);
[DllImport("user32.dll", CharSet = CharSet.Ansi, EntryPoint = "MessageBoxA")] static extern int MessageBoxA2(int hwnd, string msg, string caption, uint flags);
[DllImport("user32.dll", CharSet = CharSet.Unicode, EntryPoint = "MessageBoxW")] static extern int MessageBoxW1(int hwnd, string msg, string caption, uint flags);
[DllImport("user32.dll", CharSet = CharSet.Ansi, EntryPoint = "MessageBoxW")] static extern int MessageBoxW2(int hwnd, string msg, string caption, uint flags);
417

static int Main()
{
MessageBoxA1(0, "Привет от MessageBoxA №1!", "Сообщение", 0); MessageBoxA2(0, "Привет от MessageBoxA №2!", "Сообщение", 0); MessageBoxW1(0, "Привет от MessageBoxW №1!", "Сообщение", 0); MessageBoxW2(0, "Привет от MessageBoxW №2!", "Сообщение", 0); return 0;
}
Первое и четвертое сообщения будут нечитаемыми, т.к. вызываемым функциям будет передана строка в неподходящей кодировке. В принципе, значения именованных параметров атрибута DllImport по умолчанию настроены так, что почти всегда подходят для вызова функций API Windows. Поэтому можно просто написать
[DllImport("user32.dll")] static extern int MessageBox(int hwnd, string msg, string caption, uint flags);
и в дальнейшем без проблем использовать данный метод MessageBox. Для нестандартных библиотек может потребоваться изменение некоторых параметров.
5.1.1.2. Импорт констант
Четвертый параметр функции MessageBox определяет дополнительные флаги – набор кнопок окна сообщения, картинка, кнопка по умолчанию, модальность и т.д. Возвращаемое значение – идентификатор нажатой кнопки. Они определены в том же заголовочном файле winuser.h. Вот часть из них:
#define IDOK |
1 |
#define IDCANCEL |
2 |
#define IDABORT |
3 |
#define IDRETRY |
4 |
#define IDIGNORE |
5 |
#define IDYES |
6 |
#define IDNO |
7 |
#define IDCLOSE |
8 |
#define MB_OK |
0x00000000L |
#define MB_OKCANCEL |
0x00000001L |
#define MB_ABORTRETRYIGNORE |
0x00000002L |
#define MB_YESNOCANCEL |
0x00000003L |
#define MB_YESNO |
0x00000004L |
#define MB_RETRYCANCEL |
0x00000005L |
#define MB_ICONHAND |
0x00000010L |
#define MB_ICONQUESTION |
0x00000020L |
#define MB_ICONEXCLAMATION |
0x00000030L |
#define MB_ICONASTERISK |
0x00000040L |
|
|
Импортировать их из библиотеки напрямую нельзя, поэтому можно
418

объявить в нашей программе именованные константы, присвоив им требуемые значения. Можно использовать обычные константы, но именованными пользоваться удобнее. Например:
const int IDOK = 0x0001; const int IDCANCEL = 0x0002;
const uint MB_OKCANCEL = 0x00000001; const uint MB_ICONQUESTION = 0x00000020;
static int Main()
{
int rez = MessageBox(0, "Привет от MessageBox!", "Сообщение", MB_ICONQUESTION | MB_OKCANCEL);
if (rez == IDOK) Console.WriteLine("Вы нажали клавишу ОК"); else Console.WriteLine("Вы нажали клавишу Отмена");
return 0;
}
Увидим окно сообщения с картинкой вопроса и кнопками «ОК» и «Отмена».
5.5.1.3. Использование функций обратного вызова
Не только функции DLL могут быть вызваны из кода на языке C#, но и сами функции DLL могут вызывать определенные методы из нашего приложения в сценариях с обратными вызовами (callback). Сценарии обратного вызова включают в себя использование набора функций Win32 EnumХХХ. При вызове этих функции для перечисления элементов им передается указатель на функцию, которая будет вызываться Windows каждый раз, когда искомый элемент будет найден. Это делается комбинированием PInvoke (для вызова функции DLL) и делегатов (для определения обратного вызова).
Например, данный код выполняет перечисление всех окон в системе и вывод их заголовков и имен классов:
[DllImport("user32.dll")] static extern int GetWindowText(int hwnd, StringBuilder text, int count);
[DllImport("user32.dll")] static extern int GetClassName(int hwnd, StringBuilder text, int count);
[DllImport("user32.dll")] static extern int EnumWindows(EnumWindowProc callback, int param);
delegate bool EnumWindowProc(int hwnd, int param);
static bool OutWindowCaption(int hwnd, int param)
{
StringBuilder text = new StringBuilder(1024); GetWindowText(hwnd, text, 1024);
if (text.Length > 0)
{
Console.Write(" {0}", text); GetClassName(hwnd, text, 1024);
419

Console.WriteLine(" ({0})", text);
}
return true;
}
static int Main()
{
EnumWindowProc proc = OutWindowCaption; Console.WriteLine("Активные окна в системе:"); EnumWindows(proc, 0);
return 0;
}
Справку по этим функциям и назначению их параметров можно найти в библиотеке MSDN. Отметим лишь, почему для передачи параметра text используется класс StringBuilder. Значение этому параметру присваивается в вызываемой функции. Если бы это был числовой параметр, то мы передали бы его по ссылке или указателю. А класс string модификации строки не допускает. Поэтому резервируем 1024 символа для длины строки (можно узнать точное значение длины заголовка окна, но не будем усложнять программу) и используем класс для модифицируемых строк – Text.StringBuilder.
5.5.2. Написание небезопасного кода
Опасность и ненадежность небезопасного кода вовсе не являются его врожденными чертами. Небезопасный код – это код, выделение и освобождение памяти для которого не контролируется исполняющей средой .NET. Небезопасный код дает особенно заметные преимущества при использовании указателей для взаимодействия с написанным ранее кодом (таким как API Windows для языка C), или когда нашему приложению требуется прямое манипулирование памятью (как правило, из соображений повышения производительности).
Мы можете писать небезопасный код, применяя ключевые слова unsafe, fixed и stackalloc. Первое указывает, что помеченный им класс, член класса или блок будет работать в неуправляемом контексте. Второе отвечает за фиксацию управляемых объектов. По мере исполнения приложения выделенная под объекты память освобождается, в итоге остаются «фрагменты» свободной памяти. Чтобы не допустить фрагментации памяти, исполняющая среда .NET перемещает объекты, обеспечивая максимально эффективное использование памяти. Фиксация налагает запрет на перемещение данного объекта сборщиком мусора (garbage collector, GC). Третье указывает, что дина-
420