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 i8
u8
16-bit i16
u16
32-bit i32
u32
64-bit i64
u64
128-bit i128
u128
arch isize
usize
isize
andusize
depends 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_100
Hexadecimal 0xff
octal 0o77
Binary 0b11011
Byte 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
true
andfalse
.characters Characters are defined using
char
keyword 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 let
is great way to deal with only one case.else
can 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
impl
blocks per structure is allowed.Any function written inside is an associated function.
Having
self
as 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
}