Scalar types
Scalar types are types representing a single value. In Rust, we can identify the following scalar types:
Creating and printing these values in a program can be done as follows:
fn main() {
let ch: char = 'z'; // four bytes, expresses single Unicode Scalar Value
let b: bool = true; // true or false
let i: i32 = -111; // 32 bit signed integer, can be positive or negative
let f: f32 = 3.4; // 32 bit float number
println!("char: {}\nbool: {}\ni32: {}\nfloat32: {}\n", ch, b, i, f);
}
When we cargo run
the above, we get the following output:
char: z bool: true i32: -111 float32: 3.4
To be able to run the example without having Rust installed, go here.
The let
keyword was used the bind a value to a variable. In the previous example, we explicitly specified a type for every variable. In a lot of cases, the Rust compiler can also infer the type. The following would also work:
fn main() {
let ch = 'z';
let b = true;
let i = -111;
let f = 3.4;
println!("char: {}\nbool: {}\ni32: {}\nfloat32: {}\n", ch, b, i, f);
}
The above is valid, though it is not the same as the first code snippet. When the compiler has to infer the type, it settles on using i32
for the integer variable and f64
for the float variable.
Another thing worth pointing out is that variables are immutable by default. In the following code, we try to change the value of b
from true
to false
:
fn main() {
let b = true;
println!("b contains: {}", b);
b = false;
println!("b now contains: {}", b);
}
When we run the above, we get the following:
error[E0384]: cannot assign twice to immutable variable `b` --> src\main.rs:3:5 | 2 | let b = true; | - | | | first assignment to `b` | help: consider making this binding mutable: `mut b` 3 | b = false; | ^^^^^^^^^ cannot assign twice to immutable variable
Even though the compiler does not let us run the code, it is nice enough to give us a hint:
help: consider making this binding mutable: `mut b`
If we want to be able to change the value of a variable, we need to specify that a variable is mutable. This is done when we define the variable and we use the mut
keyword to do so:
fn main() {
let mut b = true;
println!("b contains: {}", b);
b = false;
println!("b now contains: {}", b);
}
We can now run the code without any errors:
b contains: true b now contains: false
Oftentimes, you will also read about primitive types. These primitive types include the scalar types as well as some additional ones:
The language doc mentions the entire list of primitive types here. It does a good job of thoroughly explaining all the types and their behaviours. I will cover most of the primitive types in future posts. There is one last thing I want touch on in this post.
Copy semantics and move semantics
I wanted to include this right at the beginning because it was so confusing to me when I was starting out with Rust. All primitive types share a few common traits. In Rust, traits are used to define shared behaviour. One particularly interesting trait for the primitive types is the Copy
trait.
Types that have the copy traits are said to have copy semantics
. What this means is that when you pass them into a function, you pass a copy of that value into that function.
Non-primitive types do not have the Copy
trait by default. This means they are subject to move semantics
. When you pass them into a function, you move
the value into that function. Without going into details (that I am still learning about myself), this pretty much means that when the function scope ends, the value is destroyed.
Here is an example function that takes in a primitive type:
fn main() {
let i: i32 = 100;
// Using primitive types in func is done using 'copy semantics':
copy_semantics(i);
// We can still use the values after the function scope closes:
println!("i32: {}", i);
}
fn copy_semantics(i: i32) {
println!("i32: {}", i);
}
In the code above, we define an i32
which we then pass to a function. Here, the Copy
trait ensures that the value of i
is copied into the function. After the copy_semantics
function completes, i
still exists and we print the variable to screen. This code will run without any errors.
The following illustrates the move semantics using a struct:
fn main() {
// All non-primitive types move-semantics by default:
let marie = Person {
name: String::from("marie"),
age: 2,
};
move_semantics(marie);
// Next line is illegal because a move happened when we passed marie to a function:
println!("{} is {} years old.", marie.name, marie.age);
}
fn move_semantics(person: Person) {
println!("{} is {} years old.", person.name, person.age);
}
struct Person {
name: String,
age: u8,
}
In the above code, we define a struct and pass it into a function. In this case, the value is moved
into the function. When the function move_semantics
completes, the variable is destroyed and marie
is no more. Running the above code will give us the following error:
cargo run Compiling testing v0.1.0 (example) error[E0382]: borrow of moved value: `marie` --> src\main.rs:9:49 | 3 | let marie = Person { | ----- move occurs because `marie` has type `Person`, which does not implement the `Copy` trait ... 7 | move_semantics(marie); | ----- value moved here 8 | // Next line is illegal because a move happened when we passed marie to a function: 9 | println!("{} is {} years old.", marie.name, marie.age); | ^^^^^^^^^ value borrowed here after move error: aborting due to previous error For more information about this error, try `rustc --explain E0382`. error: could not compile `testing` To learn more, run the command again with --verbose.
Here we see the compiler mention a few things. Let’s start with the following:
3 | let marie = Person { | ----- move occurs because `marie` has type `Person`, which does not implement the `Copy` trait
The compiler points out to use the fact that the Person
struct, does not implement the Copy
trait. After that, it goes on to tell us where we have a move
:
7 | move_semantics(marie); | ----- value moved here
Finally, it points us to where we encounter a problem:
9 | println!("{} is {} years old.", marie.name, marie.age); | ^^^^^^^^^ value borrowed here after move
It is saying value borrowed here after move
. Because the value was moved into the move_semantics
function, it was destroyed after that function was completed. There are ways to deal with this, using a reference for instance. The following code would run:
fn main() {
let marie = Person {
name: String::from("marie"),
age: 2,
};
// the & before marie means we are now passing a reference.
move_semantics(&marie);
println!("{} is {} years old.", marie.name, marie.age);
}
// The '&' before Person means the functionn now takes a reference.
fn move_semantics(person: &Person) {
println!("{} is {} years old.", person.name, person.age);
}
struct Person {
name: String,
age: u8,
}
The specifics on why this works, and what other solutions we could implement here, are for a future post.
Wrapping up
As I learn more about Rust, I will dive into more complicated features of the language. My goal is to learn it proper and cover the foundations first. This first post on Rust covered covered the scalar types and some other basics, like defining variables. For more on scalars and the other primitive types, you can check the docs. Rust primitives offers pretty comprehensive examples and explanations.