9

I am receiving data in the form of a string vector, and need to populate a struct using a subset of the values, like this:

const json: &str = r#"["a", "b", "c", "d", "e", "f", "g"]"#;

struct A {
    third: String,
    first: String,
    fifth: String,
}

fn main() {
    let data: Vec<String> = serde_json::from_str(json).unwrap();
    let a = A {
        third: data[2],
        first: data[0],
        fifth: data[4],
    };
}

This doesn't work because I'm moving values out of the vector. The compiler believes that this leaves data in an uninitialized state that can cause problems, but because I never use data again, it shouldn't matter.

The conventional solution is swap_remove, but it is problematic because the elements are not accessed in reverse order (assuming the structure is populated top to bottom).

I solve this now by doing a mem::replace and having data as mut, which clutters this otherwise clean code:

fn main() {
    let mut data: Vec<String> = serde_json::from_str(json).unwrap();
    let a = A {
        third: std::mem::replace(&mut data[2], "".to_string()),
        first: std::mem::replace(&mut data[0], "".to_string()),
        fifth: std::mem::replace(&mut data[4], "".to_string())
    };
}

Is there an alternative to this solution that doesn't require me to have all these replace calls and data unnecessarily mut?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Listerone
  • 1,381
  • 1
  • 11
  • 25
  • 7
    @SvenMarnach empty strings don't allocate, however `"".to_string()` is probably not the best way to create one. I'd go with `std::mem::replace(&mut data[2], String::new())`, which is probably as good as its gets. If the vector allowed to moved out its elements, it would have to keep track of which one are still live to deallocate them and that could end-up being worse for performance. – mcarton Aug 28 '19 at 08:04
  • @mcarton You are right, my bad. – Sven Marnach Aug 28 '19 at 11:35
  • As someone who is in the process of learning Rust, why would this be preferable over `data.remove(index)` is it that the indexes will shift as we remove data? – evading Aug 28 '19 at 12:27
  • @evading it's not *just* that the indexes will shift (which is annoying) but also that each `remove` call has to move all the subsequent data in the vector. It's a performance issue (albeit most likely a tiny one). – Shepmaster Aug 28 '19 at 14:11

3 Answers3

6

I've been in this situation, and the cleanest solution I've found was to create an extension:

trait Extract: Default {
    /// Replace self with default and returns the initial value.
    fn extract(&mut self) -> Self;
}

impl<T: Default> Extract for T {
    fn extract(&mut self) -> Self {
        std::mem::replace(self, T::default())
    }
}

And in your solution, you can replace the std::mem::replace with it:

const JSON: &str = r#"["a", "b", "c", "d", "e", "f", "g"]"#;

struct A {
    third: String,
    first: String,
    fifth: String,
}

fn main() {
    let mut data: Vec<String> = serde_json::from_str(JSON).unwrap();
    let _a = A {
        third: data[2].extract(),
        first: data[0].extract(),
        fifth: data[4].extract(),
    };
}

That's basically the same code, but it is much more readable.


If you like funny things, you can even write a macro:

macro_rules! vec_destruc {
    { $v:expr => $( $n:ident : $i:expr; )+ } => {
        let ( $( $n ),+ ) = {
            let mut v = $v;
            (
                $( std::mem::replace(&mut v[$i], Default::default()) ),+
            )
        };
    }
}

const JSON: &str = r#"["a", "b", "c", "d", "e", "f", "g"]"#;

#[derive(Debug)]
struct A {
    third: String,
    first: String,
    fifth: String,
}

fn main() {
    let data: Vec<String> = serde_json::from_str(JSON).unwrap();

    vec_destruc! { data =>
        first: 0;
        third: 2;
        fifth: 4;
    };
    let a = A { first, third, fifth };

    println!("{:?}", a);
}
Boiethios
  • 38,438
  • 19
  • 134
  • 183
4

In small cases like this (also seen in naïve command line argument processing), I transfer ownership of the vector into an iterator and pop all the values off, keeping those I'm interested in:

fn main() {
    let data: Vec<String> = serde_json::from_str(json).unwrap();
    let mut data = data.into_iter().fuse();

    let first = data.next().expect("Needed five elements, missing the first");
    let _ = data.next();
    let third = data.next().expect("Needed five elements, missing the third");
    let _ = data.next();
    let fifth = data.next().expect("Needed five elements, missing the fifth");

    let a = A {
        third,
        first,
        fifth,
    };
}

I'd challenge the requirement to have a vector, however. Using a tuple is simpler and avoids much of the error handling needed, if you have exactly 5 elements:

fn main() {
    let data: (String, String, String, String, String) = serde_json::from_str(json).unwrap();

    let a = A {
        third: data.2,
        first: data.0,
        fifth: data.4,
    };
}

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 2
    I like your solution, because with `Option::ok_or`, one can do a proper error handling. – Boiethios Aug 28 '19 at 15:40
  • @FrenchBoiethios certainly. I used `expect` because the original code also panicked if there weren't enough values. – Shepmaster Aug 28 '19 at 15:42
  • This does not work for me because in reality I am receiving a json file with an array of thousands of strings and I have to extract select pieces for further processing. It is not feasible to order the elements nor is it feasible to have a thousand-tuple. – Listerone Aug 29 '19 at 02:52
1

Another option is to use a vector of Option<String>. This allows us to move the values out, while keeping track of what values have been moved, so they are not dropped with the vector.

let mut data: Vec<Option<String>> = serde_json::from_str(json).unwrap();
let a = A {
    third: data[2].take().unwrap(),
    first: data[0].take().unwrap(),
    fifth: data[4].take().unwrap(),
};
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841