Rust High Performance
上QQ阅读APP看书,第一时间看更新

Compile-time checks

Rust has an amazing type system. It's so powerful that it is Turing-complete by itself. This means that you can write very complex programs just by using Rust's type system. This can help your code a lot, since the type system gets evaluated at compile time, making your runtime much faster.

Starting from the basics, what do we mean by the type system? Well, it means all those traits, structures, generics, and enums you can use to make your code very specialized at runtime. An interesting thing to know is the following: if you create a generic function that gets used with two different types, Rust will compile two specific functions, one for each type.

This might seem like code duplication but, in reality, it is usually faster to have a specific function for the given type than to try to generalize a function over multiple ones. This also allows for the creation of specialized methods that will take into account the data they are using. Let's see this with an example. Suppose we have two structures and we want them to output a message with some of their information:

struct StringData {
data: String,
}

struct NumberData {
data: i32,
}

We create a trait that we will implement for them that will return something that can be displayed in the console:

use std::fmt::Display;

trait ShowInfo {
type Out: Display;
fn info(&self) -> Self::Out;
}

And we implement it for our structures. Note that I have decided to return a reference to the data string in the case of the StringData structure. This simplifies the logic but adds some lifetimes and some extra referencing to the variable. This is because the reference must be valid while StringData is valid. If not, it might try to print non-existent data, and Rust prevents us from doing that:

impl<'sd> ShowInfo for &'sd StringData {
type Out = &'sd str;
fn info(&self) -> Self::Out {
self.data.as_str()
}
}

impl ShowInfo for NumberData {
type Out = i32;
fn info(&self) -> Self::Out {
self.data
}
}

As you can see, one of them returns a string and the other returns an integer, so it would be very difficult to create a function that allows both of them to work, especially in a strongly-typed language. But since Rust will create two completely different functions for them, each using their own code, this can be solved thanks to generics:

fn print<I: ShowInfo>(data: I) {
println!("{}", data.info());
}

In this case, the println! macro will call to the specific methods of the i32 and &str structures. We then simply create a small main() function to test everything, and you should see how it can print both structures perfectly:

fn main() {
let str_data = StringData {
data: "This is my data".to_owned(),
};
let num_data = NumberData { data: 34 };

print(&str_data);
print(num_data);
}

You might be tempted to think that this is similar to what languages such as Java do with their interfaces, and, functionally, it is. But talking about performance, our topic in this book, they are very different. Here, the generated machine code will effectively be different between both calls. One clear symptom is that the print() method gets ownership of the value it receives, so the caller must pass this in the registers of the CPU. Both structures are fundamentally different though. One is bigger than the other (containing the string pointer, the length, and capacity), so the way the call is done must be different.

So, great, Rust does not use the same structure for traits as Java does for interfaces. But why should you care? Well, there are a number of reasons, but there is one that will probably show you what this accomplishes. Let's create a state machine.