Предыдущую статью восприняли лучше, чем я ожидал, так что решился на продолжение эксперимента. Это своеобразный ответ на перевод статьи Programming in D for C Programmers за авторством Дмитрия aka vintage. Как мне кажется, в области применения C Rust более уместен, чем замена Go, как предполагалось в прошлой статье. Тем интереснее будет сравнить. Опять таки, код на С приводить не буду, тем более что аналог на D всё равно смотрится лаконичнее.
Получаем размер типа в байтах
Напомню, что в С (и в С++) для этой цели существует специальный оператор sizeof
, который может применяться как к типам, так и к переменным. В D же размер доступен через свойство (которое тоже можно применять и к переменным):
int.sizeof
(char*).sizeof
double.sizeof
Foo.sizeof
В Rust используется функция, которая обращается к внутренностям компилятора (соответствующему intrinsic):
size_of::<i32>()
size_of::<*const i8>()
size_of::<f64>()
size_of::<Foo>()
При этом, по причине отсутствия перегрузки функций, для получения размера переменных используется другая функция — size_of_val
. Возможно, такое разделение несколько менее удобно, зато не приходится вводить специальные ключевые слова — используются обычные языковые механизмы:
let a = 10i32;
println!("{}", size_of_val(&a));
Забавный нюанс: в Rust пустые структуры (такие как Foo из примера) занимают 0 байт, соответственно массив любого размера таких структур тоже будет занимать 0 байт.
[Поиграться с кодом]
Получаем максимальное и минимальное значение типа
В D, опять-таки, используются свойства типов:
char.max
char.min
ulong.max
double.min_normal
В Rust используются С-подобные константы:
i8::MAX
i8::MIN
u64::MAX
f64::MIN
Таблица соответствия типов
C D Rust
-----------------------------------------------------
bool bool bool
char char
signed char char i8
unsigned char ubyte u8
short short i16
unsigned short ushort u16
wchar_t wchar
int int i32
unsigned uint u32
long int i32
unsigned long uint u32
long long long i64
unsigned long long ulong u64
float float f32
double double f64
long double real
_Imaginary long double ireal
_Complex long double creal
Сравнение не совсем правильное, так как в С используются платформозависимые типы, а в D наоборот — фиксированного размера. Для Rust подбирал именно аналоги фиксированного размера.
Особые значения чисел с плавающей точкой
double.nan
double.infinity
double.dig
double.epsilon
double.mant_dig
double.max_10_exp
double.max_exp
double.min_10_exp
double.min_exp
f64::NAN
f64::INFINITY
f64::DIGITS
f64::EPSILON
f64::MANTISSA_DIGITS
f64::MAX_10_EXP
f64::MAX_EXP
f64::MIN_10_EXP
f64::MIN_EXP
Как видим, в Rust снова используются константы, которые, кстати, принято писать в верхнем регистре.
Остаток от деления вещественных чисел
Тут никаких откровений — в Rust, как и в D, имеется оператор %.
Обработка NaN значений
И в D, и в Rust сравнение с NaN даст в результате false
.
let x = 1f64;
let y = NAN;
println!("{}", x < y); // false
println!("{}", y < x); // false
println!("{}", x == y); // false
Асерты — полезный механизм выявления ошибок
Оба языка предоставляют асерты "из коробки", но в D они являются специальной языковой конструкцией:
assert( e == 0 );
A в Rust — просто макросами:
assert!(condition);
assert_eq!(a, b);
Впрочем, есть и интересное отличие: в D асерты в релизной сборке отключаются, кроме специального случая assert(0)
, который используется для обозначения недостижимого при нормальном выполнении кода.
В Rust они остаются и в релизе, впрочем, аналогичное поведение можно получить при помощи макроса debug_assert!
. Для более явного обозначения недостижимого когда используется отдельный макрос unreachable!
.
Итерирование по массиву (коллекции)
int array[17];
foreach( value ; array ) {
func( value );
}
let array = [0; 17];
for value in &array {
println!("{}", value);
}
Особой разницы нет, хотя цикл for
в Rust и не похож на своего родственника из С.
Инициализация элементов массива
int array[17];
array[] = value;
В D мы можем инициализировать массив одним значением, как показано выше. Стоит заметить, что после создания массив сначала будет инициализирован значением по умолчанию того типа, который в нём содержится.
let array = [value; 17];
В Rust присутствует специальный синтаксис для этого случая.
Создание массивов переменной длины
D имеет встроенную поддержку массивов переменной длины:
int[] array;
int x;
array.length = array.length + 1;
array[ array.length - 1 ] = x;
Rust, следуя своей "философии явности", требует задать значение, которым будут инициализированы новые элементы при вызове метода resize
. Поэтому правильнее пример будет записать следующим образом:
let mut array = Vec::new();
array.push(value);
Обратите внимание, что нам не приходится указывать тип элементов содержащихся в векторе — они будут выведены автоматически.
Соединение строк
В D есть специальные перегружаемые операторы ~ и ~=, предназначенные для соединения списков:
char[] s1;
char[] s2;
char[] s;
s = s1 ~ s2;
s ~= "hello";
Официальная документация аргументирует наличие отдельных операторов тем, что перегрузка оператора +
может приводить к неожиданностям.
let s1 = "abc";
let s2 = "eee";
let mut s = s1.to_owned() + s2;
s.push_str("world");
В Rust, с одной стороны, эти проблемы невозможны из-за необходимости явного приведения типов. С другой стороны, оператор +=
для строк всё-таки не реализован.
Форматированный вывод
import std.stdio;
writefln( "Calling all cars %s times!" , ntimes );
println!("Calling all cars {} times!" , ntimes);
Как видим, языки в этом плане не особо различаются. Разве что в Rust форматирование не похоже на "привычное" из С.
Обращение к функциям до объявления
Оба языка используют модули, поэтому порядок определения не имеет значения и предварительные объявления не нужны.
fn foo() -> Test {
bar()
}
fn bar() -> Test {
Test { a: 10, b: 20 }
}
struct Test {
a: i32,
b: i32,
}
Функции без аргументов
void foo() {
...
}
fn foo() {
...
}
Сравнение несколько теряет смысл в отрыве от С, так как оба языка не требуют указывать void
для обозначения отсутствия аргументов.
Выход из нескольких блоков кода
Louter: for( i = 0 ; i < 10 ; i++ ) {
for( j = 0 ; j < 10 ; j++ ) {
if (j == 3) break Louter;
if (j == 4) continue Louter;
}
}
'outer: for i in 0..10 {
'inner: for j in 0..10 {
if i == 3 {
break 'outer;
}
if j == 4 {
continue 'inner;
}
}
}
Синтаксис break/continue с меткой практически идентичен.
Пространство имён структур
Опять же, в обоих языках нет отдельного пространства имён для структур.
Ветвление по строковым значениям (например, обработка аргументов командной строки)
void dostring( string s ) {
switch( s ) {
case "hello": ...
case "goodbye": ...
case "maybe": ...
default: ...
}
}
fn do_string(s: &str) {
match s {
"hello" => {},
"goodbye" => {},
"maybe" => {},
_ => {},
}
}
В данном случае особой разницы не видно, но в Rust конструкция match
— это полноценное сравнение с образцом, что позволяет делать более хитрые вещи:
enum Type {
Common,
Secret,
Unknown,
}
struct Data {
id: i32,
data_type: Type,
info: Vec<i32>,
}
fn check_data(data: &Data) {
match *data {
Data { id: 42, .. } => println!("The Ultimate Question..."),
Data { data_type: Type::Secret, info: ref i, .. } if i.is_empty() => println!("Empty secret data!"),
_ => println!("Some data..."),
}
}
Подробнее в документации (перевод).
Выравнивание полей структур
В D есть специальный синтаксис, с помощью которого вы можете детально настроить выравнивание отдельных полей:
struct ABC {
int z; // z is aligned to the default
align(1) int x; // x is byte aligned
align(4) {
... // declarations in {} are dword aligned
}
align(2): // switch to word alignment from here on
int y; // y is word aligned
}
В Rust можно только полностью отключить выравнивание для отдельных структур:
#[repr(packed)]
struct Abc {
...
}
Анонимные структуры и объединения
D поддерживает анонимные структуры, что позволяет сохранять плоский внешний интерфейс для вложенных сущностей:
struct Foo {
int i;
union {
struct { int x; long y; }
char* p;
}
}
Foo f;
f.i;
f.x;
f.y;
f.p;
В Rust нет анонимных структур или объединений, поэтому аналогичный код будет выглядеть вот так:
enum Bar {
Baz {x: i32, y: i32 },
Option(i8),
}
struct Foo {
i: i32,
e: Bar,
}
Более того, Rust не позволит случайно обратиться не к тому полю объединения, которые было инициализировано. Поэтому и обращаться к ним придётся иначе:
match f.e {
Bar::Val(a) => println!("{}", a),
Bar::Baz { x, y } => println!("{} and {}", x, y),
}
Таким образом, объединения нельзя использовать как (полу)легальное преобразование типов, зато исключаются потенциальные ошибки.
Определение структур и переменных
Оба языка требуют раздельного объявления типа и переменной, то есть, как на С, записать не получится:
struct Foo { int x; int y; } foo;
Получение смещения поля структуры
В D у полей есть специальное свойство offsetof
:
struct Foo { int x; int y; }
off = Foo.y.offsetof;
На данный момент Rust не поддерживает такую возможность, так что при необходимости вам придётся вручную вычислять смещения, манипулируя указателями на члены структуры. Впрочем, offsetof
является зарезервированным ключевым словом, а значит со временем такая функциональность должна появиться.
Инициализация объединений
D требует явного указания на то, какому полю объединения присваивается значение:
union U { int a; long b; }
U x = { a : 5 };
Rust поступает аналогично, кроме того, как уже говорилось, он не позволит обратиться не к тому полю объединения, которое было инициализировано.
enum U {
A(i32),
B(i64),
}
let u = U::A(10);
Инициализация структур
В D структуры можно инициализировать как по порядку, так и с указанием имён полей:
struct S { int a; int b; int c; int d; }
S x = { 1, 2, 3, 4 };
S y = { b : 3 , a : 5 , c : 2 , d : 10 };
В Rust указание имён обязательно:
struct S {
a: i32, b: i32, c: i32, d: i32,
}
let x = s { 1, 2, 3, 4 }; // Erorr.
let y = S { a: 1, b: 2, c: 3, d: 4 }; // Ok.
Инициализация массивов
В D существует много способов инициализации массива, в том числе с указанием индексов инициализируемых элементов:
int[3] a = [ 3, 2, 0 ];
int[3] a = [ 3, 2 ]; // unsupplied initializers are 0, just like in C
int[3] a = [ 2 : 0, 0 : 3, 1 : 2 ];
int[3] a = [ 2 : 0, 0 : 3, 2 ]; // if not supplied, the index is the previous one plus one.
В Rust возможно либо перечислить все значения, которыми мы хотим инициализировать массив, либо указать одно значение для всех элементов массива:
let a1 = [1, 2, 3, 4, 5];
let a2 = [0; 6];
Экранирование спецсимволов в строках
Оба языка, наряду с экранированием отдельных символов, поддерживают так называемые "сырые строки":
string file = "c:\root\file.c";
string file = r"c:rootfile.c"; // c:rootfile.c
string quotedString = `"[^\]*(\.[^\]*)*"`;
let file = "c:\root\file.c";
let file = r"c:rootfile.c";
let quoted_string = r#""[^\]*(\.[^\]*)*""#;
В Rust "сырые строки" формируются довольно просто: они начинаются с символа r
, за которым следует произвольное количество символов #
, с последующей кавычкой ("
). Завершаются строки кавычкой с таким же количеством #
. В D разновидностей строк заметно больше.
ASCII против многобайтных кодировок
В D поддерживается несколько видов строк, которые хранят символы разного типа:
string utf8 = "hello"; // UTF-8 string
wstring utf16 = "hello"; // UTF-16 string
dstring utf32 = "hello"; // UTF-32 string
В Rust существует только один тип строк, которые представляют последовательность UTF-8 байт:
let str = "hello";
Константин aka kstep опубликовал на хабре серию переводов про строковые типы в Rust, так что если вас интересуют подробности, то рекомендую ознакомиться с ними. Ну или с официальной документацией (перевод).
Отображение перечисления на массив
enum COLORS { red, blue, green }
string[ COLORS.max + 1 ] cstring = [
COLORS.red : "red",
COLORS.blue : "blue",
COLORS.green : "green",
];
Аналог на Rust с применением макроса collect! будет выглядеть следующим образом:
use std::collections::BTreeMap;
#[derive(PartialOrd, Ord, PartialEq, Eq)]
enum Colors {
Red,
Blue,
Green,
}
let cstring: BTreeMap<_, _> = collect![
Colors::Red => "red",
Colors::Blue => "blue",
Colors::Green => "green",
];
Создание новых типов
D позволяет создавать новые типы из имеющихся (strong typedef):
import std.typecons;
alias Handle = Typedef!( void* );
void foo( void* );
void bar( Handle );
Handle h;
foo( h ); // syntax error
bar( h ); // ok
В том числе, с заданием дефолтного значения:
alias Handle = Typedef!( void* , cast( void* ) -1 );
Handle h;
h = func();
if( h != Handle.init ) {
...
}
В Rust это делается через использование структуры-кортежа (tuple struct, перевод):
struct Handle(*mut i8);
fn foo(_: *mut i8) {}
fn bar(_: Handle) {}
foo(h); // error
bar(h); // ok
Создать значение без инициализации Rust и так не позволит, а для создания значения по умолчанию правильным будет реализовать трейт Default:
struct Handle(*mut i8);
impl Default for Handle {
fn default() -> Self {
Handle(std::ptr::null_mut())
}
}
let h = Handle::default();
Сравнение структур
struct A {
int a;
}
if (a1 == a2) { ... }
#[derive(PartialEq)]
struct A {
a: i32,
}
if a1 == a2 { ... }
Разница только в том, что D неявно реализует для нас оператор сравнения, а Rust надо об этом попросить, что мы и делаем через #[derive(PartialEq)]
.
Сравнение строк
string str = "hello";
if( str == "betty" ) {
...
}
if( str < "betty" ) {
...
}
let str = "hello";
if str == "betty" {
...
}
if str < "betty" {
...
}
В обоих языках строки можно сравнивать на равенство и больше/меньше.
Сортировка массивов
D использует обобщённые реализации алгоритмов:
import std.algorithm;
type[] array;
...
sort( array ); // sort array in-place
array.sort!"a>b" // using custom compare function
array.sort!( ( a , b ) => ( a > b ) ) // same as above
В Rust используется несколько другой подход: сортировка, как и некоторые другие алгоритмы, реализована для "срезов" (slice), а те контейнеры, для которых это имеет смысл, умеют к ним приводиться.
let mut array = [3, 2, 1];
array.sort();
array.sort_by(|a, b| b.cmp(a));
[Запустить]
Из мелких отличий: сравнение должно возвращать не bool
, а Ordering (больше/меньше/равно).
Данное сравнение заставило задуматься, почему в Rust сделано не так как в D или С++. Навскидку не вижу преимуществ и недостатков обоих подходов, так что спишем просто на особенности языка.
Строковые литералы
"This text "spans"
multiple
lines
"
"This text "spans"
multiple
lines
"
Оба языка поддерживают многострочные строковые константы.
Обход структур данных
Несмотря на название, в этом пункте, по моему, демонстрируется только возможность вложенных функций обращаться к переменным объявленным в внешних, так что я взял на себя смелость переписать код:
void foo() {
int a = 10;
void bar() {
a = 20;
}
bar();
}
В Rust можно объявлять вложенные функции, но захватывать переменные они не могут, для этого используются замыкания:
fn foo() {
let mut a = 10;
fn bar() {
//a = 20; // Error.
}
let mut baz = || { a = 20 };
baz();
}
Динамические замыкания
В Rust тоже имеются лябмды/делегаты/замыкания. Пример был выше по тексту, ну а если вам интересны подробности, то загляните в документацию (перевод).
Переменное число аргументов
В D есть специальная конструкция "..." позволяющая принять несколько параметров в качестве одного типизированного массива:
import std.stdio;
int sum( int[] values ... ) {
int s = 0;
foreach( int x ; values ) {
s += x;
}
return s;
}
int main() {
writefln( "sum = %d", sum( 8 , 7 , 6 ) );
int[] ints = [ 8 , 7 , 6 ];
writefln( "sum = %d", sum( ints ) );
return 0;
}
Rust не имеет прямой поддержки переменного количества аргументов, вместо этого предлагается использовать срезы или итераторы:
fn sum(values: &[i32]) -> i32 {
let mut res = 0;
for val in values {
res += *val;
}
res
}
fn main() {
println!("{}", sum(&[1, 2, 3]));
let ints = vec![3, 4, 5];
println!("{}", sum(&ints));
}
Заключение
Вот и всё. Конечно, сравнение двух языков, отталкивающееся от особенностей третьего, получается довольно специфическим, но определённые выводы сделать можно. Предлагаю вам сделать их самостоятельно.
Ну и по традиции сделаем заголовок компилирующимся:
macro_rules! man {
(C => D) => {{
"https://habrahabr.ru/post/276227/"
}};
(C => D => Rust) => {{
"https://habrahabr.ru/post/280904/"
}};
(Rust => $any:tt) => {{
"You are doing it wrong!"
}};
}
fn main() {
println!("{}", man!(C => D));
println!("{}", man!(C => D => Rust));
println!("{}", man!(Rust => C));
println!("{}", man!(Rust => D));
}
Автор: DarkEld3r