Введение в MSIL - Часть 7: Приведение и преобразование типов — Архив WASM.RU

Все статьи

Введение в MSIL - Часть 7: Приведение и преобразование типов — Архив WASM.RU

Приведение и преобразование типов часто вызывает различные беспокойства среди программистов: например, по поводу скорости выполнения, безопасности или просто не совсем хорошее понимание последствий данных операций. В этой главе цикла я расскажу о них. Для примеров я буду использовать в основном C++, так как он имеет богатый набор языков средств, относящихся к приведению типов. Конечно, мы также будем рассматривать инструкции CLI, которые генерируются в итоге.

Статическое приведение типов

Когда происходит преобразование типа меньшего размера в тип большего размера, то можно использовать неявное приведение, так как нет угрозы потери данных. Это безопасно, пока используемое целевым типом пространство является надмножеством пространства исходного типа. Конвертация из 32-х битного целого числа в 64-х битное целое число будет точным, поэтому компиляторы не требуют от программиста непосредственного предупреждения. С другой стороны, когда просходит преобразование из большего типа в меньший, возможно обрезание данных, так что компилятору обычно требуется подтверждение в виде приведения типа. Это называется статическим приведением типов, так как для определения вида конвертации используется только статичная информация. Такое приведение часто считается небезопасным, так как компилятор полностью возлагает правильность приведения на программиста. Рассмотрим следующий C++-код:

Int32 small = 123;
Int64 big = small;
 
small = static_cast<Int32>(big);

Преобразование из меньшей переменной в большую является неявным, но обратная операция требует оператора static_cast, чтобы избежать предупреждения компилятора о возможной потере данных. Хотя статическое приведение типов может и должно считаться небезопасным, компилятор по мере возможностей пытается убедиться, что полученное значение будет правильным во время выполнения. Он делает это, оценивая информацию о статическом приведении. В случае с типами, заданными пользователем, он пытается определить, заданы ли какие-нибудь операторы преобразования. Все они задаются во время компилирования. Рассмотрим следующую реализацию в виде MSIL:

.locals init (int32 small,
              int64 big)
 
// Int32 small = 123;
ldc.i4.s 123
stloc small
 
// Int64 big = small;
ldloc small
conv.i8
stloc big
 
// small = static_cast<Int32>(big);
ldloc big
conv.i4
stloc small

Инструкция ldc.i4.s помещает 4-х байтное (32-х битное) число 123 в стек. Затем оно зачастую сохраняется в маленькую локальную переменную, используя инструкцию stloc. Чтобы присвоить значение маленькой переменной большой, значение сначала помещается в стек с помощью инструкции ldloc. Затем инструкция conv.i8 преобразует значение в 8-ми байтное (64-х битное) число, после чего оно забирается из стек и сохраняется в большой локальной переменной, используя инструкцию stloc. Наконец, чтобы преобразовать значение большой переменной обратно в маленькую, значение первой помещается в стек, а затем 8-ми байтное значение на стеке конвертируется в 4-х байтное посредством инструкции conv.i4, после чего результат с помощью stloc сохраняется в маленькой локальной переменной.

Как вы можете видеть, MSIL не делает различия между явным и неявным приведением типов. Всё является явным. Тем не менее, защита от переполнения должна запрашиваться отдельно, так как по умолчанию не производится никакой проверки. В вышеприведённом коде этого не делалось. Код является безопасным, потому что мы знаем, что это так, но если бы значение, сохранённое в большой переменной было бы слишком велико для 4-х байтного числа, то переполнение произошло бы без каких-либо предупреждений во время выполнения.

Преобразование с проверкой

Чтобы обнаружить ошибки переполнения, вы можете просто заменить инструкцию conv.i4 в предыдущем примере на conv.ovf.i4. Если значение на стеке слишком велико, чтобы быть представленным указанным типом, то будет брошен объект OverflowException. Дизайн языка C++/CLI, представленный в Visual C++ 2005 ещё не предоставляет языковых возможностей для использования преобразования с проверкой на переполнение. Есть вероятность, что она будет добавлена в виде ключевого слова checked, также как в C#:

checked
{
    int small = 123;
    long big = small;
 
    small = (int) big;
}

Все арифметические операции, преобразования и выражения в блоке checked будут включать проверку на переполнение. Как указано выше, множество инструкций conv. заменяется на conv.ovf.. Другие инстуркции, которые могут привести к переполеннию, также имеют соответствующие аналоги. Например, инструкции add, описанная в 2-ой части, соответствует add.ovf и add.ovf.un (сложение с проверкой на переполнение - со знаком и без соответственно).

Динамическое приведение типов

Статическое приведение может использоваться и для приведения классов, например, выполняя приведение полиморфного типа, если при этом делается предположение, что такое приведение будет всегда совершаться успешно во время выполнения. Полезно избегать накладных расходов, связанных с проверкой типов во время выполнения, но как правило, такое предположение весьма опасно.

Динамическое приведение типов используется, когда нужно отложить определение того, можно ли сделать данное приведение, до времени выполнения. Конечно, это имеет смысл только с полиморфными типами. Как и в случае с статическим приведением типов, компилятор пытается удостовериться, что производимая операция хотя бы правдоподобна. Попытка динамического приведения целого числа к объекту House будет отловлена компилятором как ошибка. Рассмотрим следующий C++-код, исходя из того, что BeachHouse и Townhouse - это подклассы House.

House^ house = gcnew BeachHouse;
 
if (nullptr != dynamic_cast<BeachHouse^>(house))
{
    Console::WriteLine("It's a beach house!");
}
 
if (nullptr != dynamic_cast<Townhouse^>(house))
{
    Console::WriteLine("It's a townhouse!");
}

Этот код прекрасно скомпилируется, и компилятор так и не узнает, пройдут ли приведения типов. В обоих случаях требуется проверка во время выполнения, чтобы определить, является ли дом BeachHouse'ом или Townhouse'ом. dynamic_cast возвращает nullptr, C++-представление для null-указателя или хэндла, если приведение не может быть выполнено. Давайте посмотрим, как это можно сделать в MSIL:

.locals init (class House house)
 
newobj instance void BeachHouse::.ctor()
stloc house
 
ldloc house
isinst BeachHouse
ldnull
 
beq.s _TOWN_HOUSE
 
ldstr "It's a beach house!"
call void [mscorlib]System.Console::WriteLine(string)
 
_TOWN_HOUSE:
 
ldloc house
isinst TownHouse
ldnull
 
beq.s _CONTINUE
 
ldstr "It's a town house!"
call void [mscorlib]System.Console::WriteLine(string)
 
_CONTINUE:

Если вы дочитали до этой главы, то код должен быть вам понятен. Создаётся объект BeachHouse, а ссылка на него сохраняется как локальная переменная. Динамическое приведение выполняется путём помещения ссылки в стек и использования инструкции isinst для проверки типов. Инструкция isinst определяет, является ли ссылка на стек BeachHouse'ом или подклассом BeachHouse. Если да, тогда ссылка приводится к типу, заданному инструкцией isinst и помещается на стек, в противном случае в стек будет помещено значение null. Выражение для if конструируется помещением null-ссылки в стек и использованием инструкцией beq.s, чтобы передать управление цеи, если isinst также поместила null в стек. Такая же проверка типов и условное ветвеление сделаны для типа Townhouse. При этом выполнение продолжается несмотря ни на что.

Если ваше приложение предполагает, что динамическое приведение должно всегда оканчиваться успешно, но вы не хотите прибегать к статическому приведению и ловить исключения, вы можете использовать инструкцию castclass вместо isinst. Она выполняет такую же проверку типов, но вместо помещения в стек null, если проверка не удалась, она бросает объект InvalidCastException. Если вам нужно именно это, то в C++ есть оператор safe_cast или простое (в стиле C) приведение типов в C#.

2002-2013 (c) wasm.ru