kezzos
kezzos

Reputation: 3221

How to use a String instead of &str in iterator, if/else block

Would someone be kind enough to explain to me why using a String in this script does not work but &str does. Additionally how can I modify it so that it will work with String's? [version 1.2]

use std::collections::{HashMap};

fn main() { 

    let mut hash = HashMap::<&str, &str>::new();
    hash.insert("this", "value");
    let l: &str = "this is a borrowed string reference";
    // If the above line was defined as:
    //let l: String = "this is a string".to_string();

    let mut all = l.split(" ");   
    let name: &str = all.next().unwrap();

    if hash.contains_key(name) == true {
        hash.remove(name);
    } else {
        hash.insert(name, "stuff");
    }
}

Upvotes: 3

Views: 218

Answers (3)

Emil Laine
Emil Laine

Reputation: 42828

If you change l to String you get:

error: l does not live long enough

Which is true:

name is a reference to one of the substrings of l. You're possibly inserting the name reference to hash, but the lifetime of hash is longer than the lifetime of l. Therefore when the lifetime of l ends, hash would contain invalid references. This is not allowed. F.ex. if you remove the insert line, Rust is happy.

There are multiple ways to fix it, depending on your needs. One of them would be to make the lifetime of hash shorter than the lifetime of l, by instantiating hash after l:

use std::collections::{HashMap};

fn main() { 
    // let l: &str = "this is a borrowed string reference";
    // If the above line was defined as:
    let l: String = "this is a string".to_string();

    let mut hash = HashMap::<&str, &str>::new();
    hash.insert("this", "value");

    let mut all = l.split(" ");   
    let name: &str = all.next().unwrap();

    if hash.contains_key(name) == true {
        hash.remove(name);
    } else {
        hash.insert(name, "stuff");
    }
}

Or, you could store copies of the strings in the map.

Upvotes: 2

Matthieu M.
Matthieu M.

Reputation: 299790

You have a lifetime issue.

Any object inserted into hash must only reference things that outlive hash (or not reference anything at all).

However, here, you define l after hash and therefore l has a shorter lifetime. In turn this means that name which references l's buffer has a shorter lifetime than hash and thus name is not suitable to be inserted into hash (though it can be used to search/remove).

Switching the order in which l and hash are defined make it work:

use std::collections::{HashMap};

fn main() { 
    let l: String = "this is a string".to_string();

    let mut hash = HashMap::<&str, &str>::new();
    hash.insert("this", "value");

    let mut all = l.split(" ");   
    let name: &str = all.next().unwrap();

    if hash.contains_key(name) == true {
        hash.remove(name);
    } else {
        hash.insert(name, "stuff");
    }
}

If this is not possible, then use a HashMap<String, String> to avoid lifetime issues.

Upvotes: 1

DK.
DK.

Reputation: 58975

Ok, let's boil this down to just the necessities:

use std::collections::HashMap;

fn main() { 
    let mut hash = HashMap::<&str, &str>::new();
    hash.insert("this", "value");
    let l: String = "this is a borrowed string reference".to_string();
    hash.insert(&l, "stuff");
}

Compiling this gives us:

<anon>:7:18: 7:19 error: `l` does not live long enough
<anon>:7     hash.insert(&l, "stuff");
                          ^
<anon>:4:49: 8:2 note: reference must be valid for the block suffix following statement 0 at 4:48...
<anon>:4     let mut hash = HashMap::<&str, &str>::new();
<anon>:5     hash.insert("this", "value");
<anon>:6     let l: String = "this is a borrowed string reference".to_string();
<anon>:7     hash.insert(&l, "stuff");
<anon>:8 }
<anon>:6:71: 8:2 note: ...but borrowed value is only valid for the block suffix following statement 2 at 6:70
<anon>:6     let l: String = "this is a borrowed string reference".to_string();
<anon>:7     hash.insert(&l, "stuff");
<anon>:8 }

Which tells you more or less exactly what it won't work. You try to insert a borrowed pointer to the String l into the hashmap. However, the String simply doesn't live long enough.

Specifically, values are destroyed by Rust in reverse lexical order. So, when execution gets to the end of the function, Rust will deallocate l first, followed by hash. This is a problem: it means that there is a window during which hash contains pointers to destroyed data, which Rust absolutely will not allow.

The reason this works with a &str is that a string literal "like this" is not merely &str; it's actually &'static str. This means that a string literal "lives" for the entire duration of the program: it never gets destroyed, thus it's safe for the hashmap to hold a pointer to it.

The solution is to ensure the String will outlive the HashMap:

use std::collections::HashMap;

fn main() { 
    // Declare `l` here ...
    let l;
    let mut hash = HashMap::<&str, &str>::new();
    hash.insert("this", "value");

    // ... but initialise it *here*.
    l = "this is a borrowed string reference".to_string();
    hash.insert(&l, "stuff");
}

Now, hash gets destroyed first, followed by l. It's fine to leave a variable uninitialised, so long as you do initialise it prior to reading or using it.

Upvotes: 5

Related Questions