Великий и ужасный cast
Приведение типов — один из самых неоднозначных инструментов в статически типизированных языках программирования. В самом деле, если для всего есть типы, то зачем конвертировать между ними? Ведь это нарушает безопасный контракт, который, собственно, и достигается использованием системы типов. Но не все так просто.
Во-первых, без приведения типов невозможна низкоуровневая работа с железом (точнее, с драйверами). Если вы хотите передать данные по сети, у вас нет иного выбора, кроме как преобразовать данные в цепочку байт — ничего другого сеть не понимает. Если вы хотите загрузить данные в видеопамять для использования при рендеринге или иных GPU-вычислениях, то это также должен быть простой байтовый буфер — видеодрайвер тоже ничего не знает о системе типов вашего языка.
Во-вторых, тип при вычислении может не совпадать с типом при хранении. Например, пиксели избражений обычно хранятся в памяти как трех- или четырехканальные 8-битные целые. Но это слишком низкая точность информации для вычислений, поэтому функции обработки изображений чаще всего пишутся на float’ах: целочисленное представление пикселя конвертируется в вещественное на входе функции и наоборот — на выходе (именно так, кстати, работает GPU). Аналогично обрабатывается и звук.
В-третьих, каст нужен для преодоления двусмысленности (ambiguity), присущей семантике языков. Например, если в бинарном операторе даны операнды двух разных типов, то компилятор может не пытаться угадывать, какую семантику имел в виду программист, и оставить это на его совести, требуя явного приведения одного типа к другому.
В D, как и в C++, есть несколько семантик приведения типов. Отличие в том, что все они записываются одинаково — ключевым словом cast. Поэтому очень важно понимать семантику приведения, релизуемую компилятором в различных ситуациях.
Статический каст
Аналог static_cast в C++. Возможен, если типы совместимы между собой, и во многих случаях необходим, чтобы операция имела однозначную для компилятора семантику. Например, в таком случае:
int a = 10;
float b = 0.5f;
int c = a * b;
Этот код не скомпилируется без уточнения, в какой тип вы хотите сконвертировать результат вещественного умножения, поскольку D запрещает неявное сужение типов (implicit narrowing). Правильным вариантом будет:
int a = 10;
float b = 0.5f;
int c = cast(int)(a * b);
Динамический каст
Также известен как даункаст и нисходящее приведение по иерархии. Компилятор автоматически приводит класс-наследник к базовому классу (апкаст), но обратная операция требует явного указания программистом. Скажем, у вас есть экземпляр класса Component, а вам нужно вызвать метод наследующего от него PhysicsComponent:
void doSomething(Component comp)
{
PhysicsComponent ph = cast(PhysicsComponent)comp;
}
Это является прямым аналогом dynamic_cast в C++. В D даункаст для классов делает проверку в рантайме — если экземпляр имеет другой тип, он просто вернет null. Можно даже приводить базовый Object в любой ваш класс или интерфейс. Отличие от C++ в том, что dynamic_cast работает только для классов с виртуальными функциями, а в D cast работает для любых классов (так как все классы наследуют от Object и имеют RTTI) и, что самое важное, для интерфейсов. Главное, точно знать, что это экземпляр именно данного класса (или реализация данного интерфейса), либо делать проверку на null, если нет такой уверенности.
Реинтерпретация
Аналог reinterpret_cast в C++. Реинтерпретация — это приведение указателя на один тип к указателю на другой. Благодаря этому можно, к примеру, любые данные рассматривать как байтовый буфер.
int[] arr = [1, 2, 3];
ubyte* pBuffer = cast(ubyte*)arr.ptr;
Преимущество D перед C и C++ в поддержке срезов на уровне синтаксиса языка, поэтому в случае с массивами можно вообще не использовать указатели и напрямую реинтерпретировать срез одного типа в другой. Длина среза автоматически пересчитается нужным образом:
ubyte[] bytesArr = cast(ubyte[])arr;
Вам остается передать bytesArr.ptr и bytesArr.length куда вам нужно.
Важно знать, что это небезопасная операция, так как компилятор не делает никаких проверок, и правильное поведение программы при реинтерпретации полностью зависит от вас.
Изменение квалификаторов
Аналог const_cast в C++. Снятие/установка квалификаторов типа (const, immutable, inout, shared):
string constStr = "hello"; // string - это immutable(char)[]
char[] mutableStr = cast(char[])constStr;
На мой взгляд, самый худший и опасный каст из всех. По возможности лучше его не использовать. Мало того, что снятие квалификаторов неизменяемости противоречит самой идее неизменяемости, так это еще и может привести к сегфолту, если неизменяемые данные находятся в сегменте .rodata. Единственный случай, когда это оправдано — для совместимости с C, при передаче указателей в библиотечные функции.