Отображение C++ на спецификацию общего (универсального) языка (CLS) и .NET Framework

C++ — мощный язык программирования, предоставляющий широкий выбор примитивных типов и позволяющий расширять возможности типов определением классов и интерфейсов. Однако одна из главных идей .NET — возможность создания кода на разных языках и объединение скомпилированного кода в интегрированное решение, работающее на основе общей платформы (общеязыковая среда выполнения CLR). Чтобы такое осуществить, программист, использующий C++, должен понимать, какие типы данных этого языка совместимы с общеязыковой средой выполнения CLR и .NET Framework.

Типы данных C++ и общеязыковая среда выполнения CLR

Многие типы данных C++ соответствуют типам данных промежуточного языка IL .NET, определенным спецификацией общего (универсального) языка CLS (Common Language Specification). Некоторые из этих типов, совместимые со спецификацией общего (универсального) языка CLS, гарантированно поддерживаются всеми языками .NET. Они определены в рамках общей системы типов CTS (Common Type System). Спецификация общего (универсального) языка CLS и общая система типов CTS обеспечивают возможность взаимодействия языков, и, хотя C++ поддерживает использование многих типов, несовместимых со спецификацией общего (универсального) языка CLS, такие заблудшие типы следует использовать только в реализациях компонентов, и никогда не открывать в общих сборках. Соблюдение этого правила гарантирует, что программы, использующие подобные сборки, можно будет создавать на любом другом языке .NET, не опасаясь проблем с несовместимостью типов. В табл. 3.1 перечислены типы данных промежуточного языка IL, совместимые со спецификацией общего (универсального) языка CLS. Заметим, что это типы данных промежуточного языка IL, а не C++; но в C++ (и во всех других языках .NET) есть типы, эквивалентные приведенным.

Типы данных C++ и .NET Framework


С другой стороны, некоторым типам C++ соответствуют классы .NET Framework. Для примитивных типов, таких, как int и float (с плавающей точкой), соответствующие классы .NET являются оберточными (wrapping) или, как их еще называют, упаковочными (boxing). Упаковка данных примитивных типов будет рассмотрена в этой главе несколько позже. В следующем примере объявляются переменные разных типов C++ и показываются соответствующие классы .NET Framework, для чего используется метод GetType класса System: :Object (Система::Объект).
Типы данных промежуточного языка

Типы данных промежуточного языка

Содержимое

Bool (логический, булев)

True (Истина) или false (ложь)

char (символ)

Символ Unicode (16-битовый)

System.Object (Система.Объект)

Объект или упакованный значимый тип

System.String (Система.Строка)

Строка Unicode

f Ioat32 (32-разрядный с плавающей точкой)

32-разрядное с плавающей точкой в формате IEEE 754

ftoat64 (64-разрядное с плавающей Точкой)

64-разрядное с плавающей точкой в формате IEEE 754

«its

8-разрядное целое число со знаком

Int16

1 6-разрядное целое число со знаком

k*32

32-разрядное целое число со знаком

kit64

64-разрядное целое число со знаком

unsigned int8

8-разрядное целое число без знака

unsigned int16

16-разрядное целое число без знака

unsigned int32

32-разрядное целое число без знака

unsigned int64

64-разрядное целое число без знака

Типы данных C++ и .NET Framework


С другой стороны, некоторым типам C++ соответствуют классы .NET Framework. Для примитивных типов, таких, как int и float (с плавающей точкой), соответствующие классы .NET являются оберточными (wrapping) или, как их еще называют, упаковочными (boxing). Упаковка данных примитивных типов будет рассмотрена в этой главе несколько позже. В следующем примере объявляются переменные разных типов C++ и показываются соответствующие классы .NET Framework, для чего используется метод GetType класса System: :Object (Система::Объект).
Таблица 3.1. Типы данных промежуточного языка
Типы данных промежуточного языка Содержимое
Bool (логический, булев) True (Истина) или false (ложь)
char (символ) Символ Unicode (16-битовый)
System.Object (Система.Объект) Объект или упакованный значимый тип
System.String (Система.Строка) Строка Unicode
f Ioat32 (32-разрядный с плавающей точкой) 32-разрядное с плавающей точкой в формате IEEE 754
ftoat64 (64-разрядное с плавающей Точкой) 64-разрядное с плавающей точкой в формате IEEE 754
«its 8-разрядное целое число со знаком
Int16 1 6-разрядное целое число со знаком
k*32 32-разрядное целое число со знаком
kit64 64-разрядное целое число со знаком
unsigned int8 8-разрядное целое число без знака
unsigned int16 16-разрядное целое число без знака
unsigned int32 32-разрядное целое число без знака
unsigned int64 64-разрядное целое число без знака
//MappingDataTypes.срр
#using <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
void main(void)
{
bool b = false; // логический (булев) b = ложь; Булева переменная
Char ch = '\0'; // Символ
Object *pobj = new Object; //
Объект String *pstr = S""; // Строка
float f = 1.OF; // f с плавающей точкой = l.OF - одинарная
//точность
double d = 1.0; // Двойная точность
char с = '\0'; // символ SByte
unsigned char uc = '\0'; // Байт - символ без знака
short s = 0; //Intl6 (короткий)
unsigned short us = 0; //UIntl6 - короткий без знака
int i = 0; //Int32
unsigned int ui = 0; //UInt32 - int без знака
long l = 0; //Xnt64
unsigned long ul = 0; //UInt64 - длинный без знака
int intManagedArray _gc[] // System.Int32[] - сборщик мусора
= new int _gc[5]; // сборщик мусора
Console::WriteLine(_box(b)->GetType() ) ;
Console::WriteLine(_box(ch)->GetType()) ;
Console::WriteLine(pobj->GetType() ) ;
Console::WriteLine(pstr->GetType()) ;
Console::WriteLine(_box(f)->GetType());
Console::WriteLine(_box(d)->GetType()) ;
Console::WriteLine(_box(c)->GetType());
Console::WriteLine(_box(uc)->GetType());
Console :: WriteLine (_box ( s) ->GetType () ) ;
Console::WriteLine(_box(us)->GetType() ) ;
Console::WriteLine(_box(i)->GetType() ) ;
Console::WriteLine(_box(ui)->GetType<));
Console::WriteLine(_box(1)->GetType()) ;
Console::WriteLine(_box(ul)->GetType());
Console::WriteLine(intManagedArray->GetType());
}
Программа напечатает:
System.Boolean // Система. Булева переменная
System.Char // Система. Символ
System.Object // Система. Объект
System.String // Система. Строка
System.Single // Система. Одинарный
System.Double // Система. Двойной
System.SByte
System.Byte // Система. Байт
System.Intl6
System.UIntl6
System.Int32
System.UInt32
System.Int32
System.UInt32
System.Int32 []

 

Программирование на C++ для платформы .NET


В этом разделе главы мы изучим основные аспекты создания кода на управляемом C++. В частности, будут рассмотрены все ключевые слова расширения управляемости C++, поддерживаемые Visual C++.NET. Заметим, что это далеко не все ключевые слова Visual C++ 7.0, не определенные стандартом ANSI C++, — ведь мы концентрируем ваше внимание именно на расширении управляемости C++. Однако в рассмотрении затрагиваются некоторые аспекты, не относящиеся к управляемому коду. Например, использование ключевого слова _interface (интерфейс) не ограничивается лишь управляемым кодом. И в заключение мы кратко опишем атрибуты, технически не относящиеся к управляемости.
Соответствие VC++.NET и ANSI C++
Стоит сказать, что все эти особые ключевые слова, связанные с управляемостью, не Противоречат ANSI C++, так что фактически VC++.NET является более совместимым с ANSI C++, нежели предыдущие версии VC++.
При использовании командной строки следует задавать параметр /CLR (Компиляция для выполнения в общеязыковой среде) компилятора, иначе применение ключевых слов, связанных с управляемостью, не допускается. В Visual Studio корректные установки параметров обеспечиваются при выборе соответствующего шаблона автоматически. Тем не менее, если возникла необходимость установить корректные значения параметров, выполните следующие указания:
1. Щелкните в окне Solution Explorer (Поиск решения) правой кнопкой на узле проекта (но не на узле решения).
2. Выберите пункт меню Properties (Свойства) При этом откроется диалог Project Property Pages (Страницы свойств проекта)
3. Выберите узел General (Общие) под узлом C/C++ и выберите Assembly Support (/clr) для опции Compile As Managed (Компилировать как управляемый).
4. Щелкните на кнопке ОК.

Управляемые и неуправляемые типы


Управляемый тип — тип данных, инициализируемый (обычно с помощью оператора new (создать)) в управляемой динамически распределяемой области памяти, но ни в коем случае не в неуправляемой динамически распределяемой области памяти или стеке Попросту говоря, управляемый тип — тип, для которого сборка мусора осуществляется автоматически, потому для освобождения ресурсов, используемых объектами этого типа, нет необходимости использовать оператор delete (удалить). Вместо того чтобы явно удалять объект, можно либо сделать так, чтобы на него не указывал ни один указатель, либо явно приравнять этот указатель нулю. Неуправляемый тип — тип, который игнорируется автоматическим сборщиком мусора, вследствие чего программист должен освобождать занимаемую объектом память с помощью оператора delete (удалить).
Объекты неуправляемых типов никогда не создаются в управляемой динамически распределяемой области памяти, а только либо в неуправляемой динамически распределяемой области памяти, либо в каком-нибудь другом месте памяти, как переменные в стеке или элементы данных другого неуправляемого класса. Поэтому именно неуправляемые типы — это то, с чем привыкли иметь дело программисты C++, тогда как управляемые типы больше похожи на ссылочные типы языка Java, для которых применяется автоматическая сборка мусора.
Ключевое слово _дс (сокращение от "garbage collection" — "сборка мусора") используется для объявления управляемых классов, или структур, и может использоваться для указателей и массивов. Ключевое слово _поде (сокращение от "no garbage collection" — "без сборки мусора") является антонимом _дс (сборщик мусора). Надо иметь в виду, что ключевое слово _дс (сборка мусора) можно использовать только в управляемом коде, а значит, при этом следует использовать параметр компилятора /CLR (Компиляция для выполнения в общеязыковой среде), причем прагма Ipragma unman-aged должна быть неактивна. Ключевое слово _nogc (без сборки мусора) можно использовать как в управляемом, так и в неуправляемом коде. Следующий фрагмент демонстрирует типичное использование _дс (сборщик мусора) при определении управляемого класса:
_gc class ManagedClass
// класс сборщика мусора ManagedClass
{
};
Ключевое слово _поде (без сборки мусора) просто означает, что класс, структура, массив или объект, на который указывает определенный с этим словом указатель, не управляется сборщиком мусора .NET. Данное ключевое слово используется для явного указания, что объект никогда не создается в управляемой динамически распределяемой области памяти. Недопустимо наследование типа, определенного с ключевым словом _дс (сборщик мусора) или _поде (без сборки мусора), от типа, определенного с другим из этих ключевых слов, равно, как не допускается использование _дс (сборщик мусора) в неуправляемом коде.
_nogc class UnmanagedClass
{
};
Заметим, что автоматическая сборка мусора управляемых объектов касается лишь освобождения неиспользуемой управляемой динамически распределяемой области памяти, но не других ресурсов, таких, как дескрипторы файлов или соединения с базами данных.

Управление сборкой мусора


В документации по языкам .NET вы могли встречать описание метода Finalize (Завершить), используемого для освобождения ресурсов, не находящихся в управляемой динамически распределяемой области памяти, но созданных управляемыми объектами. Однако в C++ реализовывать данный метод не надо. Если же все-таки сделать это, компилятор выдаст сообщение об ошибке, указав, что вместо метода Finalize (Завершить) для управляемого класса требуется определить деструктор. Сборщик мусора автоматически вызовет деструктор (в отдельном потоке) при освобождении памяти, занятой объектом; но момент вызова деструктора не определен. А это значит: не следует рассчитывать на то, что деструктор будет вызван при удалении ссылки на объект.
Если вы реализовали деструктор и удаляете управляемый объект явно, деструктор будет вызван сразу, и сборщик мусора уже не будет его вызывать. Можно также, вызвав статический метод GC: :Collect () (Сборщик мусо-ра::Собрать()), вынудить сборщик мусора попытаться освободить память из-под объекта, а вызов деструктора синхронизировать с завершением работы сборщика мусора при помощи статического метода GC: :WaitForPending-Finalizers. Впрочем, обычно неудобно и неэффективно вызывать сборку мусора принудительно или синхронизовать ее с вызовом деструктора, поэтому, если необходимо выполнять очистку в определенный момент, рекомендуется реализовать это независимо в отдельном методе, а затем вызывать его явным образом. Этот метод рекомендуется называть Dispose (Ликвидировать). Рассмотрим следующую программу.
//ManagingGC.срр
fusing <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
_gc class ManagedClass
// класс сборщика мусора ManagedClass
{
public:
ManagedClass ()
{
Console::WriteLine("c'tor");
}
~ManagedClass ()
{
Console::WriteLine("d'tor");
}
};
void main(void)
{
Console::WriteLine("start"); // начало
ManagedClass *pmc = new ManagedClass;
// Раскомментируйте следующую строку
// для предотвращения вызова деструктора
//GC::SuppressFinalize(pmc); // СБОРЩИК МУСОРА
Console::WriteLine("middle"); // середина
// Раскомментируйте следующую строку
// чтобы вызвать деструктор пораньше
//delete pmc; // удалить
pmc = 0;
// ... или две следующие строки для того,
// чтобы вызвать деструктор пораньше
//GC::Collect(); // СБОРЩИК МУСОРА:: Собрать
//GC:-.WaitForPendingFinalizers () ; // СБОРЩИК МУСОРА
Console::WriteLine("end"); // конец
}
Приведем результат работы программы. Обратите внимание, что деструктор вызывается после того, как программа напечатает end (конец).
start // начало
c'tor
middle // середина
end // конец
d'tor
Однако, если раскомментировать строку, содержащую вызов метода SuppressFi-nalize, деструктор не будет вызван вообще, что доказывается следующей выдачей.
start // начало
с' tor
middle // середина
end // конец
Кроме того, если раскомментировать оператор, в котором используется delete (удалить), деструктор будет вызван до того, как программа напечатает end (конец).
start // начало
c'tor
middle // середина
d'tor
end // конец
Наконец, если раскомментировать только два оператора, содержащих вызовы методов Collect (Собрать) и WaitForPendingFinalizers, деструктор опять будет вызван до того, как программа напечатает end (конец) В этом случае вызов метода Collect (Собрать) приводит к вы зову деструктора, а метод WaitForPendingFinalizers приостанавливает выполнение текущего потока до завершения работы деструктора.
start // начало
c'tor
middle // середина
d'tor
end // конец

Типовая безопасность


Программы, написанные на C++, не обладают свойством типовой безопасности Программы же на управляемом C++ должны гарантированно обладать указанным свойством Однако, из-за того, что программы C++ могут содержать неуправляемый код, они не обязательно обладают свойством типовой безопасности Нельзя производить арифметические операции с управляемыми указателями Кроме того, нельзя приводить тип управляемого указателя к неуправляемому Поэтому можно доказать безопасность только тех программ на C++, которые содержат лишь управляемые код и данные [Управляемый C++ может генерировать код, гарантированно обладающий свойством типовой безопасности, если избегать использования некоторых особенностей языка, таких, как неуправляемые указатели или приведение типов Для проверки типовой безопасности сборки можно использовать утилиту Pevenfy.exe]. Тем не менее, любая программа на C++, в которой выполняются арифметические действия над неуправляемыми указателями или приводятся типы управляемых указателей к неуправляемым, является потенциально опасной.
Следующая программа является примером небезопасного кода на C++, в котором выполняется приведение указателя pumc на неуправляемый объект к указателю на переменную типа j_nt В этом случае подобная операция не является опасной, но в общем случае ее выполнение может представлять опасность Затем выполняется арифметическое действие над указателем на объект, которое уже в этом примере небезопасно, так как получающийся в результате указатель не указывает на какой-либо объект Еще ниже в этом примере, в закомментированных строках, те же действия совершаются над управляемым указателем рте Если бы строки были не закомментированы, компилятор выдал бы сообщение об ошибке
// Unmanaged.срр
# using <mscorllib.dll>
class UnmanagedClass
// класс UnmanagedClass
{
public:
int i;
};
_gc class ManagedClass
// класс сборщика мусора ManagedClass
{
public:
int i;
};
void main(void)
{
UnmanagedClass *pumc = new UnmanagedClass;
pumc->i = 10;
int * pi = (int *}pumc; // Опасно приведение указателя
pi = (int *)(pumc+1); // Опасность: арифметика над указателями
ManagedClass *pmc = new ManagedClass; pmc->i = 10;
//pi = (int *)pmc; // Ошибка: приведение _gc //(сборщик мусора) * к *
//pi = (int *)(pmc+1); // Ошибка, арифметика над _gc // (сборщик мусора) *
}

Типы значений


Ключевое слово _value (значение) похоже на _nogc (без сборки мусора), поскольку оно используется для того, чтобы класс или структура не участвовали в сборке мусора Это полезно для определения объектов в стеке, а не в управляемой динамически распределяемой области памяти Основная цель использования таких типов — возможность создания объектов, не требующих затрат на сборку мусора Использование ключевого слова _value (значение) имеет побочный эффект — класс автоматически становится конечным (ключевое слово _sealed) и не может быть абстрактным (ключевое слово _abstract к нему неприменимо)
_value struct ValueStruct {
int i;
};
Может показаться удивительным, что правила позволяют определять тип _value (значение) там, где не позволяется определять тип _дс (сборщик мусора). В следующем фрагменте кода показан пример этого (вместе с несколькими другими конструкциями). Заметьте, что объекты классов Мап-agedClass, NonManagedClass и ValueClass можно создавать в динамически распределяемой области памяти, тогда как в стек можно поместить объекты только классов NonManagedClass и ValueClass. Последний оператор во фрагменте закомментирован, так как иначе компилятор выдал бы сообщение о недопустимости объявления управляемого объекта как переменной, помещаемой в стек.
//ValueType.срр
#using <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
_nogc class NonManagedClass
{
};
_value class ValueClass
// класс значения ValueClass
{
};
_gc class ManagedClass
// класс сборщика мусора ManagedClass
{
NonManagedClass nmc; // Странно! Но для компилятора это не ошибка!
ValueClass vc; // Это не ошибка, здесь допускается тип значения
};
void main(void)
{
NonManagedClass *pnmc = new NonManagedClass; //Нет ошибки
ValueClass *pvc = _nogc new ValueClass; //Нет ошибки
ManagedClass *pmc = new ManagedClass; //Нет ошибки
NonManagedClass; //Нет ошибки в стеке
ValueClass vc; //Нет ошибки в стеке
//ManagedClass me; // ошибка, не может быть размещен в стеке
}

Абстрактные типы


Значение ключевого слова _abstract (абстрактный) очень похоже на значение ключевого слова abstract (абстрактный) в языке Java. Оно также напоминает о сложившейся традиции рассматривать класс C++, содержащий хотя бы одну чистую (pure) виртуальную функцию, как абстрактный. Ключевое слово _abstract (абстрактный) делает это объявление явным. Как и в случае ключевого слова _interface (интерфейс), ключевое слово _abstract (абстрактный) используется для обозначения того, что класс определяет некоторые общие обязательные соглашения между кодом, реализующим методы этого абстрактного класса, и кодом клиентов, вызывающих эти методы. Обратите внимание, что, если абстрактный класс определяется как управляемый, в его описании следует использовать также и ключевое слово _gс (сборщик мусора).
Абстрактный класс подобен интерфейсу в том, что он является лишь средством проявления полиморфизма, а создать экземпляр такого класса непосредственно нельзя. Однако, в отличие от интерфейса, абстрактный класс может содержать реализации нескольких, или даже всех своих методов. Абстрактный класс может быть использован как базовый для других классов, экземпляры которых можно инициализировать, причем переменная абстрактного ссылочного типа (т.е. ссылка или указатель, но не тип значения) может использоваться для обращения к экземплярам классов, производных от абстрактного класса.
Обратите внимание на то, что использование ключевого слова _abstract (абстрактный) вместе с _interface (интерфейс) (это слово не является расширением управляемости) является избыточным, так как интерфейсы по определению являются абстрактными. Ключевое слово _abstract (абстрактный) нельзя использовать в комбинации с ключевыми словами _value (значение) или _sealed (конечный). Ключевое слово _value (значение) указывает на то, что объект содержит непосредственно данные, а не ссылки на объекты в динамически распределяемой области памяти. Это значит, что можно создавать экземпляры такого класса, а следовательно, он не может быть абстрактным. Ключевое слово _sealed (конечный) означает, что класс не может быть базовым для других классов, что, очевидно, противоречит концепции абстрактного класса. В следующем фрагменте приведен пример типичного использования ключевого слова _abstract (абстрактный).
//AbstractExample.срр
#using <rnscorlib.dll>
using namespace System;
// использовать пространство имен Система;
_abstract class AbstractClass
// абстрактный класс AbstractClass
{
public:
virtual void Methodlf) = 0; // виртуальный; не реализован здесь
virtual void Method2() // виртуальный; реализован здесь
{
Console::WriteLine("Method2");
}
};
class DerivedClass : public AbstractClass
{
public:
void Method1() // реализован здесь
{
Console::WriteLine("Method1");
}
};
void main(void) » {
//AbstractClass *pac = new AbstractClass; // ошибка
AbstractClass *pac = new DerivedClass; // указатель
pac->Methodl();
pac->Method2();
AbstractClass &ac = *new DerivedClass; // ссылка
ас.Methodl();
ac.Method2() ; }
Профамма напечатает:
Method1
Method2
Method1
Method2

Интерфейсы


Ключевое слово _interface (интерфейс) технически не относится к расширению управляемости, так как его можно использовать и в управляемом, и в неуправляемом коде. Однако оно часто используется при создании управляемого кода, поэтому стоит остановиться для его рассмотрения.
Интерфейсы используются как обобщенные базовые типы для классов, при реализации которых применяются некоторые общие соглашения (контракты). Эти контракты используются для согласования реализации основной программы и программы-клиента посредством определения общего полиморфного набора методов. Интерфейс можно считать крайней формой абстрактного класса, поскольку цели их существования сходны, но интерфейсы — наименее конкретная его разновидность. Так сложилось, что программисты, работающие с C++, используют термин "интерфейс" для обозначения любого класса, содержащего лишь чистые виртуальные методы. Поэтому новое ключевое слово _interface (интерфейс) лишь делает эту договоренность явной.
Класс, определенный с использованием ключевого слова _interface (интерфейс), может содержать лишь общедоступные (public) чистые виртуальные методы. В частности, ни один из методов класса не должен быть реализован, класс не может содержать статические или нестатические элементы данных, конструкторы, деструкторы, статические методы, и не может перегружать операторы. Интерфейс может быть потомком любого количества базовых интерфейсов, но не потомком какого бы то ни было абстрактного или неабстрактного класса. Обратите внимание, что, хотя интерфейс не может содержать элементы данных, он может содержать свойства (доступ к которым осуществляется методами получения/установки (get/set)). О свойствах будет рассказано ниже. Как и в случае абстрактных классов, создать экземпляр интерфейса нельзя, так что они используются как полиморфные базовые классы.
В описании интерфейса можно использовать только спецификатор общего доступа (public); однако его использование не обязательно, поскольку в качестве спецификатора доступа по умолчанию принимается именно public (общедоступный). Исходя из того, что задача интерфейса — определять базовый контракт для производных классов, несложно сделать вывод, что описывать интерфейс с ключевым словом _sealed (конечный) бессмысленно.
К управляемым интерфейсам (т.е. определенным с ключевым словом _дс (сборщик мусора)) предъявляются некоторые дополнительные требования. Они не могут быть производными от неуправляемых интерфейсов. Однако они могут быть непосредственными потомками произвольного количества управляемых интерфейсов. Следующий фрагмент представляет пример типичного использования ключевого слова _interface (интерфейс):
//InterfaceExample.срр
fusing <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
_interface Somelnterfасе // интерфейс
{
public:
virtual void Methodl() = 0; // чистый виртуальный явный
void Method2(); // чистый виртуальный подразумеваемый
};
class DerivedClass : public Somelnterface
{
public:
void Methodl() // реализован здесь
{
Console::WriteLine("Methodl");
}
void Method2() // реализован здесь
{
Console::WriteLine("Method2");
}
};
void main(void)
{
//Somelnterface *psi = new Somelnterface; // ошибка
Somelnterface *psi = new DerivedClass; // указатель
psi->Methodl();
psi->Method2();
Somelnterface &si = *new DerivedClass; // ссылка
si.Methodl ();
si.Method2 () ;
}
Программа напечатает:
Method1
Method2
Method1
Method2

Упаковка и распаковка примитивных типов данных


Упаковка и распаковка — важная концепция программирования в .NET вне зависимости от того, какой именно язык программирования вы используете. Одно из самых важных преимуществ .NET — унифицированная система типов. Каждый тип, в том числе простые упакованные встроенные типы, такие как _box (int), является потомком класса System.Object (Система.Объект). В языках, подобных Smalltalk, все типы являются объектами, но это приводит к неэффективности использования простых типов. В стандартном C++ простые встроенные типы данных и объекты обрабатываются по-разному, — это повышает эффективность использования типов, но исключает возможность унификации системы типов. Управляемый C++ объединяет преимущества обоих подходов, используя прием, называемый упаковкой (boxing). Упаковка — преобразование типов значений, таких, как int или double (с удвоенной точностью), в ссылку на объект, хранимый в динамически распределяемой области памяти. Упаковка производится с помощью ключевого слова _box. Распаковка — преобразование упакованного типа (хранимого в динамически распределяемой области памяти) в неупакованное значение (хранимое в стеке). Распаковка выполняется приведением типов. Проиллюстрируем упаковку и распаковку следующим фрагментом кода:
int x = 5; // простой встроенный тип int
_box int *po = _box(x); // упаковка
x = *ро; // распаковывание
Ключевое слово _box создает в управляемой динамически распределяемой области памяти управляемый объект, инкапсулирующий копию выражения, имеющего тип значения. Под выражением, имеющим тип значения, подразумевается примитивный тип данных, такой как int, float (с плавающей точкой), double (с удвоенной точностью), или char (символ), либо тип значения, определенный как класс или структура и описанный с использованием ключевого слова _value (значение). Например, предопределенный управляемый тип _boxed_System_Int32 инкапсулирует упакованный int, a управляемый тип _boxed_ValueStruct — упакованный тип значения ValueStruct. Эти странные названия типов (_boxed_System_Int32 и _boxed_ValueStruct) не обязательно будут встречаться в вашем исходном коде, но они показываются утилитой Ildasm.exe. Обратите внимание, что _box int * — альтернативное имя управляемого типа _boxed_System_Int32, a _box ValueStruct* — альтернативное имя управляемого типа _boxed_ValueStruct.
Если ключевое слово _box используется для создания управляемого объекта, сборщик мусора .NET будет автоматически освобождать память, используемую данным объектом. Это похоже на концепцию использования для примитивных типов интерфейсных классов, однако упаковка имеет более важное значение в среде .NET, чем в программировании на обычном C++. Это происходит из-за того, что объекты в C++ можно использовать и как значения, и как ссылочные типы, тогда как в среде .NET управляемые объекты всегда являются ссылочными типами (т.е. ссылками или указателями на объекты, хранимые в управляемой динамически распределяемой области памяти).
Доступ к типам значений осуществляется так же, как и доступ к неупакованным типам. В приведенном ниже коде это делается в присваивании plntBox = 50. Несмотря на то, что plntBox указывает на управляемый объект, разыменованный указатель используется так, как будто он является просто указателем на неупакованный тип int.
//BoxExample.срр
#using <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
_value struct ValueStruct
{
public:
int i;
};
// функция ожидает получить управляемый указатель на объект
void ExpectManagedObjectPointer(
_box ValueStruct* pManagedObject)
{
pManagedOb]ect->i = 20; // изменяет упакованную копию
Console::WriteLine(pManagedObject->i) ;
}
// функция ожидает получить управляемый указатель на объект
void ExpectBoxedPrimitivePointer(_box int* plntBox)
{
*pIntBox = 50; //изменяет упакованную копию примитивного типа
Console::WriteLine(*рIntBox);
}
void main(void)
{
ValueStruct ValueStruct; // объект типа значение в стеке
ValueStruct.i = 10; // изменяет оригинал распакованной копии
Console::WriteLine(ValueStruct.i);
_box ValueStruct* pManagedObject
= _box(valueStruct); //_boxed_ValueStruct
ExpectManagedObjectPointer(pManagedObject) ;
pManagedObject->i = 30;* // изменяет упакованную копию
Console::WriteLine(pManagedObject->i);
int j; // тип значения - примитивный тип данных
j = 40; // изменяет первоначальный распакованный
// примитивный тип
Console::WriteLine(j);
_box int *p!ntBox = _box(j); // ynaKOBaHHbm_System_Int32
ExpectBoxedPrimitivePointer(plntBox);
}
Приведенная программа напечатает:
10
20
30
40
50

Делегаты


Ключевое слово _delegate (делегат) используется для объявления класса-делегата, основанного на описании сигнатуры метода. Делегаты очень сходны с указателями на функции в обычном C++, с той лишь разницей, что делегат может указывать только на метод управляемого класса. Чаще всего в приложениях .NET Framework делегаты используются для реализации функций обратного вызова или обработки событий. Однако они могут найти применение во всех случаях, когда необходимо вызывать методы динамически.
В .NET Framework определены (как абстрактные классы) два типа делегатов — System: : Delegate (Система::Делегат) и System: :MulticastDelegate. Эти два типа делегатов используются как базовые классы для одноадресных (или делегатов единственного приведения — single-cast) и многоадресных (или групповых — multicast) делегатов соответственно. Одноадресный делегат связывает указатель на метод с методом одного управляемого объекта, тогда как многоадресный делегат связывает указатель на метод с одним или несколькими методами управляемого объекта. Вызов одноадресного делегата приводит к вызову только одного метода, а при вызове многоадресного делегата может выполняться неограниченное количество методов. В связи с тем, что многоадресный делегат можно использовать и для вызова одного метода, одноадресная форма делегата является излишней. Обычно в программах используются лишь многоадресные делегаты.
Встретив в программе ключевое слово _delegate (делегат) компилятор создает особый управляемый класс, производный от System: :MulticastDelegate. Конструктор этого класса имеет два аргумента: указатель на экземпляр управляемого класса (который равен нулю, если делегат связывает статический метод), и сам метод, вызываемый с помощью делегата. Этот класс также содержит метод Invoke (Запустить), сигнатура которого совпадает с сигнатурой метода, вызываемого делегатом. Следующий пример демонстрирует использование делегатов:
//DelegateExample.срр
#using <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
// определить управляемые классы для использования
// в качестве делегатов
_delegate int SomeDelegate // делегат
(int i, int j);
_delegate // делегат
void SomeOtherDelegate (int i);
_gc class SomeClass
// класс сборщика мусора SomeClass содержит методы,
// вызываемые делегатами
{
public:
int SomeMethod(int i, int j)
{
Console::WriteLine(
"SomeMethod({0}, {!})", _box(i), _box(j));
return i+j; }
static int SomeStaticMethod(int i, int j) // статический
{
Console::WriteLine(
"SomeStaticMethod({0}, {!})", _box(i), _box(j));
return i+j; }
void SomeOtherMethod(int i) {
Console::WriteLine(
11 SomeOtherMethod ({0}) ", _box(i) ) ;
}
};
Void main ()
{
SomeDelegate *pscd; int sum; // сумма
// связать делегат с нестатическим методом
// требуется экземпляр
SomeClass SomeClass * psc = newSomeClass(); pscd = // создать экземпляр класса делегат sc new SomeDelegate(
psc, SSomeClass::SomeMethod); // нестатический sum = pscd->Invoke(3, 4); // вызвать метод через делегат // сумма = pscd->Bbi3BaTb (3, 4); Console::WriteLine(sum); // сумма
// связать делегат со статическим методом, - нет нужды
// ни в каком экземпляре
pscd = // создать другой экземпляр класса делегата sc new SomeDelegate(
О, SSomeClass::SomeStaticMethod); // статический sum = pscd->Invoke(3, 4); // вызвать метод через делегата // сумма = pscd->Bbi3B3Tb (3, 4); Console::WriteLine(sum); // сумма
// объединить два делегата SomeClass * pscl = new SomeClass(); SomeClass * psc2 = new SomeClass(); SomeOtherDelegate *pmcdl = new SomeOtherDelegate(
pscl, &SomeClass::SomeOtherMethod); SomeOtherDelegate *pmcd2 = new SomeOtherDelegate(
psc2, SSomeClass::SomeOtherMethod); SomeOtherDelegate *pmcd =
static_cast<SomeOtherDelegate *>(Delegate:.Combine(
// Объединение делегатов pmcdl, pmcd2)); pmcd->Invoke(1); // Вызвать }
SomeMethod(3, 4)
7
SomeStaticMethod(3, 4)
7
SomeOtherMethod(I)
SomeOtherMethod(1)

События


События представляют собой механизм, посредством которого объект имеет возможность получать информацию о происходящем вне него Событие может быть вызвано неким действием пользователя, например, нажатием кнопки мыши, или некими изменениями состояния приложений, например, приостановкой или завершением задачи Объект, генерирующий событие, называется источником или отправителем события, объект, который реагирует на событие, называется приемником или получателем события
В обычном C++ для работы с событиями реализуют функции обратного вызова, для выполнения которых используются указатели на функции В модели компонентных объектов Microsoft (COM) для работы с событиями используются интерфейсы IConnec-tionPomt и IConnectionPointContainer В NET используются управляемые события Все эти подходы по сути одинаковы, так что для их объединения Microsoft предложила Унифицированную модель событий (Unified Event Model) Для поддержки этой новой Унифицированной модели событий в C++ введены новые ключевые слова _event (событие), _hook (привязать) и _unhook (отцепить), а также атрибуты event_source (источник события) и event_receiver (приемник события)
Ключевое слово _event (событие) используется для описания события, которое может быть сгенерировано источником события Это слово можно использовать не только в управляемых классах, оно может применяться к следующим объявлениям
1. Описание метода класса обычного C++ (обычный обратный вызов)
2. Описание интерфейса модели компонентных объектов Microsoft (COM) (точка стыковки)
3. Описание метода управляемого класса (управляемое событие)
4. Описание элемента данных управляемого класса (управляемое событие с использованием делегата)
Мы рассмотрим только третий случай, т е случай, в котором источником события является метод управляемого класса Для того чтобы объявить обработчиком какого-то события метод класса-получателя этого события, используется ключевое слово _hook (привязать) После того, как это сделано, при каждом возникновении события будет вызываться его обработчик А чтобы такое объявление метода аннулировать, используется ключевое слово _unhook (отцепить) В следующем примере демонстрируется использование ключевых слов _event (событие), _hook (привязать) и _unhook (отцепить), а также атрибутов event_source (источник события) и event_receiver (приемник события) для реализации механизма обратного вызова
//Event.cpp
fusing <mscorlib.dll>
using namespace System;
// использовать пространство имен Система,
[event_source(managed)] // управляемый
public _gc class ManagedEventSource
// класс сборщика мусора ManagedEventSource
{ public:
_event void ManagedEvent(} ; // нет реализации
void Fire_ManagedEvent()
{
ManagedEvent();
}
};
[event_receiver(managed)] // управляемый
gc class ManagedEventReceiver // класс сборщика мусора ManagedEventReceiver
{
public:
void HandleManagedEvent() // вызывается через ManagedEvent
{
Console::WriteLine("HandleManagedEvent called");
}
void HookEvent(ManagedEventSource *pEventSource)
{
_hook( // обработчик
SManagedEventSource::ManagedEvent,
pEventSource,
SManagedEventReceiver.:HandleManagedEvent) ,
}
void UnhookEvent(ManagedEventSource* pEventSource)
{
_unhook( // отцепиться
&ManagedEventSourсе::ManagedEvent,
pEventSource,
SManagedEventReceiver:HandleManagedEvent) ; }
};
void main ()
{
ManagedEventSource* pEventSource =
new ManagedEventSource;
ManagedEventReceiver* pReceiver =
new ManagedEventReceiver;
pReceiver->HookEvent(pEventSource) ;
pEventSource->Fire_ManagedEvent(); // вызывается обработчик
pReceiver->UnhookEvent(pEventSource);
}
Профамма напечатает:
HandleManagedEvent called

Свойства


Ключевое слово _property (свойство) используется для указания на то, что метод получения и/или установки реализует свойство управляемого класса. В отличие от элемента данных (называемого также полем), который однообразен и негибок, свойство может быть доступно для чтения и записи, либо только для чтения и только для записи, может быть реализовано как обычная переменная или как вычисляемое значение. Например, свойство только для чтения должно быть реализовано методом get_, но не методом set_. Свойство доступно другим программам, написанным на любых языках .NET, посредством использования обычного синтаксиса доступа к элементам данных этого языка, как если бы свойство было обычным элементом данных (точнее псевдоэлементом данных). Это продемонстрировано в следующем фрагменте кода, в котором pmcwp->someProperty используется так, как если бы оно было элементом данных с именем someProperty. Фактически такого элемента данных не существует, но то, что класс содержит методы get_someProperty и set_someProperty, объявленные с ключевым словом _property (свойство), делает возможным такой способ доступа. В действительности класс содержит защищенный (protected) элемент данных m_someProperty, но это уже подробности реализации, инкапсулированные в компоненте. Свойство компонента в .NET похоже на свойства OLE-автоматизации в компонентах ActiveX или свойства компонентов (bean) в JavaBean.
Другой интересной чертой управляемых свойств, отсутствующей у элементов данных, является то, что вы можете проверить приемлемость аргумента метода set_ и, в случае, если значение аргумента некорректно, вызвать исключение. Это помогает в создании корректно работающих компонентов.
//PropertyExample.срр
#using <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
_gc class ManagedClassWithProperty
// класс сборщика мусора ManagedClassWithProperty
{
public:
ManagedClassWithProperty() : m_someProperty(0) {}
_property int get_someProperty() // свойство -
// должно быть "get_"
{
return m_someProperty;
}
_property void set_someProperty( // свойство
int propertyValue) // должно быть "set__"
{
m_someProperty = propertyValue;
}
protected: // защищенный
int m_someProperty; // можно реализовать как элемент данных
};
void main() {
ManagedClassWithProperty* pmcwp = new ManagedClassWithProperty;
pmcwp->someProperty = 7; // псевдоэлемент данных
Console::WriteLine(pmcwp->someProperty) ;
}
Вышеприведенная программа выведет:
7

Закрепление управляемых объектов


Ключевое слово _pin (закрепить) указывает на то, что указатель на управляемый объект будет оставаться корректным (т.е. общеязыковая среда выполнения CLR не переместит Объект в памяти) на протяжении существования закрепленного указателя. Закрепленный Объект остается на своем месте в памяти до тех пор, пока на него указывает закрепленный указатель. Если изменить указатель так, что он будет указывать на другой объект или присвоить ему нулевое значение, объект может быть перемещен сборщиком мусора. Когда при определении указателя не задано ключевое слово _pin (закрепить), общеязыковая среда Выполнения CLR может в любой момент переместить объект, на который указывает этот указатель. Перемещение объектов происходит вследствие сборки мусора и уплотнения динамически распределяемой области памяти, выполняемых общеязыковой средой выполнения CLR. Эти перемещения не сказываются на управляемом коде, так как общеязыковая среда выполнения CLR автоматически изменяет значения управляемых указателей при перемещении объектов, но могут повлиять на выполнение неуправляемого кода, в котором Используются неуправляемые указатели на управляемые объекты.
Ключевое слово _рш (закрепить) следует применять только в тех случаях, когда это Крайне необходимо, так как закрепление объектов расстраивает сборку мусора и снижает ее эффективность. Для примера необходимости закрепления можно упомянуть ситуацию, в которой вы передаете неуправляемой функции в качестве аргумента указатель На управляемый объект (или указатель на элемент данных такого объекта). В данном случае проблема состоит в том, что в процессе выполнения программы управляемый объект Может быть перемещен сборщиком мусора, но неуправляемая функция будет при этом Использовать старый, некорректный указатель. Это приведет к тому, что неуправляемая функция обратится по некорректному адресу и последствия этого могут быть катастрофическими.
Ниже приведен фрагмент, иллюстрирующий использование ключевого слова _pin (закрепить) в описанной ситуации. Обратите внимание, что объект pPinnedObject закреплен в памяти, так что передача указателя на него методам SetGlobalPointerValue и GetGlobalPointerValue в качестве аргумента является допустимой. Реализация этих методов основана на том, что глобальный указатель дх остается корректным, а это может быть верны только в случае, когда общеязыковая среда выполнения CLR не будет перемещать объект класса ManagedClass. Заметим, что компилятор способен предсказать возникновение такой ситуации и выдаст сообщение об ошибке, если из приведенного примера удалить ключевое слово _pin (закрепить).
//PinExample.срр
#using <mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
_gc class ManagedClass
// класс сборщика мусора ManagedClass
{
public:
int x; };
ttpragma unmanaged // неуправляемый
int *gx; // глобальный указатель void SetGlobalPointer(int* pi)
{
// установить глобальный указатель,
// чтобы указать на управляемый объект
gx = pi;
}
void SetGlobalPointerValue(int i)
{
// установить управляемый объектный элемент данных
// через глобальный указатель
*gx = i;
}
int GetGlobalPointerValue()
{
// получить управляемый объектный элемент данных
// через глобальный указатель
return *gx;
}
Ipragma managed // управляемый
void main()
{
ManagedClass _pin * pPinnedObject = new ManagedClass;
// обратите внимание на ошибку, генерируемую компилятором
//в следующей инструкции...
// если ключевое слово _pin удалить из предыдущей инструкции
SetGlobalPointer(&pPinnedObject->x); // неуправляемый
SetGlobalPointerValue(1); // неуправляемый
int x = GetGlobalPointerValue();//неуправляемый
}

 

Резюме


В этой паве рассмотрено использование расширений C++ при создании программ и компонентов для платформы NET Изложение основных концепций создания управляемого кода на С-г+ проил пострировано на MHOIHX примерах, таких, как HelloWorld (Привет, мир), ConvertTemp и др. Были рассмотрены классы String (Строка) и Array (Массив) активно используемые почти во всех типах приложений NET Подробно изучены ключевые слова расширения управляемости C++ Рассмотрены также делегаты, события и управление обработкой исключений Напоследок проанализировано использование атрибутов С++ для создания компонентов на основе модели компонентных объектов Miciosott (COM)

 

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика