Back to Home

Traits out of the box

rust · traits · oop

Traits out of the box

There are several traits in the Rust standard library that you can declare for free using derive . These traits are sure to come in handy when declaring your own structures, they are very often found in various open-source libraries, but their implementation is generated by the compiler and may cause questions.
Often see:

#[derive(RustcEncodable, RustcDecodable, Clone, Eq, Default)]
struct Foo {
}

and don’t understand what it is and where?

The compiler itself can provide you with simple "built-in" implementations for some traits using #[derive]. Of course, these same traits can also be implemented manually if more complex behavior is required.

So, here is a sample list of traits that can be "extracted" (derive translates that way):

  • [↓] Comparison traits: Eq , PartialEq , Ord , PartialOrd
  • [↓] Clone and Copy are traits responsible for cloning and copying.
  • [↓] Hash - to calculate the hash&T
  • [↓] Default - allows you to set the "default value"
  • [↓] Debug - defines the format for outputting the structure value when using the {:?}formatter

In addition, unstable:

  • [↓] Zero , One - set the unit and zero values ​​of the "numerical" structures

All of the above traits are in the standard library std. In addition, you can find libraries made with your own hands, where there are also traits that can be extracted using #[derive]. An example is the commonly used RustcEncodable / RustcDecodable . You can implement support derivefor your traits using very tricky macros (which most likely will not work in half the cases :)), but this is beyond the scope of our article.

Consider the above traits


Compare Traits


They allow you to arrange for your structures a model of equivalence relations . Subsequently, the structures can be compared, organized, structured and organized. Let's consider them in more detail.

Eq and PartialEq


Two similar traits are responsible for what can be said foo == baror not. But why such a simple thing two different traits? And the solution is this:

PartialEq does not guarantee equality with oneself: not the fact that a == a

How can this be? For example, the numbers NaN! = NaN

use std::f32;
fn main() {
    let a = f32::NAN;
    let b = f32::NAN;
    println!("{}", a == b);
}

returns false

Imagine that we are the creator of a library that has some sort of comparison function:

fn foobar(param: T) {
  ...
}

How to choose a restriction for T? If complete equivalence is fundamental to us, and without it the function will fall apart, we will write it like this:

fn foo(param: T) {
  ...
}

but in this case, for example, it will not be possible to pass into this function f32: as we saw earlier, it only supports it PartialEq.
If such restrictions are not significant, and the function is stable for any parameters, then the semantics will look like this:

fn bar(param: T) {
  ...
}

This function with less restrictions (you can play here )

. Since Eqthere is one more restriction than PartialEq, and does not differ from it in any way that is “smaller” in terms of restrictions, the following important conclusion follows:

All structures implementing Eqshould also implementPartialEq

That is, a record #[derive(Eq)]is not competent, if not defined on the structure PartialEq- will give an error!

The easiest way to avoid this:

#[derive(Eq, PartialEq)]
enum Foo {
  ...
}

Recall our function from the previous example. According to semantics, it is defined in it as , but it turns out that it automatically accepts structures. Another property and - they cannot be automatically extracted for structures, at least one element of which does not implement , respectively. For example:fn foo()TEqPartialEq

EqPartialEqEqPartialEq

#[derive(Eq, PartialEq)]
struct Foo {
  bar: f32
}

will give an error, because, as we have already found out, it f32does not implement Eq.

Similarly, the following code will not work:

struct Foo {}
#[derive(PartialEq)]
struct Bar {
  foo: Foo
}

because our structure Foodoes not implement PartialEq(you can play here )

However, this behavior corresponds to all derivabletraits . We will see this further.

Conclusion: when writing code it will be wise to implement Eq(where possible), or at least PartialEqin all public structures - it will come in handy! And it will save the person using your code from sudden compilation errors when he decides to compare a couple of objects, or allows him to include your structure in his own, implementing the comparison, without any tambourines!

Ord and PartialOrd


Again, two similar traits, only this time responsible for more-less. However, they differ more than Eqand PartialEq:

Trait PartialOrdadds to the comparison function: > , < , > = , <= , ! = , = And sells non-strict ordering:Option

use std::cmp::Ordering;
let result = 1.0.partial_cmp(&2.0);
assert_eq!(result, Some(Ordering::Less));
let result = 1.0.partial_cmp(&1.0);
assert_eq!(result, Some(Ordering::Equal));
let result = 2.0.partial_cmp(&1.0);
assert_eq!(result, Some(Ordering::Greater));
let result = std::f64::NAN.partial_cmp(&1.0);
assert_eq!(result, None);

The trait Ordimplements "strict" ordering:Ordering

use std::cmp::Ordering;
assert_eq!(5.cmp(&10), Ordering::Less);
assert_eq!(10.cmp(&5), Ordering::Greater);
assert_eq!(5.cmp(&5), Ordering::Equal);

If you look at the dependencies of these traits, then everything becomes clear:

To implement Ord, the structure must implement Eq.
To implement PartialOrd, the structure must implementPartialEq

Indeed, is it possible to say for sure whether the “greater” or “greater or equal” value is relative to the second, if it may turn out that the structure is not equal to itself?

We also note that it Ordrequires implementation PartialOrd, therefore, all comparison operations for such structures will work guaranteed. In general, further - a complete analogy with Eqand PartialEq.
You can play with all this economy here .

Conclusion : by analogy with Eqand PartialEq- we try to implement, Ordor at least PartialOrdin all structures, where it is possible and logical. Accordingly, structures with Eqcan support and Ord, structures with PartialEq- PartialOrd.


Clone and Copy


Even in Russian, the words “cloning” and “copying” are synonyms with an indistinguishable difference. But in the Rust language, these are two different things. Now we will deal with them

Cloneallows you to create Tfrom&T using copy.
Copyallows you to copy, not " move " the value of a variable when assigning.

In short, these traits are generally responsible for different things, they are connected only by the fact that:

To implement Copy, the structure must implement Clone.

Cloneessentially a common thing. It is implemented for almost everything that can be assumed. And copying an object by returning the clone()generated copy of the object from the function is not such a big problem as such an object would not be stored in memory - create a new object, bring it to the same form, return it.

Copy- another song. In essence, the “copy” here is a one-by-one transfer of an object by byte. And not all structures support such functionality. For example, with a “dumb” copy of a pointer, two pointers are created that point to the same place, which completely contradicts the “safe” Rust ideology. Or, for example, objects that store some kind of metadata also cannot be copied byte-by-bye.

However,Copy

#[derive(Debug, Copy, Clone)]
struct Foo;
#[derive(Debug, Clone)]
struct Bar;
fn main(){    
    let foo = Foo;
    let bar = foo;
    println!("foo is {:?} and bar is {:?}", foo, bar);
    let foo = Bar;
    let bar = foo;
    println!("foo is {:?} and bar is {:?}", foo, bar);
}

The first println!will work, the second will not. And the difference is in Copy.
You can play with it here .

Conclusion : as advised in official docks, we implement it Copygenerally wherever possible. But you can’t implement it, in the general case, where Drop is implemented . Because, in the general case, if a destructor needs to be called for a structure, then it is made in such a way that you cannot copy it with a simple memory cast. Clonedoesn't seem to hurt anywhere.


Hash, Default, and Debug


The trait Hashis responsible for the possibility of taking a hash from the structure. This is a necessary condition in order to be able to compose from such structures HashMap or HashSet .

The trait Defaultis responsible for the initial (default) values ​​of the newly created structure. Implementing structures are Defaultoften required in various data libraries. The implementation of this trait is also useful for structures that characterize the parameters of a system - there are always default parameters that are too lazy to write each time :) The

trait Debugis responsible for displaying the structure in the form of a text formatted string. It is not required anywhere, except in the logging libraries, but it can be useful when debugging your own programs.

Traits are different in themselves, combined them because derivethey work the same way:

Hash,, Defaultand Debugcan be implemented in those structures all of whose members support Hash, Defaultand Debugaccordingly.

And this is not only necessary, but also sufficient. No extra hassle, traits and anything else. You can play with this farm here .

Conclusion : Hashit can be implemented in all public structures - it will not be superfluous. Defaultand Debug- to your taste, but preferably. This will help people using your code to not have problems when incorporating your structures into theirs. In short, if it’s not a pity, we recruit #[derive(...)]and pour everything from bounty there.


One and Zero


As of this writing (Rust 1.6 Stable), these traits are declared unstable. However, in all likelihood, in the future they will be able to define vector spaces for arbitrary data structures when used together with the operations of addition ( Add ) and multiplication ( Mul ). To implement these traits, your structures must have the following properties:

For One : use in conjunction with Mul - x * T::one() == x
For Zero : use in conjunction with Add -x + T::zero() == x

These traits can be declared with deriveif all members of the structures also support Oneor Zero.

Conclusion : if you do not exclude the possibility that your public structures will someday be used in vector spaces, such as participating in all sorts of algebras there, then it makes sense to implement these traits for structures. But so far, any mention of these traits causes the compiler to only swear ...

The most concluding warning


If a field that does not implement this trait enters your structure that implements some kind of Traitone derive, then your code will fall apart. And if because of this you remove the implementation of your trait, then the code will fall apart among people using your structures. Therefore, the golden rule :

The more traits, the more responsibility.

Read Next