Rust Programming Language
Learning
To learn I am using different resources including
Rust Doc
Hello World
Hello World using rustc
We call println!(), which is a macro and NOT a function. We write this inside the main() function, and we define functions using fn keyword.\\
To compile we use rustc <filename.rs>. After compiling we will get a binary with same name as filename but without any extension(in Linux) and with .exe in windows.
fn main() {
println!("Hello World!");
}
Hello World!
By default rust binaries are really big because it statically links libraries, to force it to link libraries dynamically add -C prefer-dynamic to the rustc command.
Hello World using cargo
Cargo is our package handler. To create a cargo project will use cargo new hello_cargo, this will create hello_cargo directory with the following structure.
hello_cargo/
├── Cargo.toml
└── src
└── main.rs
main.rs is where our code will go(for now). This also initializes a git repository inside hello_cargo directory.\\
cargo.toml is our project's configuration file. Here we will add different dependencies etc. Now write code to display in main.rs. To build cargo project we can will use cargo build. This will create a binary(with lots of things beside it) inside a target directory. To simply run the program, use cargo run, for error checking use cargo check.
Common Concepts
From: Rust Doc::Common Concepts
Variables and Mutable
First to define variables we use let keyword. For example:
fn main(){
let var=5;
println!("The value of var is {}",var);
}
The value of var is 5
By default variables are immutable,i.e. cannot be changed. For example code given below wont work
fn main(){
let var=5;
println!("The value of var is {}",var);
var = 6;
}
error: Could not compile `cargouqGIqJ`.
and will result in error.\\
To deal with this we use mut keyword. Like
fn main(){
let mut var=5;
println!("The value of var is {}",var);
var = 6;
println!("The value of var now is {}",var);
}
The value of var is 5 The value of var now is 6
Value to a immutable can be assigned only once, either in declaration, or in assignment.
The variable we are referring to depends on the scope of the object. Scope define lifespan as well as where and how variable can be accessed. In a program same named variables can exist inside same program in different scopes.
fn main(){
let var=5;
let var2=45;
println!("The value of outer var is {} and var2 is {}",var,var2);
{
let var = 10;
println!("The value of inner var is {}! but value of var2 is still {}",var,var2);
}
println!{"The value of outer var is still {} and value of var2 is {}",var,var2};
}
The value of outer var is 5 and var2 is 45 The value of inner var is 10! but value of var2 is still 45 The value of outer var is still 5 and value of var2 is 45
Even without mut we can still change the value as well as type of variable by redefining it. For example
fn main(){
let var=5;
println!("Value of var is {}", var);
let var = "hello!!!";
println!("Now value of var is {}", var);
}
Value of var is 5 Now value of var is hello!!!
Constants
cost keyword is used to define constants. These are like let but with some differences.
always immutable
data type MUST be annotated
can be set during compile time or before only
Data types in Rust
Every "value" in Rust has a certain data type. Rust is a statically typed language, so it must know type of every value and variable at compile time, either implicitly or explicitly. Rust has different types of data types:
Scalar Data Type
Scalar type represent single value. These divide in 4 types
integer Numbers without fractional components. Signed integer contain negative numbers, unsigned do not. Types of integer data types.
Length Signed Unsigned 8-bit i8u816-bit i16u1632-bit i32u3264-bit i64u64128-bit i128u128arch isizeusizeisizeandusizedepends on the architecture of the system. To show a specific type we can append type at the end like:fn main(){ let var1:u32 = 300; let var2 = 300u8; }error: Could not compile `cargoVFL40k`.
Above code wont compile because 300 is too big to be u8(will cause overflow without checks). We can also write integer literals in following forms too.
Literal Type Example Decimal 98_100Hexadecimal 0xffoctal 0o77Binary 0b11011Byte b'A'For example
fn main(){ let var1 = 0o67; let var2 = 0xf4; let var3 = b'Z'; println!("Decimal: {} {} {}", var1, var2, var3); }Decimal: 55 244 90
floating point Floating points in rust are
f32(single-precision) andf64(double-precision).boolean This contain
trueandfalse.characters Characters are defined using
charkeyword and in single quote',
Numberic Operations
Numeric operations are possible on and using integer and floating point numbers.
fn main(){
let ( a, b, c, d) = (5.56, 45.56, 75.65 , 98.56);
println!("Sum {} Diff {} Mul Div {} Remainder {}", a+b, b-c, c / a, d % a);
}
Sum 51.120000000000005 Diff -30.090000000000003 Mul Div 13.606115107913672 Remainder 4.040000000000009
Compound Data Types
Multiple values in one type. Rust has two primitives
Tuple
Array
Functions in Rust
We define a function using fn keyword and following it with function name. For example
fn main() {
function_name();
}
fn function_name() {
println!("Hello Function!");
}
Hello Function!
Parameters
Functions can have parameter(s) and type must be specified.
fn main(){
multi(45.5,84.69);
}
fn multi(a: f64,b: f64){
println!("{}",a*b);
}
3853.395
Expression and expression blocks
{
a+b;
"returned"
}
This is a expression block, statement without ; is returned at last and before that processed normally.
Function With Return Value
We must specify return type to a function. We do that using ->. Like expression block it may be last value or can be explicitly stated using return statement.
fn main(){
println!("{}",multi(45.5,84.69));
}
fn multi(a: f64,b: f64)->f64{
a*b
// or
// return a*b;
}
3853.395
Control Flow in Rust
if and else
if is an expression in rust, making it much more powerful.
if condition {
// something
} else if {
// something else
} else {
// something else but different
}
because it is an expression it can be used anywhere any normal expression can be used. But when used in left side of let, value from all the branches should be of same data type
loop
Loops code forever. break exits the loop. Value written just after break will be returned. continue continues skips execution of loop from that point and starts over.
loop{
//code
if condition {
break value_to_return;
}
}
while
while condition {
//code
}
for
Loops over a range
fn main(){
let numbers= {1,7,5,7,10};
for num in numbers {
println!("{}",number);
}
}
match
Matches with all the possible values of a variable.
match somevariable{
possible_value1 => { code for it },
possible_value2 => code, //{ are not required,
_ => code // code for rest of the cases
}
if letis great way to deal with only one case.elsecan be used as_in this case.if let possible_valuen = somevariable { // code in this case }
Structures in Rust
To define
struct stuct_name {
key1 : data_type,
key2 : data_type,
//....
}
To use
let var = stuct_name {
key2 : data_value,
key10 : data_value,
keynth, // if variable name in which data is stored is same as key then it can be written directly
//...
}
struct_name.key1 //gives access to the key
Trailing commas are allowed.
let var = stuct_name {
key2 : data_value,
..another_struct //can also be used
// except key2 all the key are assigned values from another_struct
//...
}
Tuple struct can be used for storing data without defining key names.
stuct stuct_name(type1,type2,type3);
let var = struct_name(val1, val2, val3);
Methods in Rust
Methods are defined on struct, enum and trait.
impl struct_name {
fn fn_name(&self,...){
//code
}
fn fn_name2(&self,...){
//code
}
}
Here self is associated with struct (or enum or trait) itself. Methods can take ownership of self, borrow mutably or immutably.
Multiple
implblocks per structure is allowed.Any function written inside is an associated function.
Having
selfas parameter is not essential.
Enums in Rust
Enum let us define one from a set of value.
enum EnumName {
FirstOption,
SecondOption,
//...
}
Here it is needed that option should be an identifier. Now that identifier can have objects associated with it.
enum EnumName{
FirstOption(val1, val2),
SecondOption(val4,val5)
}
=Option<T>-
It is an Enum. It can be used when we do not know when some value will be available or not.
enum Option<T> {
None,
Some(T),
} // T is type
Error Handling in Rust
Generics in Rust
Generic Data Types in Rust
Function Definitions
fn_name<T>(v: T)->&T{
// code
}
Struct and Enum definations
struct strt_name<T>{
var1: T,
var2: T,
//...
}
In Enums
enum EnumName<T1,T2>{
A(T1),
B(T2),
//...
}
Method Definitions
impl<T> struct_name<T>{
fn method_name(&slef)->T{
//code
}
}
This T should be constrained.
Traits in Rust
Traits are methods defined of types that may share functionality. Same trait name can be used to define similar functionality for different types.
An example of trait with name TraitName is written below. It has one method method_name (can be multiple) which takes self as parameter and returns T.
pub trait TraitName{
fn method_name(&self)->T{
//code
}
}
Now we will impl-iment this trait for a type. This is done by writing
impl TraitName for type_name{
//code
}
Now we can use this trait for type_name.
Basically you define multiple functions in a trait and then implementing these functions for different types. All the functions in a trait must be implemented for a type.
Default Trait
Default trait is used to define default values for a type. It code inside the trait if not defined later for a specific type.
pub trait Default{
fn fndefault()->Self{
//code
}
fn fnanother()->Self;
}
Here only fndefault has a default code. fnanother needs to be explicitly defined for a type.
Traits as parameters
We can give a trait as a parameter to a function. This is called trait bound. Only types that implement the trait can be used as arguments. This is done by writing
fn fn_name(arg: &impl TraitName)->T{
//code
}
And there is another way of writing this known as trait bound.
fn fn_name<T: TraitName>(arg: &T)->T{
//code
}
Multiple traits can be used as bounds too with + sign.
fn fn_name<T: TraitName + TraitName2>(arg: &T)->T{
//code
}
Finally we can also use where clause.
fn name<T1,T2>(arg1: &T1, arg2: &T2)->T1
where T1: TraitName, T2: TraitName2{
//code
}
Returning Traits
We can also return a trait from a function. This is done by writing
fn fn_name()->impl TraitName{
//code
}
Especially useful when we want to return something that implements a trait but we do not know(or want to know, for example iterators and closures) the type. Only a single type can be returned even if multiple types implement the trait. It is more to do with branching statements in rust(according to me).
Using Trait Bounds to Conditionally Implement Methods
We can use trait bounds to conditionally implement methods. This is done by writing
impl<T: TraitName> struct_name<T>{
fn fn_name(&self)->T{
//code
}
}
Here the method will be implemented only if the type implements the trait(or traits). For example, we can implement a method for String type only if it implements Display trait.
impl<T: Display> String<T>{
fn fn_name(&self)->T{
//code
}
}
Validating References with Lifetimes in Rust
Lifetimes are a type of generic. They are used to tell the compiler about the relationship between the references. This is done by writing
fn fn_name<'a>(arg: &'a str)->&'a str{
//code
}