Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Trait with default implementation and required struct member

Tags:

rust

traits

I have a rust trait which is supposed to add a value to vector. In order for the add_job function to be working, it must be made sure that the vector exists when the trait is implemented for a concrete struct.

The following code fails of course, because jobs is never implemented. It's just there to demonstrate my intention:

trait Person {
    // default implementation of add job
    fn add_job(&self, job: String) {
        self.jobs.push(job)
    }
}

struct Customer {
    // add_job is used as default implementation 
    // provided by trait
}

impl Person for Customer {
    // some stuff
}

fn main() {
    let mut george = Customer {};
    george.add_job("programmer".to_string());
}

Is there a way to have a trait which also provides struct members?

Propably not, but what would be the "rustful" way to solve the above problem?

like image 655
LongHike Avatar asked Jan 25 '26 15:01

LongHike


1 Answers

Traits can't provide or require struct fields. Though there is an RFC (#1546) about allowing fields in traits. However, there isn't any unstable features allowing this (yet?).


You can still simplify what you're trying to do though. I've taken the liberty to rename and change your trait, to be able to provide more thorough examples.

Let's consider that we have a Jobs trait. Which defines various methods, that all requires the jobs: Vec<String> field.

trait Jobs {
    fn add_job(&mut self, job: String);
    fn clear_jobs(&mut self);
    fn count_jobs(&self) -> usize;
}

Using a macro

One solution could be to use a macro, which implements all those methods.

macro_rules! impl_jobs_with_field {
    ($($t:ty),+ $(,)?) => ($(
        impl Jobs for $t {
            fn add_job(&mut self, job: String) {
                self.jobs.push(job);
            }

            fn clear_jobs(&mut self) {
                self.jobs.clear();
            }

            fn count_jobs(&self) -> usize {
                self.jobs.len()
            }
        }
    )+)
}

Then you can easily reuse the code, by using the macro.

struct Person {
    jobs: Vec<String>,
}

struct Customer {
    jobs: Vec<String>,
}

impl_jobs_with_field!(Person);
impl_jobs_with_field!(Customer);
// or
impl_jobs_with_field!(Person, Customer);

Using a second HasJobs trait

Another solution could be to introduce a second HasJobs trait. Then you can use a blanket implementation for Jobs if a type implements HasJobs.

trait HasJobs {
    fn jobs(&self) -> &[String];
    fn jobs_mut(&mut self) -> &mut Vec<String>;
}

impl<T: HasJobs> Jobs for T {
    fn add_job(&mut self, job: String) {
        self.jobs_mut().push(job);
    }

    fn clear_jobs(&mut self) {
        self.jobs_mut().clear();
    }

    fn count_jobs(&self) -> usize {
        self.jobs().len()
    }
}

Now HasJobs still needs to be implemented for all your types. But if Jobs has a significant amount of methods. Then implementing HasJobs is a lot easier to deal with. Which we can also do using a macro:

macro_rules! impl_has_jobs {
    ($($t:ty),+ $(,)?) => ($(
        impl HasJobs for $t {
            fn jobs(&self) -> &[String] {
                &self.jobs
            }

            fn jobs_mut(&mut self) -> &mut Vec<String> {
                &mut self.jobs
            }
        }
    )+)
}

Then once again, you just do:

struct Person {
    jobs: Vec<String>,
}

struct Customer {
    jobs: Vec<String>,
}

impl_has_jobs!(Person);
impl_has_jobs!(Customer);
// or
impl_has_jobs!(Person, Customer);

Using Deref and DerefMut

Lastly, if Customer is always a Person, then you could change Person into a struct and use composition, i.e. add person: Person to Customer (and other types).

So first, impl Jobs for Person:

struct Person {
    jobs: Vec<String>,
}

impl Jobs for Person {
    fn add_job(&mut self, job: String) {
        self.jobs.push(job);
    }

    fn clear_jobs(&mut self) {
        self.jobs.clear();
    }

    fn count_jobs(&self) -> usize {
        self.jobs.len()
    }
}

Then now you can use Deref and DerefMut to dereference Customer to Person. Thus all Person's methods are available directly through Customer.

However, this solution only works if you only have one "trait", i.e. you can only Deref Customer to Person. You wouldn't be able to also Deref Customer to SomethingElse.

use std::ops::{Deref, DerefMut};

struct Customer {
    person: Person,
}

impl Deref for Customer {
    type Target = Person;

    fn deref(&self) -> &Self::Target {
        &self.person
    }
}

impl DerefMut for Customer {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.person
    }
}
like image 155
vallentin Avatar answered Jan 28 '26 12:01

vallentin



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!