Rust lang series episode #11— generics (#rust-series)
Hello everyone, here is new a rust series episode. It's starting to be a little bit more interesting (hopefully also for you), today we will discuss generics. We will also utilize a lot of knowledge from previous episodes so make sure you understand it well and if not take a step back and make sure you understand at least few previous episodes. Generics makes, simply said, your constructions more general from types point of view. I must admit I was a bit "scared" of generics in languages like C++ for several years considering them like something super-advanced but there is absolutely no reason for that. Once you grasp the idea behind, it's actually pretty simple.
Let's start with this quite stupid function that just returns first of two given parameters.
fn first(a: i32, b: i32) -> i32 {
a
}
fn main() {
first(1,2);
first("abc","cde"); // this will not work
}
The function is quite limited with parameter types. Only 32-bit integer can be used. If you try to pass anything else compiler will complain. If you wish similar function that will work with 16-bit integer or string you will need to create new function with that parameter types. But with generic types you can boost the original one to do it. Generic types are defined with something like <T>
. T can be anything else and there can be also multiple generic types but let's start with simple case.
fn first<T>(a: T, b: T) -> T {
a
}
fn main() {
let ret_int = first(1,2);
let ret_str = first("abc","cde");
let ret_float = first(1.2, 2.3);
println!("{}", ret_int);
println!("{}", ret_str);
println!("{}", ret_float);
}
# output
1
abc
1.2
You see, we have just one function and still it's pretty universal, it works with integers, strings and float types (or in this case with all other types). But it will not be that easy, limitations will come as soon as your generic function will start using other functions requiring proper types.
Just try to put for example println! into function to print first parameter and you will see. Rust compiler will complain because your generic variable is just...ehm too generic :). So don't worry compiler will not let you go until you have it right.
fn first<T>(a: T, b: T) -> T {
println!("{}",a);
a
}
So this is a version which will not wrok. We just need to "limit" parameter type it a bit. We have already discussed traits and we know to print with {} pattern we need to have variable implementing std::fmt::Display trait. And so we can limit generic function and then we can print inside. This we can do with T: trait_name syntax.
fn first<T: std::fmt::Display>(a: T, b: T) -> T {
println!("{}",a);
a
}
fn main() {
let ret_int = first(1,2);
let ret_str = first("abc","cde");
let ret_float = first(1.2, 2.3);
}
# output
1
abc
1.2
Woohoo! It works now and function is printing variable name and is still as generic as possible.
Generics in other data types
You can use generics across other data types, like struts, enums, etc. See this examples:
Generic struct
struct Position<T> {
x: T,
y: T
}
fn main() {
let pos_int = Position{x:10, y:0};
let pos_float = Position{x: 1.1, y: 2.2};
}
We have one struct and we were easily able to define multiple instances with various types.
Generic enum
We can also use generic types in enums.
enum Number<T> {
Countable(T),
Infinity
}
fn main() {
let number_int = Number::Countable(1);
let number_float = Number::Countable(2.1);
let number_infinity = Number::Infinity::<i32>;
}
Note that Rust requires to define generic type even though it's not used (Infinity case).
Multiple generic functions
Our examples used just one generic function but we can use multiple of them if needed. Check given code below. You can add additional generic variables just after a comma.
struct Position<X,Y> {
x: X,
y: Y
}
Generic traits and impl
You can also use generic in traits and implementation definitions. We will implement Movable trait on struct from previous piece of code.
struct Position<T> {
x: T,
y: T
}
trait Movable<T> {
fn do_move(&mut self, x: T, y: T);
}
impl<T> Movable<T> for Position<T> {
fn do_move(&mut self, x: T, y: T) {
self.x = x;
self.y = y;
}
}
fn main() {
let mut p_int : Position<i32> = Position{x:0,y:0};
p_int.do_move(10,20);
let mut p_float : Position<f32> = Position{x:0.1, y:0.2};
p_float.do_move(0.3,2.8);
}
Postfix
That's all for today, thanks for all appreciations, feel free to comment and point out possible mistakes (first 24 hours works the best but any time is fine). Jesus bless your programming skills, use them wisely and see you during next episodes.
And congratulations, acquiring knowledge from all the episodes so far increased your rank to Rust apprentice.
Meanwhile you can also check the official documentation to find more about generics: