Basics
Bindings
Variables, or more properly, bindings look as follows:
An assignment like a = 10
produces a blank type ()
called unit. Whenever
there is no other meaningful response type, ()
is returned.
We can explicitly specify the type of a binding:
Bindings may be shadowed, which means that a binding may be defined twice
with the same name. This way, we don’t have to artificially define things like
data_string
and data
. First, we could define data
as String
, and then
define data
as i32
with a value coming from parsed data
.
Type Inference
Rust compiler is pretty smart about type inference. For example, this works:
The new()
method of Vec
is called without any generic type. Compiler sees
that on the left side of the assignment, we specified the type to be i32
.
This also works:
The compiler “scans” the code and it can “see” that the vector instance should
be created for the i32
type, because that’s what we’re adding to it later on.
Scope
Variables that go out of scope are automatically removed by Rust. We don’t need
to free
memory explicitly. There is no runtime overhead for that.
Mutable Reference vs. Mutable Value
let &mut x
means that x points to some mutable value. However, x
itself
is not mutable, we can’t change what it points to.
Deep Copying
Heap data may be copied by value using the clone()
method.
s1
and s2
are two different variables pointing at different data in memory
(but the values happen to be the same). clone()
is much more expensive than a
simple pointer copy.
Values that are stored on the stack (ints, floats, tuples (when they hold scalar values), arrays, chars, etc.) do not need that.
Functions
The i + j
has no semicolon. With a semicolon, the return type would be ()
instead of i32
.
Functions return the last expression’s result by default, so return
is not
required.
Macros
Macro example:
Macros are similar to functions, but instead of returning data, they return code. They are often used to simplify common patterns.
In the case of printing, there are many ways of doing that depending on the provided
data type. The println!
macro takes care of figuring out the exact method to
call.
Types
There is a large choice of number types.
Conversions between types are always explicit. We use as
for that.
Rust does not have constructors. Every type has a literal form (e.g.
let a = Complex { re: 2.1, im: -1.4 };
). Many types also have a
new
method (it’s not a Rust keyword though, these are just
normal methods).
Loops
After such an iteration, accessing collection
is invalid! Rust assumes the
collection
is no longer needed, its lifetime is finished.
To circumvent it, include the &
symbol to use a reference:
To be able to modify item
during the iteration, a mutable reference should be used:
All three loops shown above are syntactic sugar for different method calls:
for item in collection
=for item in itoIterator::into_iter(collection)
for item in &collection
=for item in collection.iter()
for item in &mut collection
=for item in collecion.iter_mut()
Index variable
Such a pattern is discouraged in Rust.
Anonymous loops
Since a local variable is not needed, _
is used.
while(true) alternative
Rust has loop
loop type:
It acts as while true
, but is preferred.
Breaking out of nested loops
Loops can be labeled, and nested loops can be exited using the outer loop label.
Conditions
There is no concept of truthy/falsey values. Conditions rely on true
and
false
- that’s it.
match
There is match
, which is similar to switch
in other languages, but is much
more powerful, because it supports expressions. Compilation fails if match
does not cover all possible arms.
match
does not fall through through other cases. As soon as a case is matched,
it’s executed, and match
is exited.
match
is very useful with enums.
match
patterns can also provide values of internals of matched values:
Above, both bytes
(the whole array) and individual bit
are accessible to the
second match
arm.
Expressions
Almost everything is an expression. The following are possible:
Even break
returns a value:
The following are not expressions and, thus do not return values:
- expressions delimited by
;
- binding a name to a value with
=
(isn’t that already covered by the first rule anyway?) - type declarations (
fn
,struct
,enum
keywords)
References
References are created with &
. They are dereferenced with *
. In many cases,
Rust will deference a reference automatically for us, for convenience.
Strings
There is a String
type, which is stored on a heap. There is also a string
literal, which is actually a string slice.
If a function expects a string parameter, it’s a good practice to use a string
slice (&str
) instead of a String
, because:
- a string literal can be passed as is;
- a string slice can be easily taken out of a
String
with&some_string
A format!
macro is useful for creating strings composed of other values:
str
is a dynamically sized type (DST) in Rust. It means that the contents
of that type are not static and during runtime, the str
value may be of various
lengths (depending on the length of the string). Rust prefers types that have
known sizes. That’s why we use &str
instead, which is a slice - its size is
known - it’s a pointer to str
and a length of the value there (stored as
usize
).
OOP
Rust can be considered object-oriented or not, depending on the definition that we use. Some facts:
- Rust has structs and enums that can include some data and methods - like objects in OOP
- Parts of a struct may be public or private - encapsulation
- There is no inheritance, but traits may have default implementations of methods
- Some form of polymorphism can be achieved with generics