The previous article was perceived better than I expected, so I decided to continue the experiment. This is a kind of answer to the translation of the article Programming in D for C Programmers by Dmitry aka vintage . It seems to me that in the field of application C Rust is more appropriate than the replacement of Go, as suggested in the last article. The more interesting it will be to compare. Again, I will not give the code on C, especially since the analogue on D still looks laconic.
Let me remind you that in C (and in C ++) for this purpose there is a special operator sizeof
, which can be applied to both types and variables. In D, the size is accessible through a property (which can also be applied to variables):
int.sizeof (char*).sizeof double.sizeof Foo.sizeof
Rust uses a function that refers to the internals of the compiler (corresponding to intrinsic ):
size_of::<i32>() size_of::<*const i8>() size_of::<f64>() size_of::<Foo>()
At the same time, due to the lack of overloading of functions, another function is used to obtain the size of variables - size_of_val
. Perhaps this separation is somewhat less convenient, but it is not necessary to enter special keywords — the usual language mechanisms are used:
let a = 10i32; println!("{}", size_of_val(&a));
Funny thing: in Rust, empty structures (such as Foo from the example) occupy 0 bytes, respectively, an array of any size of such structures will also occupy 0 bytes.
[Play around with the code]
In D, again, type properties are used:
char.max char.min ulong.max double.min_normal
Rust uses C-like constants:
i8::MAX i8::MIN u64::MAX f64::MIN
[Run]
')
CD 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
The comparison is not entirely correct, since C uses platform-dependent types, and in D, on the contrary, it is of a fixed size. For Rust, I picked exactly analogs of fixed size.
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
As you can see, Rust uses constants again, which, by the way, are usually written in upper case.
There are no revelations - in Rust, as in D, there is the operator% .
In both D and Rust, a comparison with NaN will result in false
.
let x = 1f64; let y = NAN; println!("{}", x < y); // false println!("{}", y < x); // false println!("{}", x == y); // false
Both languages provide assertions out of the box, but in D they are a special language construct:
assert( e == 0 );
A in Rust - just macros:
assert!(condition); assert_eq!(a, b);
However, there is an interesting difference: in D, the asserts in the release assembly are disabled, except for the special case assert(0)
, which is used to indicate the unattainable code in normal execution.
In Rust, they remain in the release, however, similar behavior can be obtained using the macro debug_assert!
. For a more explicit designation of the unattainable when using a separate macro unreachable!
.
int array[17]; foreach( value ; array ) { func( value ); }
let array = [0; 17]; for value in &array { println!("{}", value); }
There is not much difference, although the for
loop in Rust is not similar to its relative from C.
int array[17]; array[] = value;
In D, we can initialize an array with a single value, as shown above. It is worth noting that after creating the array, it will first be initialized with the default value of the type it contains.
let array = [value; 17];
Rust has a special syntax for this case.
D has built-in support for variable-length arrays:
int[] array; int x; array.length = array.length + 1; array[ array.length - 1 ] = x;
Rust, following its "philosophy of clarity," requires you to specify a value with which new elements will be initialized when calling the resize
method. Therefore, it is more correct to write the example as follows:
let mut array = Vec::new(); array.push(value);
Please note that we do not have to specify the type of elements contained in the vector - they will be displayed automatically.
In D, there are special overloaded operators ~ and ~ = designed to join lists:
char[] s1; char[] s2; char[] s; s = s1 ~ s2; s ~= "hello";
Official documentation argues for the presence of individual operators so that overloading the operator +
can lead to surprises.
let s1 = "abc"; let s2 = "eee"; let mut s = s1.to_owned() + s2; s.push_str("world");
In Rust , on the one hand, these problems are impossible because of the need for explicit type conversions. On the other hand, the +=
operator for strings is still not implemented.
import std.stdio; writefln( "Calling all cars %s times!" , ntimes );
println!("Calling all cars {} times!" , ntimes);
[Run]
As you can see, the languages in this regard are not particularly different. Is that in Rust formatting is not similar to the "usual" from C.
Both languages use modules , so the order of definition does not matter and preliminary declarations are not needed.
Rust example:
fn foo() -> Test { bar() } fn bar() -> Test { Test { a: 10, b: 20 } } struct Test { a: i32, b: i32, }
void foo() { ... }
fn foo() { ... }
Comparison loses some sense in isolation from C, since both languages do not require to specify void
to indicate the absence of arguments.
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; } } }
The break / continue syntax with a label is almost identical.
Again, in both languages there is no separate namespace for structures.
void dostring( string s ) { switch( s ) { case "hello": ... case "goodbye": ... case "maybe": ... default: ... } }
fn do_string(s: &str) { match s { "hello" => {}, "goodbye" => {}, "maybe" => {}, _ => {}, } }
In this case, there is not much difference, but in Rust the match
construction is a full-fledged comparison with the sample, which allows you to do more clever things:
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..."), } }
Read more in the documentation ( translation ).
D has a special syntax with which you can fine-tune the alignment of individual fields:
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 }
In Rust, you can only completely disable alignment for individual structures:
#[repr(packed)] struct Abc { ... }
D supports anonymous structures, which allows you to save a flat front end for nested entities:
struct Foo { int i; union { struct { int x; long y; } char* p; } } Foo f; fi; fx; fy; fp;
There are no anonymous structures or unions in Rust, so similar code would look like this:
enum Bar { Baz {x: i32, y: i32 }, Option(i8), } struct Foo { i: i32, e: Bar, }
Moreover, Rust will not allow to accidentally turn to the wrong join field that was initialized. Therefore, you will have to address them differently :
match fe { Bar::Val(a) => println!("{}", a), Bar::Baz { x, y } => println!("{} and {}", x, y), }
Thus, associations cannot be used as (semi) legal type conversions, but potential errors are eliminated.
Both languages require separate type and variable declarations, that is, as in C, it will not work:
struct Foo { int x; int y; } foo;
In D, fields have a special offsetof
property:
struct Foo { int x; int y; } off = Foo.y.offsetof;
At the moment, Rust does not support this feature, so if necessary, you will have to manually calculate the displacements by manipulating pointers to members of the structure. However, offsetof
is a reserved keyword, which means that such functionality should appear over time.
D requires an explicit indication of which join field is assigned a value:
union U { int a; long b; } U x = { a : 5 };
Rust does likewise, moreover, as already mentioned, it will not allow accessing the wrong join field that was initialized.
enum U { A(i32), B(i64), } let u = U::A(10);
In the D structure, you can initialize both in order and with the indication of the field names:
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 };
In Rust, the names must be indicated:
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.
In D, there are many ways to initialize an array, including specifying the indexes of elements to be initialized:
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.
In Rust, it is possible either to list all the values with which we want to initialize the array, or to specify one value for all the elements of the array:
let a1 = [1, 2, 3, 4, 5]; let a2 = [0; 6];
Both languages, along with the screening of individual characters, support the so-called "raw lines":
string file = "c:\\root\\file.c"; string file = r"c:\root\file.c"; // c:\root\file.c string quotedString = `"[^\\]*(\\.[^\\]*)*"`;
let file = "c:\\root\\file.c"; let file = r"c:\root\file.c"; let quoted_string = r#""[^\\]*(\\.[^\\]*)*""#;
In Rust, "raw lines" are formed quite simply : they begin with the character r
, followed by an arbitrary number of characters #
, followed by a quote ( "
). The lines are completed with a quote with the same number #
. In D, the number of lines is noticeably larger .
D supports several types of strings that store different types of characters:
string utf8 = "hello"; // UTF-8 string wstring utf16 = "hello"; // UTF-16 string dstring utf32 = "hello"; // UTF-32 string
In Rust, there is only one type of string that represents a sequence of UTF-8 bytes:
let str = "hello";
Konstantin aka kstep published on Habré a series of translations about string types in Rust, so if you are interested in the details, I recommend reading them. Well, or with official documentation ( translation ).
enum COLORS { red, blue, green } string[ COLORS.max + 1 ] cstring = [ COLORS.red : "red", COLORS.blue : "blue", COLORS.green : "green", ];
Analog on Rust using the macro collect! would look like this:
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 allows you to create new types of existing (strong typedef):
import std.typecons; alias Handle = Typedef!( void* ); void foo( void* ); void bar( Handle ); Handle h; foo( h ); // syntax error bar( h ); // ok
Including, with the task of default value:
alias Handle = Typedef!( void* , cast( void* ) -1 ); Handle h; h = func(); if( h != Handle.init ) { ... }
In Rust, this is done through the use of a tuple structure ( tuple struct , translation ):
struct Handle(*mut i8); fn foo(_: *mut i8) {} fn bar(_: Handle) {} foo(h); // error bar(h); // ok
Creating a value without initializing Rust will not allow it, and to create a default value, it will be correct to implement the Default trait:
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 { ... }
The only difference is that D implicitly implements a comparison operator for us, and Rust needs to ask about it, which we do through #[derive(PartialEq)]
.
string str = "hello"; if( str == "betty" ) { ... } if( str < "betty" ) { ... }
let str = "hello"; if str == "betty" { ... } if str < "betty" { ... }
In both languages, strings can be compared for equality and more / less.
D uses generic implementations of algorithms:
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 uses a slightly different approach: sorting, like some other algorithms, is implemented for "slices" (slice), and those containers for which it makes sense can be brought to them.
let mut array = [3, 2, 1]; array.sort(); array.sort_by(|a, b| b.cmp(a));
[Run]
From small differences: the comparison should return not bool
, but Ordering (more / less / equal).
This comparison made you wonder why in Rust it was done differently from D or C ++. Offhand I do not see the advantages and disadvantages of both approaches, so we will write off simply on the peculiarities of the language.
"This text \"spans\" multiple lines "
"This text \"spans\" multiple lines "
Both languages support multiline string constants.
Despite the name, in this paragraph, in my opinion, only the ability of nested functions to access variables declared in external is demonstrated, so I took the liberty to rewrite the code:
void foo() { int a = 10; void bar() { a = 20; } bar(); }
In Rust, you can declare nested functions, but they cannot capture variables, for this they use closures:
fn foo() { let mut a = 10; fn bar() { //a = 20; // Error. } let mut baz = || { a = 20 }; baz(); }
Rust also has lyabmdy / delegates / closures. The example was higher in the text, but if you are interested in the details, check out the documentation ( translation ).
In D, there is a special "..." construct that allows you to take several parameters as a single typed array:
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 does not have direct support for a variable number of arguments, instead it is proposed to use slices or iterators:
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)); }
That's all. Of course, a comparison of two languages, based on the characteristics of the third, is quite specific, but certain conclusions can be made. I suggest you make them yourself.
Well, according to tradition, we will make the title compiled :
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)); }
Source: https://habr.com/ru/post/280904/
All Articles