Search code examples
rustlifetime

&str with lifetime for "After Deserialization"


I'm beating my head on lifetimes and &str.

I have a bunch of strings in my program that aren't actually static, but come close: they're loaded from a json at initialization, and should remain in scope for the entire length of the program.

I have a bunch of structs that use these. I would like to get away with just having &strs in those structs, for Copy, etc. I can do this by having lots of lifetime parameters, but those propagate everywhere and every time I've used them they've been a mistake. On the other hand, I don't want to flood my code with .to_owned()s and .clone()s either, when it's always the same strings and they're never mutated.

I can almost say these are & 'static str stings, but I don't think that's quite true and I haven't found a way to do so. However, they'd come out of "initialization" functions at the start of the program, and remain in scope past the end.

This mirrors a problem I've had in general - wanting references that refer to something whose lifetime is "long" in some sense (longer than an associated type is used for, or longer than all of the invocations of some function).

One way I can see to do this is to have a 'program lifetime that goes on basically everything, and try to maintain that that parameter always refers to the actual lifetime of the (post-initialization) program. That sounds like it's going to be a lot of boiler plate and get me int trouble down the line, though.

What would be truly ideal would be to have lifetime parameters on the module level - basically say that for everything from a given module, certain things stay in scope. I don't think there's any way to do that, is there?

Is there a better way to do this? Or a way to take a String that comes out of some arbitrary function and get a valid 'static reference to it?


Solution

  • Your usecase seems like a perfect fit for the lazy_static crate, or the once_cell crate/standard library api (more on this later).

    The basic use of lazy cell, is

    #![feature(lazy_cell)]
    
    use std::sync::LazyLock;
    use rand;
    
    static LAZY: LazyLock<String> = LazyLock::new(|| {
        rand::random::<f64>().to_string() // <-- pretend this loads json
    });
    
    fn main() {
        println!("{}", *LAZY);
        let lazy_ref: &'static str = &*LAZY;
        println!("{}", lazy_ref);
    }
    

    You'll notice the feature attribute at the top of the code. This only works in nightly rust, but it is heavily based on an existing crate that works in stable, called once_cell (replace the use with once_cell::sync::Lazy). lazy_static works similarly, but uses a macro and won't be included in the standard library, so I chose to show how to use LazyLock instead.

    You want to read and parse all your json at once, as re-parsing the json for every setting would be very slow. I think your best option is to parse your json file into a struct with a field for every string.

    #![feature(lazy_cell)]
    
    use std::sync::LazyLock;
    use std::fs::File;
    use serde_derive::{Serialize, Deserialize};
    
    #[derive(Serialize, Deserialize)]
    struct Strings {
        string1: String,
        string2: String,
    }
    
    static STRINGS: LazyLock<Strings> = LazyLock::new(|| {
        serde_json::from_reader(File::open("strings.json").unwrap()).unwrap()
    });
    
    fn main() {
        let string1: &'static str = &STRINGS.string1; //yep still 'static
        println!("{}", string1);
        //Strings.string2 has been lazily loaded
    }