Rust枚举中str/String值的最佳实践是什么?

5

我有一个看起来非常愉悦的东西,但我担心它的影响:

#[derive(Eq, PartialEq, Debug)]
pub enum SmtpHost {     
    DOMAIN(String),
    IPV4(Ipv4Addr),
    IPV6(Ipv6Addr),
    UNKNOWN { label:String, literal:String },
}

我正在使用PEG语法填充文本,它给了我&str,因此所有字符串调用都像这样 - SmtpHost::Domain(s.to_string())

我希望这些枚举类型是解析器的结果,例如smtp_parser::host< 'input >(s: 'input & str) -> SmtpHost

我也尝试了引用方法,但很快就变得笨拙不堪:

#[derive(Eq, PartialEq, Debug)]
pub enum SmtpHost<'a > {     
    DOMAIN(&'a str),
    IPV4(Ipv4Addr),
    IPV6(Ipv6Addr),
    UNKNOWN { label:&'a str, literal:&'a str },
}

我觉得这个问题可以是两个方案中的一个,但你知道更好,告诉我吧 :o)

这是我的参考研究项目


5
顺便提一下,Rust中的枚举变量通常采用CamelCase而不是FULLCAPS书写。如果你有特殊原因需要这样做,我道歉。但如果没有,最好遵循约定 :) - Kroltan
1
&str is not owned, so if you want to be able to keep your tokens around after the parser finishes, you probably have to use String - zstewart
@zstewart,我可以理解为你已经回答了我的问题吗?我希望这些枚举类型是解析器的结果,就像smtp_parser::host<'input>(s: &'input str) -> SmtpHost一样。 - Robert Cutajar
@RobertCutajar-Robajz 如果您使用带有生命周期参数的 &str 返回 SmtpHost,那么返回的 SmtpHost 的生命周期必须是 <= 'input,因为签名必须是:smtp_parser::host<'input>(s: &'input str) -> SmtpHost<'input>。除非您有特定的原因需要这些值借用不同字符串的一部分;在我看来,它们应该拥有匹配的值。因此,请使用 String - zstewart
@RobertCutajar-Robajz 这是 Rust,可变/不可变通常不是类型的属性。String 是否可变取决于它是否存储在可变变量中。如果您使用 let host = smtp_parser::host(s);,那么 String 也将是不可变的。从技术上讲,可以构造使用隐私来始终强制不可变性的类型,但这通常是不必要的,因为通常可以在使用站点而不是声明处控制可变性。 - zstewart
显示剩余3条评论
2个回答

5
&strString之间的关键区别在于所有权。 String是拥有所有权的,但&str是借用的。如果您存储一个&str值,则容器的生命周期将仅限于所借字符串的生命周期。
如果您的解析器生成器生成了一个带有以下签名的解析函数:
smtp_parser::host<'a>(&'a str) -> SmtpHost<'a>

当它向您传递一个&str以用于构造解析树/解析值时,它通常会给您提供输入的子字符串。这意味着您在SmtpHost枚举中存储的&str必须具有比原始输入字符串更短的生命周期。实际上,在签名中可以看到这一点;输入字符串和输出SmtpHost都具有生命周期参数'a

这意味着您得到的SmtpHost的生命周期不能超过生成它所使用的输入。如果输入是字符串常量&'static str,那可能没问题,但如果您从标准输入或读取文件中获取输入,则无法在所拥有输入字符串之后返回SmtpHost

例如,假设您想声明一个函数,该函数从标准输入中解析SmtpHost

fn read_host<'a>() -> SmtpHost<'a> {
    let mut line = String::new();
    let stdin = io::stdin();
    stdin.lock().read_line(&mut line).expect("Could not read line");
    smtp_parser::host(&line)
}

您会收到一个错误,类似于“行寿命不够长”的提示。这里是 Rust playground 上的一个简单示例

当您只是从其他地方借用一个不需要超过源的值时,应使用&str。当您需要拥有该值的所有权时,应使用String

对于更复杂的情况,您需要拥有一个拥有的值,但希望能够在多个位置使用它而不必拥有许多副本,则可以使用Rc<T>Rc<RefCell<T>。但在您的情况下,SmtpHost应该只拥有它存储的字符串的所有权。


感谢@zstewart,顺便说一句,Rust Playground的链接无法通过我的浏览器,请尝试缩短链接,但我明白你的意思。 - Robert Cutajar
@RobertCutajar-Robajz 嗯,奇怪的是,它对我来说似乎是有效的。我最初尝试使用 Rust playground 的链接缩短器,但 StackOverflow 显然不喜欢链接缩短器。不幸的是,完整的 URL 包括示例的完整代码...也许它比您的浏览器可以处理的更长。 - zstewart

3
如果您想进行解析而不复制,那么您需要的签名是:
// Notice that the 'input goes after the &. Syntax.
fn smtp_parser::host<'input>(s: &'input str) -> SmtpHost<'input>;

那么您可以像这样定义枚举类型:

然后你可以像这样定义你的枚举:

#[derive(Eq, PartialEq, Debug)]
pub enum SmtpHost<'input> {
    DOMAIN(&'input str),
    IPV4(Ipv4Addr),
    IPV6(Ipv6Addr),
    UNKNOWN { label: &'input str, literal: &'input str },
}

另一方面,如果在某些情况下这种做法太麻烦了,可以使用“Cow”(复制时写入)类型,实现两者的方式:
use std::borrow::Cow;
#[derive(Eq, PartialEq, Debug)]
pub enum SmtpHost<'input> {
    DOMAIN(Cow<'input, str>),
    IPV4(Ipv4Addr),
    IPV6(Ipv6Addr),
    UNKNOWN { label: Cow<'input, str>, literal: Cow<'input, str> },
}

如果主机部分有时可以直接从输入中使用,但有时需要更改才能使用,则应执行以下操作。


感谢 @notriddle 的奶牛。 - Robert Cutajar

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接