There's also something I want to say about this.
struct UnixError {
error: u32,
error_description: String,
}
struct StringError {
error: String,
error_description: String,
}
pub fn do_something<T>(obj: &mut T) {
let x = obj.error;
x.insert(1, 'b'); // possible on strings, but not integers
}
// somewhere else...
do_something(z);
Food for thought: In do_something
, is the x.insert
line an error?
In Python, a dynamic language, either line in the function could cause the program to die at runtime with an AttributeError
if the member doesn't exist.
In C++, it depends on what you call it with. Calling the function with a UnixError
will cause an error at compile time. Calling it with a StringError
will compile just fine. This is because C++ does its name resolution and type checking "during template instantiation" (which is to say, when it already knows the type of obj
and all its members).
Compared to the above languages, Rust is a bit of a paradigm shift. In Rust, it is always possible to determine whether a piece of code can be compiled successfully without looking at how it is used. Any input types consistent with the type signature---actual or theoretical---must all be fair game.
For this analysis, the actual types are irrelevant, so let's throw them away!
pub fn do_something<T>(obj: &mut T) {
let x = obj.error;
x.insert(1, 'b'); // possible on strings, but not integers
}
Looking at this piece of code, we see a function that promises to work with all possible types T, but in actuality expects the type to have an "error" member. Because the function fails to uphold its own promises, it fails to compile, even if it is never used.
This is where the importance of traits come in:
trait AsError {
type Error;
fn as_error(&self) -> &Self::Error;
}
pub fn do_something<T>(obj: &T)
where T: AsError
{
let x = obj.as_error();
}
By adding a trait bound to do_something
, we have restricted its promises. And (again without even looking at any types) we can see this time that this function will always compile succesfully given types that meet its signature. This is because it now only promises to work for types that implement AsError
, which provides an as_error
method.
In this manner, traits are an unavoidable part of any sort of polymorphic code in Rust---at least, outside of *shudders* macros.
(never mind std::mem::size_of()
or anything else which is made of black magic (but size_of
is the example people keep going to))