Ben Chuanlong Du's Blog

It is never too late to learn.

Trait in Rust

Things on this page are fragmentary and immature notes/thoughts of the author. Please read with your own judgement!

In [ ]:
:timing
:sccache 1

Tips and Traps

  1. Sometimes a trait bound might be too generic for your use case. If you want your function to take only a fixed number of types, make your function takes an Enum instead of a generic type.

  2. If you define a trait with a bound, e.g., trait Derived : Base, avoid defining default method implmentations with the same name. That is if the trait Base provides a default implementation f, you should avoid providing another default implementation f for Derive. If you do provide default implementations with the same name in both traits, they are uncorrelated and can cause confusion easily. For more discussions, please refer to Rust Quiz 27.

Trait

Currently, a trait in Rust cannot access fields. If a default implementation is provided, the implementation cannot refer to any struct field.

Best Practices When Defining a Default Implementation for a Trait’s Method

Allow fields in traits that map to lvalues in impl'ing type #1546

Fields in Traits

Traits and trait objects - more than just interfaces - Rust Community Stuttgart

Traits That You Should Consider Implementing for Your Structs

Other Useful Traits

PartialEq, Eq and Hash

  1. Unlike other popular programming languages, Rust introduces the trait PartialEq (for solving issues such as NaN in floating numbers). The Eq trait is a trait without any method which indicating that an object of the struct implementing Eq is comparable to itself. You always need to implement the trait PartialEq if you implement Eq. This eventually means that if you implement any of Eq or Hash, you need to implement all three of PartialEq, Eq and Hash. One simple way to do this is to use the macro #[derive(PartialEq, Eq, Hash)]. However, you can implement PartialEq without implementing Eq or Hash.

Sized vs ?Sized

Sync vs !Sync

In [ ]:

Copy vs Clone

  1. Clone means the type can be duplicated. Copy means the type can be duplicated by copying bytes. This means that Copy implies Clone, so when you implements Copy you should always implement Clone.

  2. Clone is a common trait for the ability to explicitly duplicate an object. It differs from Copy in that Copy is implicit and extremely inexpensive, while Clone is always explicit and may or may not be expensive. In order to enforce these characteristics, Rust does not allow you to reimplement Copy, but you may reimplement Clone and run arbitrary code. Since Clone is more general than Copy, you can automatically make anything Copy be Clone as well.

  1. If a type does not implement the Copy trait, it is moved when passed as a parameter. This might cause issues of "object moved". To resolve this issue, you have to implement the Copy trait. A simple way is to drive the Copy and Clonable traits using #[derive(Copy, Clone)].

Moves, copies and clones in Rust

Trait std::clone::Clone

AsRef

  1. The AsRef trait is very useful to make a function taking a generic parameter of the type T where T can be converted into the reference of a type by calling the method T.as_ref(). For example, if Card is a struct and you'd like to implement a function which accepts both &Vec<Card> and &Vec<&Card> as the parameter, you can implement it as below.
In [ ]:
fn id_sum<T>(cards: &Vec<T>) -> u64
where
    T: AsRef<Card>,
{
    cards.into_iter().map(|c| c.as_ref().id).sum()
}

A perhaps more useful example is AsRef<str>. It is well known in Rust that if you want to have a parameter accpeting a string, it is best to specify its type as &str as a String value can be converted to &str without copying.

In [14]:
fn print_str(s: &str) {
    println!("{}", s);
}
In [15]:
print_str("How are you doing?");
How are you doing?
In [17]:
let s: String = "How are you doing".into();
print_str(s.as_ref());
How are you doing

First, the above example is not generic enough as uses have to manually cast the type of value to &str. Second, what if we want to implement a function taking a vector of strings (&str, String, etc.)? AsRef comes to rescue!

In [2]:
fn count_chars<T>(strs: &Vec<T>) -> usize
where
    T: AsRef<str>
{
    strs.iter().map(|s| s.as_ref().len()).sum()
}
In [5]:
let strs = vec!["how", "are", "you"];
count_chars(&strs)
Out[5]:
9
In [6]:
let strs = vec!["how".to_string(), "are".to_string(), "you".to_string()];
count_chars(&strs)
Out[6]:
9
In [12]:
{
    let strs = vec!["how", "are", "you"];
    let strs_ref = vec![&strs[0], &strs[1], &strs[2]];
    count_chars(&strs_ref)
}
Out[12]:
9
In [13]:
{
    let strs = vec!["how".to_string(), "are".to_string(), "you".to_string()];
    let strs_ref = vec![&strs[0], &strs[1], &strs[2]];
    count_chars(&strs_ref)
}
Out[13]:
9

As you can see that the above function accepts a vector of &str, String, &&str, &String, and more.

IntoIterator - Trait for into_ter

When demonstraing the use of AsRef<T>, we has a function taking a vector of values. This is not generic enough. For the same reason that &str is preferred over String as function parameters, the slice type &[T] is preferred over &Vec<T> (as a vector reference can be converted to a slice implicitly).

In [18]:
fn count_chars<T>(strs: &[T]) -> usize
where
    T: AsRef<str>
{
    strs.iter().map(|s| s.as_ref().len()).sum()
}
In [19]:
let strs = vec!["how", "are", "you"];
count_chars(&strs)
Out[19]:
9
In [20]:
let strs = ["how", "are", "you"];
count_chars(&strs)
Out[20]:
9

Pushing generic one step further, we can make the above function taking a type implementing IntoIterator instead of &[T] (similar to AsRef<str> vs &str). This makes the function takes even more collection/iterator types as long as they implement IntoInterator.

In [22]:
fn count_chars<I, T>(strs: I) -> usize
where
    I: IntoIterator<Item = T>, 
    T: AsRef<str>
{
    strs.into_iter().map(|s| s.as_ref().len()).sum()
}
In [23]:
let strs = vec!["how", "are", "you"];
count_chars(&strs)
Out[23]:
9
In [24]:
let strs = vec!["how", "are", "you"];
count_chars(strs)
Out[24]:
9
In [25]:
let strs = ["how", "are", "you"];
count_chars(&strs)
Out[25]:
9
In [26]:
let strs = ["how", "are", "you"];
count_chars(strs)
Out[26]:
9

Trait for iter

There is no Trait in Rust for iter as it is not necessary and can be achieve by calling into_iter on a reference type.

Examples of Generic Types with Trait Bounds

The following 2 examples are identical ways to specify trait bounds.

In [31]:
:dep num-traits = "0.2.14"
In [32]:
use num_traits::AsPrimitive;
In [33]:
fn sp1<T: AsPrimitive<usize>>(major_rank: T) -> f64 {
    let r = major_rank.as_();
    if r <= 5 {
        return 0.0;
    }
    (r - 5) as f64
}
In [34]:
enum MyEnum {
     A = 0,
     B,
}
In [36]:
MyEnum::B as usize
Out[36]:
1
In [37]:
MyEnum::B as i64
Out[37]:
1
In [40]:
let x: i64 = MyEnum::B.into();
x
let x: i64 = MyEnum::B.into();
                       ^^^^ the trait `From<MyEnum>` is not implemented for `i64`
the trait bound `i64: From<MyEnum>` is not satisfied
help: the following implementations were found:
  <i64 as From<NonZeroI64>>
  <i64 as From<bool>>
  <i64 as From<i16>>
  <i64 as From<i32>>
and 4 others
In [35]:
sp1(MyEnum::A)
sp1(MyEnum::A)
    ^^^^^^^^^ the trait `AsPrimitive<usize>` is not implemented for `MyEnum`
the trait bound `MyEnum: AsPrimitive<usize>` is not satisfied
In [19]:
sp1(6usize)
Out[19]:
1.0
In [20]:
fn sp<T>(major_rank: T) -> f64 where T: AsPrimitive<usize> {
    let r = major_rank.as_();
    if r <= 5 {
        return 0.0;
    }
    (r - 5) as f64
}
In [21]:
sp(6usize)
Out[21]:
1.0

Super/Sub Trait and Generic Functions

RFC: Supertrait item shadowing #2845

[https://github.com/rust-lang/rfcs/pull/2845]

In [14]:
trait Super {
    fn foo(&self);
}

trait Sub: Super {
    fn foo(&self);
}

impl Super for i32 {
    fn foo(&self) { 
        println!("super"); 
    }
}

impl Sub for i32 {
    fn foo(&self) { 
        println!("sub"); 
    }
}

fn super_generic_fn<S: Super>(x: S) {
    x.foo();
}

fn sub_super_generic_fn<S: Sub>(x: S) {
    generic_fn(x);
}

fn sub_generic_fn<S: Sub>(x: S) {
    x.foo();
}
    x.foo();
      ^^^ multiple `foo` found
multiple applicable items in scope
help: disambiguate the associated function for candidate #1

Super::foo(x)
help: disambiguate the associated function for candidate #2

Sub::foo(x)
In [8]:
let x: i32 = 42;
x.foo()

candidate #1 is defined in an impl of the trait `Super` for the type `i32`
In [11]:
let x: i32 = 42;
sub_generic_fn(x);
super
In [47]:
let x = 8u8;
let arr: [i64; 10] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
In [50]:
(2..x as usize).map(|i| arr[i]).sum::<i64>()
Out[50]:
27
In [51]:
(2..x).map(|i| arr[i as usize]).sum::<i64>()
Out[51]:
27

Traits that You Probably Shouldn't Implement

In [ ]:

Comments