For sometime I have been thinking to start coding in Rust however due to never ending things, I couldn’t start. Usually I start learning a language by start writing some utility code in it and learn along the way. However this time, time seems to be not on my side.
So, I decided to start learning small concepts step by step, one or two things at a time and then take it from there. This blog is all about how I took this challenge and started making progress toward learning Rust
Variables a immutable by default
Took me sometime to realize that Rust by default treats all assigned variables as immutable. Which means once binding is done, (another Rust concept, solely means that value has been assigned to the variable), you can’t reassign that variable with different value
fn main() {
// Variables by default are immutable use `mut` key word to make
// mutable
concept1()
}
fn concept1() {
let x: i8 = 15;
println!("Value of X = {}", x);
x = 20;
println!("After Reassignment, value of X = {}", x);
}
This doesn’t work
error[E0384]: cannot assign twice to immutable variable x
--> src/main.rs:17:5
|
15 | let x: i8 = 15;
| -
| |
| first assignment to x
| help: consider making this binding mutable: mut x
16 | println!("Value of X = {}", x);
17 | x = 20;
| **^^^^^^ cannot assign twice to immutable variable**
For more information about this error, try rustc --explain E0384.
So, question comes in how do we then run counters ? Simple answer is make the variable mutable
fn main() {
concept1() // Variables by default are immutable, use `mut`
}
fn concept1() {
let **mut** x: i8 = 15;
println!("Value of X = {}", x);
x = 20;
println!("After Reassignment, value of X = {}", x);
}
And it runs fine
Value of X = 15
After Reassignment, value of X = 20
Why Rust does that, even though Shadowing is possible. In fact Shadowing allows us to change type of the variable as well. Basically in nutshell by using let keyword we are creating an another variable with the same name.
Shadowing a Variable
fn main() {
variables_are_immutable();
variable_shadowing()
}
fn variables_are_immutable() { .... }
fn variable_shadowing() {
let x: i8 = 15;
println!("Value of X = {}", x);
**let** x = x + 20; **// This is possible because of `let` keyword**
println!("After Reassignment, value of X = {}", x);
}
And we get
Value of X = 15
After Reassignment, value of X = 35
Check out the below illustration for shadowing
and code
value of shadowed_variable is 10 -> 1st Value
value of shadowed_variable is ten -> 2nd Value
value of shadowed_variable is 10 -> First Value in different scope
value of shadowed_variable is scope check -> 2nd Value different scope
value of shadowed_variable is 20 -> 3rd Value
First value of shadowed_variable is 10 -> 1st Value accessed using reference variable first_value
As you can see Shadowing works like a STACK internally and you can remove the previous value by using drop(shadowed_variable)
and it will behave like STACK and start poping out values in lifo manner. Also keep in mind that Shadowing variable only work if the variable is in scope.
Similarly const is used to defined constants, which are valid for whole run of the program. Value of the constant can’t be something which needs to calculated at runtime
const DAY_IN_YEAR: i32 = 31 * 7 + 30 * 4 + 28;
fn main() {
print_constant();
}
fn variables_are_immutable() {...}
fn variable_shadowing() -> i32 {
let x: i32 = 15;
println!("Value of X = {}", x);
let x = x + 20;
println!("After Reassignment, value of X = {}", x);
return x
}
fn print_constant() {
println!("Value of constant {}", *DAY_IN_YEAR*);
}
Variable Type Conversion aka Casting
Rust allows to variable type conversion aka Casting on the fly. Like
fn calculate_avg_using_casting() {
let (a, b, c): (i32, f32, f32) = (13, 2.3, 120.0);
let avg = (a as f32 + b + c) / 3.0; // a is re-casted as f32
assert_eq!(avg, 45.100002);
println!("Test Passed");
}
It will print Test Passed . As you can see that variable a has be casted as f32
from i32
using as keyword. Word of caution, casting should be used with care as it might produce unpredictable outcome, like if f32
has been re-casted as i32
then everything after decimal will be truncated. Casting uses truncation then round will type conversion.
The Magic of Functions
Like most of the languages Rust also requires at least one function arguable main()
and in Rust we define functions with fn keyword. Apart from this functions in Rust is pretty straight forward.
fn main() {
println!("{}", variable_shadowing_and_return());
}
fn variable_shadowing_and_return() -> i32 {
let x: i32 = 15;
println!("Value of X = {}", x);
let x = x + 20;
x * x **// missing `;` is intentional as it is an expression **
}
Statements vs Expressions
Above is an example of function in Rust with a return type i32
. Functions are combination of Expressions and Statements. Expressions always generates a value and Statements are just an action. So, in Rust something like this is invalid a = b = 1
as we can’t assign a Statements to variable as b = 1
is an action not a value. Adding a ;
to Expressions will convert that to Statements
In Rust if the last line is an expressions then it will be passed out as return values for the function.
Function main()
can return a Result type from Rust 1.26 onwards (details). For Example
use std::num::ParseIntError;
fn main() -> Result<(), ParseIntError> {
let some_number = "10";
let n = match some_number.parse::<i32>() {
Ok(n) => n,
Err(e) => return Err(e),
};
println!("{}", n);
Ok(())
}
Lastly, if your function doesn’t return any value then it return something called unit data type
()
. Rust complier automatically infer to unit data type if the function is not returning. We can explicitly mention that too like
fn some_func(x: i8) **-> ()** {
println!("Square of X is {}", x * x)
}
String Literals or Types … hmm!!
Take a look at the below Diff View and I will try to explain what I meant to show
In Rust there 2 ways work with Strings using String Literals
or String Type
. When say let some_string = "actually it is random string"
this is called String Literals
because it is literally written in the executable. As mentioned above they are immutable and the size needs to be known before compilation. So it can’t hold any value which is dynamic in nature.
A String is stored as a vector of bytes
(Vec<u8>)
, but guaranteed to always be a validUTF-8
sequence. String is heap allocated, growable and not null terminated.&str
is aslice (&[u8])
that always points to a validUTF-8
sequence, and can be used to view into a String, just like&[T]
is a view intoVec<T>
.
To deal with dynamic nature of String we use String Type
, which are stored in heap and mutable.
The code below will create a variable called my_string
which is &str
type stored in STACK and will store the value in Heap. In Stack it will hold the pointer to the Heap location, length of the string and capacity. The star suggests that the capacity is always greater than equal to the length of the string.
fn main() {
work_with_strings();
}
fn work_with_strings() {
let mut my_string = String::from("APPLE");
println!("Original my string: {}", my_string);
}
Same code with adding more to the existing string
fn main() {
work_with_strings();
}
fn work_with_strings() {
let mut my_string = String::*from*("APPLE");
println!("Original my string: {}", my_string);
my_string.push_str(" is GOOD");
println!("Final my string: {}", my_string);
}
Becomes something like this
To reference characters in string using index, because strings are valid UTF-8
, they do not support indexing. We need to use the .chars()
method. Read more about the Strings here
fn main() {
work_with_strings();
}
fn work_with_strings() {
let mut my_string = String::*from*("APPLE");
println!("Original my string: {}", my_string);
my_string.push_str(" is GOOD");
println!("Final my string: {}", my_string);
println!("The characters are \n");
for ch in my_string.chars() {
print!("{},", ch)
}
}
Loopy Loops
Like any other language Rust also has couple of mechanisms which we can use to iterate over some stuff. Loops, While and For, as the name suggest they more or less similar to other language except Loops.
Loops are very much similar to While, with 2 main difference. It doesn’t come with a condition like while and it can return value which while can’t.
As you can see the loops actually returns the count
to the result variable once the break
statement executes, however while
just execute and breaks out of the loop when the condition matched.
Also notice that because the
loop
is assigned to a variable result so we put;
at the end of it to make itstatement
.
Suppose we have nested loops and want to break out to the outer loop
. Then, we can use loop labels to specify which loop a break or continue applies to. In the following example, 'outer
is the label given to the outer loop.
'outer: for x in 0..5 {
for y in 0..5 {
if y > 2{
break 'outer
}
println!("x: {}, y: {}", x, y);
}
}
I think I have enough content to start with for now. Hope this helps a little more to understand the basics of Rust . I am still learning and will keep putting my thoughts in future write-ups!!
Top comments (0)