TheBusyTypist
TheBusyTypist

Reputation: 369

How can I own a string through reference if its lifetime is static?

In Rust I would like to give a name to a class, and this class owns this name.

Sometimes the name is passed by an String. For this case I can just simply move the ownership.

But sometimes this name is given by a static string(&str). For this case I want to refer to that string, rather than making a String from it.

My question is: how can I declare this name field in my class? What type should it be?


Some updates/background on the requirements:

Upvotes: 1

Views: 329

Answers (2)

user4815162342
user4815162342

Reputation: 155465

One option is to declare the name member as an enum that can either contain a String or an &'static str:

enum Name {
    Static(&'static str),
    Owned(String),
}

struct Class {
    name: Name,
    // ...
}

The class can then provide appropriate constructors (there will have to be two) and a get_name() method for accessing the name as string slice:

impl Class {
    pub fn new_from_str(name: &'static str) -> Class {
        Class { name: Name::Static(name) }
    }
    pub fn new_from_owned(name: String) -> Class {
        Class { name: Name::Owned(name) }
    }

    pub fn get_name(&self) -> &str {
        match self.name {
            Name::Owned(ref s) => s.as_str(),
            Name::Static(s) => s,
        }
    }
}

fn main() {
    let c1 = Class::new_from_str("foo");
    let c2 = Class::new_from_owned("foo".to_string());
    println!("{} {}", c1.get_name(), c2.get_name());
}

The other option is to use the Cow type provided by the standard library for this purpose:

use std::borrow::Cow;

struct Class {
    name: Cow<'static, str>,
}

Since Cow implements the Into trait, the constructor can now be written as a single generic function:

pub fn new<T>(name: T) -> Class
    where T: Into<Cow<'static, str>> {
    Class { name: name.into() }
}

Cow also implements the Deref trait, allowing get_name() to be written as:

pub fn get_name(&self) -> &str {
    return &self.name;
}

In both cases the name member will equal the size of the larger variant plus the space taken by the discriminator. As String is the larger type here, and it takes up three pointer sizes (the string contents is allocated separately and doesn't count), Name will take four pointer sizes in total. In case of explicit enum, the member can be made smaller still by boxing the string:

enum Name {
    Static(&'static str),
    Owned(Box<String>),
}

This will cut down the size of Name to three pointer sizes, of which one slot is used for the discriminator and the remaining two for the string slice. The downside is that it requires an additional allocation and indirection for the owned-string case - but it might still pay off if the majority of your class names come from static string slices.

Upvotes: 4

squiguy
squiguy

Reputation: 33380

What you can do to allow for a mix of types is use the Into trait. This is versatile in that it makes sure a safe conversion happens between the types.

A str slice can be converted "into" an owned String as such.

Some test code to demonstrate it:

#[derive(Debug)]
struct Test {
    name: String,
}

impl Test {
    pub fn new<T: Into<String>>(t: T) -> Test {
        Test { name: t.into() }
    }
}

fn main() {
    let t = Test::new("a");
    println!("{:?}", t);
}

Upvotes: 0

Related Questions