Maxie
Maxie

Reputation: 98

How can I set a struct field value by string name?

Out of habit from interpreted programming languages, I want to rewrite many values based on their key. I assumed that I would store all the information in the struct prepared for this project. So I started iterating:

struct Container {
    x: String,
    y: String,
    z: String
}
impl Container {
    // (...)    
    fn load_data(&self, data: &HashMap<String, String>) {
        let valid_keys = vec_of_strings![ // It's simple vector with Strings
            "x", "y", "z"
        ] ;
        for key_name in &valid_keys {
            if data.contains_key(key_name) {
                self[key_name] = Some(data.get(key_name);
                // It's invalid of course but
                // I do not know how to write it correctly.
                // For example, in PHP I would write it like this:
                // $this[$key_name] = $data[$key_name];
            }
        }
    }
    // (...)
}

Maybe macros? I tried to use them. key_name is always interpreted as it is, I cannot get value of key_name instead.

How can I do this without repeating the code for each value?

Upvotes: 4

Views: 3400

Answers (1)

Shepmaster
Shepmaster

Reputation: 431889

With macros, I always advocate starting from the direct code, then seeing what duplication there is. In this case, we'd start with

fn load_data(&mut self, data: &HashMap<String, String>) {
    if let Some(v) = data.get("x") {
        self.x = v.clone();
    }
    if let Some(v) = data.get("y") {
        self.y = v.clone();
    }
    if let Some(v) = data.get("z") {
        self.z = v.clone();
    }
}

Note the number of differences:

  1. The struct must take &mut self.
  2. It's inefficient to check if a value is there and then get it separately.
  3. We need to clone the value because we only only have a reference.
  4. We cannot store an Option in a String.

Once you have your code working, you can see how to abstract things. Always start by trying to use "lighter" abstractions (functions, traits, etc.). Only after exhausting that, I'd start bringing in macros. Let's start by using stringify

if let Some(v) = data.get(stringify!(x)) {
    self.x = v.clone();
}

Then you can extract out a macro:

macro_rules! thing {
    ($this: ident, $data: ident, $($name: ident),+) => {
        $(
            if let Some(v) = $data.get(stringify!($name)) {
                $this.$name = v.clone();
            } 
        )+
    };
}

impl Container {
    fn load_data(&mut self, data: &HashMap<String, String>) {
        thing!(self, data, x, y, z);
    }
}

fn main() {
    let mut c = Container::default();
    let d: HashMap<_, _> = vec![("x".into(), "alpha".into())].into_iter().collect(); 

    c.load_data(&d);

    println!("{:?}", c);
}

Full disclosure: I don't think this is a good idea.

Upvotes: 8

Related Questions