Преобразования типов и безопасность типов
Этот документ определяет распространенные проблемы преобразования типов и описывает, как их можно избежать в коде C++.
При написании программы C++ важно убедиться, что она является типобезопасной. Это означает, что каждое значение переменной, аргумента функции и возвращаемого функции сохраняет приемлемый тип данных, и что операции, включающие значения различных типов», «имеют смысл» и не вызывают потери данных, неправильной интерпретации битовых шаблонов или повреждения памяти. Программа, которая никогда явно или неявно преобразовывает значения из одного типа в другой является типобезопасной по определению. Однако иногда требуются преобразования типов, даже небезопасные преобразования. Например, может потребоваться сохранить результат операции с плавающей запятой в переменной типа int или передать значение в unsigned int функцию, которая принимает значение signed int . Оба примера иллюстрируют небезопасные преобразования, так как они могут привести к потере данных или повторной интерпретации значения.
Когда компилятор обнаруживает небезопасное преобразование, он выдает ошибку или предупреждение. Ошибка останавливает компиляцию; Предупреждение позволяет продолжить компиляцию, но указывает на возможную ошибку в коде. Однако даже если программа компилируется без предупреждений, она по-прежнему может содержать код, который приводит к неявным преобразованиям типов, которые создают неправильные результаты. Ошибки типа также могут быть представлены явными преобразованиями или приведениями в коде.
Неявные преобразования типов
Если выражение содержит операнды различных встроенных типов и нет явных приведения, компилятор использует встроенные стандартные преобразования для преобразования одного из операндов таким образом, чтобы типы соответствовали. Компилятор пытается выполнить преобразования в четко определенной последовательности, пока не завершится успешно. Если выбранное преобразование является повышением, компилятор не выдает предупреждения. Если преобразование является сужающим, компилятор выдает предупреждение о возможной потере данных. Указывает, происходит ли фактическая потеря данных, зависит от фактических значений, но рекомендуется рассматривать это предупреждение как ошибку. Если используется определяемый пользователем тип, компилятор пытается использовать преобразования, указанные в определении класса. Если не удается найти допустимое преобразование, компилятор выдает ошибку и не компилирует программу. Дополнительные сведения о правилах, которые управляют стандартными преобразованиями, см. в разделе «Стандартные преобразования». Дополнительные сведения о пользовательских преобразованиях см. в разделе «Определяемые пользователем преобразования» (C++/CLI).
Расширение преобразований (повышение уровня)
При расширении преобразования значение в меньшей переменной назначается большей переменной без потери данных. Так как расширяющие преобразования всегда безопасны, компилятор выполняет их автоматически и не выдает предупреждения. Следующие преобразования расширяются.
| С дт. | Кому |
|---|---|
| Любой signed или unsigned целочисленный тип, кроме long long или __int64 | double |
| bool или char | Любой другой встроенный тип |
| short или wchar_t | int , long , long long |
| int , long | long long |
| float | double |
Сужение преобразований (приведение)
Компилятор выполняет неявно сужающие преобразования, но предупреждает о потенциальной потере данных. Примите эти предупреждения очень серьезно. Если вы уверены, что потеря данных не будет происходить, так как значения в большей переменной всегда будут соответствовать меньшей переменной, добавьте явный приведение, чтобы компилятор больше не выдает предупреждение. Если вы не уверены, что преобразование безопасно, добавьте в код какой-то вид среды выполнения проверка для обработки возможной потери данных, чтобы она не приводила к неправильному результату программы.
Любое преобразование из типа с плавающей запятой в целочисленный тип является сужающим преобразованием, так как дробная часть значения с плавающей запятой не карта и потеряна.
В следующем примере кода показаны некоторые неявные сужающие преобразования и предупреждения о проблемах компилятора.
int i = INT_MAX + 1; //warning C4307:'+':integral constant overflow wchar_t wch = 'A'; //OK char c = wch; // warning C4244:'initializing':conversion from 'wchar_t' // to 'char', possible loss of data unsigned char c2 = 0xfffe; //warning C4305:'initializing':truncation from // 'int' to 'unsigned char' int j = 1.9f; // warning C4244:'initializing':conversion from 'float' to // 'int', possible loss of data int k = 7.7; // warning C4244:'initializing':conversion from 'double' to // 'int', possible loss of data
Подписанные — незаписанные преобразования
Подписанный целочисленный тип и его неподписанный аналог всегда одинаковый, но они отличаются в том, как битовый шаблон интерпретируется для преобразования значений. В следующем примере кода показано, что происходит, когда тот же битовый шаблон интерпретируется как подписанное значение и как неподписаемое значение. Битовый шаблон, хранящийся в обоих num и num2 никогда не изменяется от того, что показано на предыдущем рисунке.
using namespace std; unsigned short num = numeric_limits::max(); // #include short num2 = num; cout
Обратите внимание, что значения переосмыслены в обоих направлениях. Если программа создает нечетные результаты, в которых знак значения по-видимому, повернут из ожидаемого значения, найдите неявные преобразования между подписанными и неподписанными целочисленными типами. В следующем примере результат выражения (0 – 1) неявно преобразуется из int unsigned int того, когда он хранится. num Это приводит к повторному интерпретации битового шаблона.
unsigned int u3 = 0 - 1; cout
Компилятор не предупреждает о неявных преобразованиях между подписанными и неподписанными целочисленными типами. Поэтому рекомендуется избегать полностью подписанных преобразований в unsigned. Если вы не можете избежать их, добавьте среду выполнения проверка, чтобы определить, больше ли преобразованное значение больше или равно нулю и меньше или равно максимальному значению подписанного типа. Значения в этом диапазоне будут передаваться из входа в unsigned или из без знака на подписанный без повторного понимания.
Преобразования указателей
Во многих выражениях массив стилей C неявно преобразуется в указатель на первый элемент в массиве, а преобразования констант могут выполняться автоматически. Хотя это удобно, это также потенциально подвержено ошибкам. Например, следующий плохо разработанный пример кода кажется нечувствичным, но он компилирует и создает результат p. Во-первых, константный литерал строки help преобразуется char* в тот, который указывает на первый элемент массива. Затем указатель увеличивается тремя элементами, чтобы он указывал на последний элемент p.
char* s = "Help" + 3;
Явные преобразования (приведение)
С помощью операции приведения можно указать компилятору преобразовать значение одного типа в другой тип. Компилятор вызовет ошибку в некоторых случаях, если два типа полностью не связаны, но в других случаях это не приведет к возникновению ошибки, даже если операция не является типобезопасной. Используйте приведение с разреженным способом, так как любое преобразование из одного типа в другой является потенциальным источником ошибки программы. Однако литья иногда требуются, и не все литые одинаково опасны. Одно эффективное использование приведения заключается в том, что код выполняет сужение преобразования, и вы знаете, что преобразование не приводит к неправильному результату программы. В действительности это сообщает компилятору, что вы знаете, что вы делаете, и прекратить беспокоить вас с предупреждениями об этом. Другим способом является приведение от класса указателя на производный от указателя к базовому классу. Другое использование заключается в том, чтобы отбросить константу переменной, чтобы передать ее в функцию, требующую аргумента, отличного от константа. Большинство этих операций приведения включают некоторый риск.
В программировании в стиле C один и тот же оператор приведения в стиле C используется для всех видов приведения.
(int) x; // old-style cast, old-style syntax int(x); // old-style cast, functional syntax
Оператор приведения в стиле C идентичен оператору вызова () и поэтому неудобен в коде и легко упускать из виду. Оба плохо, потому что они трудно распознать на первый взгляд или найти, и они достаточно разрозненные, чтобы вызвать любое сочетание static , const и reinterpret_cast . Определение того, что на самом деле делает старый стиль, может быть трудным и подверженным ошибкам. По всем этим причинам, если требуется приведение, рекомендуется использовать один из следующих операторов приведения C++, которые в некоторых случаях значительно более типобезопасны, и которые выражают гораздо более явно намерение программирования:
-
static_cast , для приведения, которые проверка только во время компиляции. static_cast возвращает ошибку, если компилятор обнаруживает, что вы пытаетесь приведения между типами, которые полностью несовместимы. Вы также можете использовать его для приведения между указателем на базу и указатель на производный, но компилятор не всегда может определить, будут ли такие преобразования безопасными во время выполнения.
double d = 1.58947; int i = d; // warning C4244 possible loss of data int j = static_cast(d); // No warning. string s = static_cast(d); // Error C2440:cannot convert from // double to std:string // No error but not necessarily safe. Base* b = new Base(); Derived* d2 = static_cast(b);
Base* b = new Base(); // Run-time check to determine whether b is actually a Derived* Derived* d3 = dynamic_cast(b); // If b was originally a Derived*, then d3 is a valid pointer. if(d3) < // Safe to call Derived method. cout DoSomethingMore() else < // Run-time check failed. cout << "d3 is null" //Output: d3 is null;
void Func(double& d) < . >void ConstCast() < const double pi = 3.14; Func(const_cast(pi)); //No error. >
Примечание. Этот оператор приведения не используется так часто, как и другие, и он не гарантируется переносимым к другим компиляторам.
В следующем примере показано, как reinterpret_cast отличается от static_cast .
const char* str = "hello"; int i = static_cast(str);//error C2440: 'static_cast' : cannot // convert from 'const char *' to 'int' int j = (int)str; // C-style cast. Did the programmer really intend // to do this? int k = reinterpret_cast(str);// Programming intent is clear. // However, it is not 64-bit safe.
Перевод единиц измерения - онлайн-конвертер величин
Перевод основных величин используемых в физике, термодиномике и для инженерных рассчетов. Конвертер величин работает с основными разделами: вес масса, время, длина и др. Так и конвертирует специфические величины, используемые в теплоэнергетике: удельная теплоемкость, теплопроводность и др.
Конвертер величин позволяет переводить значения в "СИ" (метрическая) и альтернативных системах измерения.
Вес, масса
Вес — сила, с которой взаимодействуют тело и опора этого тела (или подвес, к которому прикреплено тело).
Масса — мера инертных свойств тела. Если произвольная сила в инерциальной системе отсчёта одинаково ускоряет разные тела, этим телам приписывают одинаковую инертную массу.
Расход топлива
Расход топлива
Расход топлива — расход единицы топлива на единицу расстояния или в единицу времени.
Является одной из важных характеристик двигателя.
Время
Время в классической физике — непрерывная величина, априорная характеристика мира. В качестве основы измерения берётся некая последовательность периодичных событий (то есть происходящих через равные промежутки времени).
В релятивистской физике время — часть единого пространства-времени, и, значит, может меняться при его преобразованиях.
Скорость
Скорость — векторная величина, характеризующая быстроту перемещения и направление движения материальной точки в пространстве относительно выбранной системы отсчёта.
В науке используется также скорость в широком смысле, то есть как скорость изменения какой-либо величины.
Давление
Давление — величина, характеризующая состояние сплошной среды и численно равная силе, действующей на единицу площади поверхности перпендикулярно этой поверхности.
В большинстве приборов измеряемое давление преобразуется в деформацию упругих элементов, поэтому они называются деформационными.
Скорость потока (мас.)
Скорость потока (массовая)
Скорость потока (массовая) — масса вещества, проходящего через поперечное сечение потока в единицу времени.
Длина, расстояние
Длина, расстояние
Длина — размер предмета в продольном направлении, то есть расстояние между его двумя наиболее удалёнными точками. Это одна из фундаментальных единиц измерения, на основе которых образуются другие единицы.
Расстояние — степень удалённости объектов друг от друга.
Скорость потока (об.)
Скорость потока (объёмная)
Скорость потока (объёмная) — величина, характеризующая объёмное количество вещества, проходящее через поперечное сечение потока в единицу времени.
Доли и проценты
Доли и проценты
Доля — часть чего-нибудь; а также старорусская единица измерения массы.
Процент (%) — одна сотая доля. Используется для обозначения доли чего-либо по отношению к целому.
Температура
Температура
Температура — величина, примерно характеризующая приходящуюся на одну степень свободы среднюю кинетическую энергию частиц системы, находящейся в состоянии термодинамического равновесия.
В системе СИ температура измеряется в кельвинах. Но на практике часто применяют градусы Цельсия из-за привязки к важным характеристикам воды.
Момент силы (вращ.)
Момент силы (вращательный)
Момент силы — величина, характеризующая вращательное действие силы на твёрдое тело.
Угловая мера
Угловая мера
Угол — неограниченная геометрическая фигура, образованная двумя лучами (сторонами угла), выходящими из одной точки (вершины угла).
Углом называют также угловую меру.
Мощность
Мощность — величина, равная отношению работы, выполняемой за некоторый промежуток времени, к этому промежутку времени.
Так как работа является мерой изменения энергии, мощность можно определить также как скорость изменения энергии системы.
Ускорение
Ускорение — производная скорости по времени — векторная величина, показывающая, насколько изменяется вектор скорости точки при её движении за единицу времени.
Объём, ёмкость
Объём, ёмкость
Объём — это мера, характеризующая вместимость области пространства, которую оно занимает.
Ёмкостью чаще обозначают сосуды, тогда как объёмом чаще обозначают вместимое (газ, жидкость, сыпучие тела) в сосуд. Принципиальных различий нет.
Энергия, работа
Энергия, работа
Энергия — величина, являющаяся единой мерой различных форм движения материи и мерой перехода движения материи из одних форм в другие.
Работа — количество энергии, переданной или полученной системой.
Теплопроводность
Теплопроводность
Удельная теплоемкость
Удельная теплоемкость
Площадь
Площадь — величина, в некотором смысле соответствующая размеру поверхности.
В простейшем случае, когда фигуру можно разбить на конечное множество единичных квадратов, площадь равна числу квадратов.
Приставки СИ
Приставки СИ
| Кратность | Приставка | Обозначение |
| 10 -9 | нано | н |
| 10 -6 | микро | мк |
| 10 -3 | милли | м |
| 10 -2 | санти | с |
| 10 -1 | деци | д |
| 10 1 | дека | да |
| 10 2 | гекто | г |
| 10 3 | кило | к |
| 10 6 | мега | М |
| 10 9 | гига | Г |
Как конвертировать в c

О проекте: Проект по конверсии кода, написанного с применением новых конструкций, внесенных в C++ 11, в код, совместимый с C++ 98.
Зачем это нужно?
При кросс-платформенной разработке на языке C++ часто возникает головная боль с попыткой получить единую кодовую базу. Традиционное решение — туча макросов, проверяющих версию C++ и перекрывающих реализацию фрагментов кода для разных по возможностям компиляторов, очень легко замусоривает код. Поэтому обладая чистым, отлаженным кодом, написанным с применением весьма полезных функций C++ 11, таких как range-based for, enum-классы, спецификаторов delete, default, override и т.д., и задачей заставить его собраться с применением например GCC 4.3.4 весьма грустно переписывать его на C++ 98. Конечно, может возникнуть вопрос, а почему бы не взять просто более новый GCC, например 5.4? К сожалению, это возможно не всегда. Например, если имеешь дело с производителем POS-терминала и его Platform SDK, содержащим конкретную версию GCC и не намеренного ничего менять. Конвертор явился дешевой альтернативой варианту переписать все места, где в коде применялся C++ 11, позволившей сохранить единую кодовую базу для всех платформ, использующую возможности, включенные C++ 11. Интегрированный через интерфейс командной строки в процедуру автоматической сборки он позволил сохранить большой объем человеко-часов.
Что умеет?
- Конвертировать параметрические циклы в простые циклы for с выведением авто-типов.
- Внедрять в код эмуляцию перечислений со строгой типизацией (enum-классов).
- Эмулировать спецификаторы delete, default, override, final.
- Вычитывать информацию из заголовочных файлов для выведения типов в процессе конверсии циклов.
- Устранение неподдерживаемых #pragma (например #pragma region, #pragma endredion), приводящих к выводу замусоривающих логи сборки предупреждений компилятора.
Как правильнее всего использовать?
Самым правильным видится интеграция вызова этого конвертора в процесс автоматической сборки. Все может выглядеть, например так:
- автоматический сборщик выкачивает из репозитория исходный код для сборки
- если платформа такая, что компилятор старый и что-то не поддерживает, то запускается конвертор, изменяющий код в рабочей копии
- запускается компиляция
Как запустить?
Конвертер запускается с командной строки. Предусмотрено два варианта запуска:
Запуск в режиме конверсии одного файла:
ccc.exe
где — файл для преобразования, — имя файла в который следует записать результат.
Запуск в режиме конверсии всех файлов в папке:
ccc.exe
где — папка для преобразования (конвертер ищет в ней и во всех ее подкаталогах файлы *.h и *.cpp, конвертирует код и заменяет содержимое).
Можно ли использовать в закрытых проектах?
Программа раздается бесплатно, использовать можно.
Роль в проекте: автор концепции и реализации.
Тип проекта: научно-исследовательский, бесплатно-распространяемый.
Состояние: активен, выпущена версия, эксплуатируется.
Участие в проекте: c 2016 года.
Операционные системы: Windows, Linux.
Правообладатель и распространитель: Куриленко И.Е.
Опыт конвертирования кода C# в код Rust
Код на языке C# нужно перевести в код на Rust. Точнее, требуется такая процедура перевода (разработка продолжается на C#), чтобы в любой момент можно было получить работающий код на Rust. Эту задачу я решал для языков Java, Python, JavaScript и PHP, написав конвертер из C# в эти языки. Концепция такого конвертирования была изложена в статье UniSharping пару лет назад. Я разрабатывал этот конвертер, чтобы переводить код своего проекта SDK Pullenti (лингвистический анализ текста). И подумалось мне: а не замахнуться ли на Rust? Да, слышал разные отзывы, что язык необычный и пр., но попытка же не пытка… Тем более, что у одного из заказчиков группа программистов увлечённо пишет на нём.
Сразу скажу, что в полном объёме, как для других языков, этого сделать не получилось — не хватило сил. Может, ещё вернусь к этой задаче. Полтора месяца было потрачено на борьбу с собой и языком, удалось довести конвертер до состояния, что морфологический блок начал переводиться и даже компилироваться (значит, и работать) на Rust-е. Разумеется, за это время модуль морфологии можно было написать с нуля, но за ним маячили ещё около 500 классов C#, создаваемые и отлаживаемые почти 10 лет, а их переписать не так просто. В этой статье хочу поделиться своими впечатлениями от языка Rust, а также описать приёмы, которые я использовал для конвертирования.
Впечатление от языка Rust
Говорят, что мастер не ищет лёгкий путей. Это в полной мере относится к Rust, так как многое простое и привычное на других языках становится сложным, при этом сложное не становится простым. Вы как бы попадаете в другой мир с абсурдной на первый взгляд логикой, которая становится далеко не сразу понятна после освоения базовых концепций. Неважно, на чём вы писали до сих пор: Си++, Java, Python и пр., но когда оказывается, что после добавления в список объект нельзя использовать: it = new . (); list.add(it); it.val = . , а вот так можно: it = new . (); it.val = . ; list.add(it); , то это обескураживает. Или чтобы реализовать перекрёстные ссылки между объектами класса Foo, нужно использовать конструкцию Option>> , а для доступа к полю val этого класса вызывать foo.unwrap().borrow().val .
Выскажу своё личное мнение: мне представляется, что прогресс в области программирования идёт в направлении оптимизации труда программиста, чтобы меньшими усилиями достигать большего эффекта. Случай Rust уникален тем, что не вписывается в эту тенденцию. Удивительным для меня фактом является рост его популярности (сейчас Rust вошёл в 20-ку). Но почему? Вот вопрос.
По производительности на моей задаче Rust не произвёл впечатления — выигрыш по сравнению с C# получился всего в 2 раза. Подчеркну, что это на моей частной задаче морфологического анализа, которую удалось перевести в эквивалентный код (наверняка человек написал бы оптимальнее). В разных статьях сравнения производительности приводятся разные данные, и в целом создаётся впечатление, что Rust и C/C++ близки по скорости. Но существенно разнятся по сложности написания кода. Утверждается, что Rust сильно уменьшает вероятность утечек памяти по сравнению с С/C++, доступа за границу массива и прочее, но какой ценой.
Единственным разумным для меня объяснением роста популярности Rust является то, что Си "поднадоел" за почти 50 лет, и молодое поколение программистов желает чего-то нового, не обязательно лучшего. Как молодёжь 80-х добровольно ехала из обжитых городов строить БАМ (такая железная дорога на Дальнем Востоке), стойко перенося трудности и лишения, застревая потом в таёжных городках и посёлках. Подобно и здесь. Си-шника я ещё могу понять, так как он получает trait-ы (типа interface в Java и C#), на которых можно худо-бедно реализовывать ООП, и ещё некоторые полезные штучки. Но вот что здесь искать программистам других языков, кроме романтики новой экосистемы, в создании которой можно поучаствовать? Мне показалось, что при переходе на Rust большее теряешь, чем находишь.
Работа с кучей
Базовое отличие Rust от других языков — в парадигме работы с кучей (heap). В древних языках всё просто — выделение и освобождение памяти в куче происходит явно операторами типа new/delete. Это С\С++, Паскаль, Фортран и др. Возникает утечка памяти, если delete не вызвать. Потом решили упростить программистам жизнь, избавив от необходимости явно освобождать память. Этот процесс перенесли на уровень так называемых сборщиков мусора, которые сами заботятся об освобождении в нужный момент, программист же только выделяет через new. Это имеет место во всех известных мне современных языках: Java, C#, Python, JavaScritp, PHP и пр.
В Rust освобождение памяти происходит тоже автоматически, но сразу при окончании жизни объекта, например, когда он выходит из области видимости. Скажем, если внутри блока создать объект < . let x = Foo <. это конструктор>; . > , то память автоматически освободится при выходе управления из этого блока. И вот весь геморрой — из-за этой парадигмы. Приходится вводить понятие владельца объекта, ссылки на владельца, изменяемой (mut) ссылки на владельца, время жизни и другие понятия, порождающие ограничения языка. Скажем, изменяемая ссылка может быть только одна, и вот аналог C# чтения в буфер buf из потока stream.Read(buf, 0, buf.Length) не будет компилироваться, поскольку в первом аргументе buf перезаписывается и должен быть mut-ссылкой, поэтому в третьем аргументе уже никак этот buf использовать нельзя. А вот так можно: int len = buf.Length; stream.Read(buf, 0, len); .
Далее опишу решения, которые я использовал в конвертере для перевода кода C# в Rust. Напомню, что именно это и было исходной задачей — конвертирование существующего кода.
Ограничения кода C#
Несколько лет назад я озаботился тем, чтобы перевести свой SDK на C# в язык Java. Опробованные конвертеры не подошли, так как выдавали на выходе грязный код, который ещё править и править. Да, они заточены на задачу разовой миграции, а мне хотелось иного — продолжать разработку на C# и получать на выходе сразу исполняемый код, работающий эквивалентно. Пришлось писать самому. Этому была посвящена статья UniSharping. Если кратко, то в общем случае невозможно решить эту задачу. Однако, если придерживаться некоторых ограничений при написании кода C#, то невозможное становится возможным. Например, в Java отсутствует аналог оператора yield, так давайте избавимся от него в исходном коде C# — невелика потеря!
С каждым новым поддержанным языком исходный код на C# слегка корректировался. Для Java пришлось отказаться от плагинной техники динамической загрузки DLL, поскольку в Java и других языках понятие сборки просто отсутствует. Для Python пришлось убрать одинаковые названия методов в классе, поскольку сигнатура Питона включает только имя, а типы аргументов у него не указываются. У JavaScript обнаружилось отсутствие типа long (есть byte, short, int, float, double, а вот на long-е разработчики сэкономили), пришлось мне в коде SDK C# заменить все long на int, благо их оказалось немного. В PHP ждала засада в виде представления string как последовательность байт кодировки utf-8 с невозможностью быстрого доступа к i-му символу строки без перебора. Тут я уже ничего у себя переделывать не стал, а использовал их штатные функции mb_, из-за чего производительность получилась чудовищно низкой. В Rust со строками такая же ситуация, но тут я поступил по-другому.
Также плодотворной оказалась идея использования директив препроцессора с коде C#, когда нужно в зависимости от языка слегка что-нибудь подправить: #if JAVA || PYTHON… #else… #endif — конвертер понимает такие конструкции.
Огромная сложность автоматического перевода — это библиотечные классы и методы. Хорошо, когда есть полный аналог в другом языке, а если нет? Тогда приходится реализовывать на конечном языке эту функциональность и добавлять её в файлы генерируемого кода. Для Rust это пришлось делать более, чем для других языков.
Стандартные конструкции
Выражения C#, операторы ветвления, циклы, функции — для всего этого Rust имеет эквиваленты, тут всё просто. Только циклы for(. ; . ; . ) в большинстве случаев приходится разворачивать в while — к этому я был хорошо подготовлен Питоном. С простыми типами byte, int, float и пр. тоже просто, на то они и простые. А вот с непростыми сложнее.
Для непростого типа T из C# в Rust следует использовать три разных типа: T (для владельца объекта), &T (неизменяемая ссылка) и &mut T (изменяемая ссылка). Можно считать, что в C# произошло как бы слияние в один тип трёх разных типов — с C# он один, а в Rust это три типа, причём в нужных местах этот тип должен быть одним из этих трёх.
var obj = new T(); // создали объект класса T FuncNotModif(obj); // передали аргументом в функцию FuncModif(obj); // здесь объект модифицируется list.Add(obj); // добавили в список List var obj2 = obj; // другая ссылка на тот же объект var obj3 = obj; // другая ссылка на тот же объект
Вот как этот фрагмент можно представить в Rust:
let obj = T < >; // создали объект класса T (о классах чуть ниже) func_not_modif(&obj); // передаём неизменяемую ссылку, иначе дальше obj нельзя будет использовать func_modif(&mut obj); // а здесь модифицирующая ссылка list.push(&obj); // можно добавлять только в вектор ссылок Vec, иначе потом obj недоступен let obj2 : &T = &obj; // другая ссылка на объект let obj3 : T = obj; // а здесь владение переходит к obj3, и obj с obj2 больше нельзя использовать
Итак, принципиальным моментом в Rust является владение экземпляром, которое переходит при присваивании значения: в операторе =, return и передаче аргументом без указания &. После этого предыдущая переменная и все ссылки становятся недействительными, эстафета как бы передаётся другой переменной.
А как в коде C# понять в каждом случае, какую из этих трёх разновидностей использовать: T, &T или &mut T? Хороший вопрос, и я его решил так:
По умолчанию аргументы методов являются &T или &mut T в зависимости от того, модифицируются ли они или нет в самом методе (это автоматом определяет конвертер), у методов, реализующих property < get; set; >возвращаемые значения &T, всё остальное — T. В коде C# сразу за типом можно указать через комментарий /*&*/ или /*&mut*/ нужный вариант. Например, для списка ссылок подсказка конвертеру будет List , а если и сам список является ссылкой, то List/*&*/ .
Здесь некоторые из дочитавших до этого места разочарованно произнесут: мы то думали, что конвертер сам понимает, а ему приходится указывать, ещё и коверкать исходный код нелепыми вставками. Согласен, после разоблачения фокус воспринимается уже не так. Но это — решение, и лучше мне не удалось найти. К тому же оказалось, что в моём случае морфологического блока таких вставок получилось не так уж много.
Строки
Строки в Rust представляются последовательностью байт кодировки utf-8 (как и в PHP). Думаю, здесь 2 причины. В C#, Java и др. строки есть последовательность символов char размером 16 бит (в Си 8 бит, в Си++ 8 и 16), что есть ограничение с точки зрения разработчиков Rust. Сейчас Unicode уже 32-битный, а вдруг в будущем он вообще станет 64-битным? А они будут к этому готовы. Другая причина субъективная — основатель англоязычный, и его слабо волнуют проблемы за пределами 7-битной ASCII.
А в моей переводимой библиотеке идёт интенсивная работа со строками и доступом к её элементам str[i]. Как быть?
Решение — реализовать класс (struct в терминологии Rust), содержащий как вектор символов, так и сам string.
#[derive(Clone)] pub struct NString < pub chars : Vec, pub string : String, _is_null : bool > impl NString < pub fn from_string(s : &String) ->NString < NString < chars : s.chars().collect(), string : s.clone(), _is_null : false >> pub fn from_str(s : &str) -> NString < NString < chars : s.chars().collect(), string : s.to_string(), _is_null : false >> pub fn from_chars(s : &Vec) -> NString < NString < chars : s.clone(), string : s.into_iter().collect(), _is_null : false >> . >
Когда нужно работать как массивом элементов, то используется поле chars, иначе — штатный String. Для этой структуры реализованы различные стандартные методы C# работы со строками, если аналогов не находилось в Rust. Например, вот метод Substring(int start, int len) для получения подстроки:
pub fn substring(&self, pos : i32, len : i32) -> NString < let length : i32 = if len else < len >; let sub = self.chars[pos as usize .. (pos + length) as usize].to_vec(); NString::from_chars(&sub) >
А строки-лексемы конвертер представляет так, ссылаясь в коде &STR_HELLO для ссылки или STR_HELLO.clone() для владения:
static STR_HELLO : Lazy = Lazy::new(|| < NString::from_str("Hello!") >); use once_cell::sync::Lazy;
Коллекции
Разумеется, в Rust есть множество типов для работы с коллекциями, но они по ряду причин не подошли. Если сразу писать на Расте, то может и ничего, но транслировать из кода C# оказалось затруднительно, поэтому пришлось как и для строк написать обёртки над Vec и HashMap и использовать их. Причём получилось 3 обёртки для каждого типа в зависимости от типа элементов: для простых типов, для ссылок &T и для владений T. Массивы array[] я транслировал в Rust так же, как и List.
Object
В Rust нет привычных всем null и базового класса object, практически отсутствует и приведение типов. То, что в C# любой тип можно "упаковать" в object и затем "распаковать" его — для Rust это за пределами возможного. Я не придумал лучшего решения, чем следующее.
Если в существующем коде используется object, то как правило в реальности в конкретном месте фигурирует ограниченный набор типов значений для этого object. Поэтому можно создать служебный класс, содержащий в отдельных полях значения этих типов, и использовать его, указав конвертеру в виде подсказки /*=имя*/ после object.
object/*=ObjValue*/ obj = "Hello"; Console.WriteLine(obj); obj = 10; if (obj is int) < int ii = (int)obj; Console.WriteLine(ii); >obj = cnt.First; // объект класса Item if(obj is Item) Console.WriteLine((obj as Item).Str); #if RUST // компилятор C# игнорирует этот фрагмент //RUST object_class class ObjValue < public string Str; public int Int; public Item/*&*/ Item; >#endif
Здесь мы знаем, что object принимает значения только int, string и Item, причём это именно ссылка, а не владение Item — им владеют в другом месте.
Создаём класс ObjValue, который игнорируется компилятором C#, но который воспринимается конвертером.
let mut obj : ObjValue = ObjValue::from_str_(STR_HELLO.clone()); println!("<>", &obj.to_nstring()); obj = ObjValue::from_int(10); if obj.is_class("i32") < let mut ii : i32 = obj.int; println!("<>", &NString::from_string(&ii.to_string())); > obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap()))); if obj.is_class("Item") < println!("<>", obj.item.as_ref().unwrap().borrow().get_str()); > pub struct ObjValue < pub str_ : NString, pub int : i32, pub item : Option>>, _typ : &'static str > impl ObjValue < pub fn from_str_(val : NString) ->ObjValue < ObjValue < str_ : val, int : 0, item : None, _typ : "NString" >> pub fn from_int(val : i32) -> ObjValue < ObjValue < str_ : NString::null(), int : val, item : None, _typ : "i32" >> pub fn from_item(val : Option>>) -> ObjValue < ObjValue < str_ : NString::null(), int : 0, item : val, _typ : "Item" >> pub fn null() -> ObjValue < ObjValue < str_ : NString::null(), int : 0, item : None, _typ : "" >> pub fn is_null(&self) -> bool < self._typ.len() == 0 >pub fn is_class(&self, typ : &str) -> bool < self._typ == typ >pub fn to_nstring(&self) -> NString < if self._typ == "NString" < return self.str_.clone(); >if self._typ == "i32" < return NString::from_string(&self.int.to_string()); >if self._typ == "Item" < return NString::from_str("Option>>"); > NString::null() > >
Да, громоздко. Но ведь это же конвертер генерирует! Главное, что работает.
Обратим внимание: для шарпового obj = cnt.First на Rust получается obj = ObjValue::from_item(Some(Rc::clone(cnt.borrow().get_first().as_ref().unwrap()))) . Что говорите, это жесть? Нет, это Раст! Разумеется, человек напишет короче, здесь лишь попытка дать универсальное решение доступа к члену класса.
Классы и наследование
Аналогом класса C# в Rust выступает struct, аналогом интерфейса — trait. Трейты множественно наследуются, структуры — вообще не наследуются. Структура может реализовывать любое число трейтов. То есть как бы в C# убрали наследование от другого класса, но оставили интерфейсы: из ООП полноценной осталась только инкапсуляция. Ну и на том спасибо.
Есть какой-то хитрый способ через виртуальные таблицы как-то моделировать наследование и полиморфизм, разобраться в котором мне попросту не хватило мозгов.
Я придумал следующий способ. Для класса A, от которого идёт наследование, всегда генерируется один trait, в который переносятся все методы, а для публичных полей генерируется функции get и set (как для property). В struct наследного класса B добавляется экземпляр класса A ( struct B < base : A, другие поля >), и этот B также реализует этот trait от A. Причём если нужен доступ к полю или методу A, то используется self.base.x.
Приведу пример из кода реализации двунаправленного списка.
//RUST RefCell class Item < public Item(int val) < Val = val; >public int Val < get; set; >public string Str; public Item/*&*/ Prev < get; set; >public Item/*&*/ Next < get; set; >public virtual void Inc() < Val += 1; >> //RUST RefCell class ItemChild : Item < public ItemChild(int val) : base(val) < >public override void Inc() < Val *= 2; >>
Вот работа конвертера (некоторые фрагменты будут удалены для краткости). Это сгенерированный базовый trait.
pub trait IItem < fn get_val(&self) ->i32; fn set_val(&mut self, value : i32) -> i32; fn get_str(&self) -> &NString; fn set_str(&mut self, value : NString) -> &NString; fn get_prev(&self) -> &Option>>; fn set_prev(&mut self, value : Option>>) -> &Option>>; fn get_next(&self) -> &Option>>; fn set_next(&mut self, value : Option>>) -> &Option>>; fn inc(&mut self); fn get_base_class(&self) -> &dyn IItem; fn is_class(&self, name : &str) -> bool; fn as_item(&self) -> &dyn IItem; fn as_mut_item(&mut self) -> &mut dyn IItem; >
Это реализация базового класса.
pub struct Item < pub _val : i32, pub m_str : NString, pub _prev : Option>>, pub _next : Option>>, > impl IItem for Item < fn get_val(&self) ->i32 < return self._val; >fn set_val(&mut self, mut value : i32) -> i32 < self._val = value; return self._val; >fn get_prev(&self) -> &Option>> < return &self._prev; >fn set_prev(&mut self, mut value : Option>>) -> &Option>> < self._prev = utils::clone_opt_ref(&value); return &self._prev; >. fn inc(&mut self) < self.set_val(self.get_val() + 1); >fn as_item(&self) -> &dyn IItem < self >fn as_mut_item(&mut self) -> &mut dyn IItem < self >fn get_base_class(&self) -> &dyn IItem < self >fn is_class(&self, name : &str) -> bool < name == "Item" >> impl Item < pub fn new(mut __val : i32) ->Item < let mut self_result = Item < _val : 0, _prev : None, _next : None, m_str : NString::null() >; self_result.set_val(__val); self_result > >
А вот наследный класс:
pub struct ItemChild < pub base : Item, // экземпляр базового класса >impl IItem for ItemChild < fn get_val(&self) ->i32 < self.base.get_val() // вот здесь работа через экземпляр base >fn set_val(&mut self, value : i32) -> i32 < self.base.set_val(value) >// а это - переопределённая как бы виртуальная функция fn inc(&mut self) < self.base.set_val(self.get_val() * 2); >. > impl ItemChild < pub fn new(mut __val : i32) ->ItemChild < ItemChild < base : Item::new(__val) >; > >
Обращение к объектам Item и ItemChild везде идёт через ITrait, так что вызов inc() будет именно той функции, объект которой находится за этим trait — а это и есть полиморфизм! Что и требовалось доказать.
Ссылки
У каждой ссылки &T должно быть явно или неявно задано так называемое время жизни (lifetime), чтобы не оказалось так, чтобы ссылка жила дольше самого объекта, на который ссылается. Если использовать ссылки в полях структур, то для самой структуры тоже нужно указывать время жизни: struct A < ref : &'a Item, . >. При этом получается как бы новый тип, при использовании которого нужно учитывать это 'a. Это должно ещё коррелировать со временем жизни самого объекта. Короче, когда таких ссылок становится много, наступает lifetime-hell, как я его назвал. Да, Rust тоже внёс свой вклад в коллекцию этих хеллов!
Решение подсказали опытные товарищи: использовать конструкцию Option>> . Выше в примерах кода она уже встречалась. И применять правило — если на объекты класса ссылаются в нескольких местах, то использовать только эту конструкцию для ссылок. Хотя и это не гарантирует корректного освобождения памяти, так как при циклической зависимости обратная ссылка должна быть Option>> . "Вот тут, Василий Иванович, я и сломался! — Дурак ты, Петька. "
Послесловие
Не стану описывать все нюансы конвертации, но признаюсь, что задачу удалось решить лишь частично. Из всего SDK получилось перевести только блоки морфологии и онтологии, что составляет примерно 10% от планируемого. Остальное оказалось непосильным, да и время закончилось, пора возвращаться к своим лингвистическим задачам. Первый "подход к снаряду" показал, что задача в принципе решается, но требует относительно большой корректировки исходного кода C# — это подсказки конвертеру и переписывание недопустимых для Rust фрагментов. А выигрыш производительности получился у меня всего в два раза.
Как язык, Rust не прост, совсем не прост… Советских людей учили, что в жизни всегда есть место подвигу. Программирование на Rust — это, конечно, не подвиг, но что-то героическое в этом есть! Я это оценил на собственной шкуре и приветствую героев!
- Ненормальное программирование
- C#
- Rust