写给前端看的Rust教程(6)String 第一部分

1,803 阅读4分钟

原文:24 days from node.js to Rust

前言

rust字符串中遇到的第一个障碍源自结果不合预期,一个字符串字面量"Hi!"不是String的实例,下面这段代码揭示了这样的结果:

fn main() {
  print_type_of(&"Hi!");
  print_type_of(&String::new());
}

fn print_type_of<T>(_: &T) {
  println!("Type is: {}", std::any::type_name::<T>())
}
$ cargo run
Type is: &str
Type is: alloc::string::String

有趣的是,在JavaScript中字符串字面量也不是String的实例:

"Hi!" === "Hi!";
// > true

"Hi!" === new String("Hi!");
// > false

更多的例子:

typeof "Hi!";
// > "string"

typeof new String("Hi!");
// > "object"

typeof String("Hi!");
// > "string"

"Hi!" === String("Hi!");
// > true

String("Hi!") === new String("Hi!");
// > false

JavaScript基本上抹平了string字面量和String对象的差异,如果你想在string字面量调用String对象的方法,JavaScript会自动进行转化,不必你显示的做类型转化,rust中也有类似的机制

相关阅读

关于String有不少资料,官方文档的介绍不可错过,一定要仔细阅读

正文

&str

字符串字面量本质是一个字符串切片的引用,这意味着实际上它们是字符串数据的子字符串指针。rust编译器会将我们所有的字符串字面量存放在某个地方,然后将它们的值替换为指针,这就让rust优化掉了重复字符串的问题。你可以通过代码来验证这种优化现象,将下面代码中的字符串复制n次,然后查看下编译打包出来的可执行文件体积:

fn main() {
  print("TESTING:12345678901234567890123456789012345678901234567890");
}

fn print(msg: &str) {
  println!("{}", msg);
}

我们可以对生成的二进制文件用strings命令(这不是rust提供的功能)进行探查:

$ strings target/release/200-prints | grep TESTING
TESTING:12345678901234567890123456789012345678901234567890

String

String可以被改变,支持截取、缩小、增长等操作,当然这也会带来额外的成本

&str转化成String

简单来讲,用.to_owned()方法可以将&str(一个字符串切片的引用)变为有所有权的String,例如:

let my_real_string = "string literal!".to_owned();

底层实际上是这样的:

String::from_utf8_unchecked(self.as_bytes().to_owned())

注意:self就是rust中的this

这就是为什么要在学习string前先要了解ownership,字符串字面量只是一个引用,没有所有权,想变成有所有权的string,需要进行格式的转化。是不是这意味着我们要每次都使用.to_owned()呢?既是也不是,在介绍Traitsgenerics(泛型)之前你可以理解成“是的”

.to_string()、.into()、String::from()、format!()

标题列出的这些都可以将&str变成String,下面的我们将会给那些熟悉上述概念的人解释下为什么这些方法不正确

注意:rust中的``trait我们目前为止还未深入涉及到,你可以把它简单的当做是JavaScript中的mixin`(mixin pattern in JavaScript.)

为什么不用.to_string()

fn main() {
  let real_string: String = "string literal".to_string();

  needs_a_string("string literal".to_string());
}

fn needs_a_string(argument: String) {}

something.to_string()会将something转化为一个string,经常用来实现Display trait的一部分。你会看到很多文章推荐使用.to_string()、另有很多文章不推荐。推荐中的细微差异在于你对编译器帮助所希望的程度,当你的程序变得庞大,特别是开始使用generics(泛型)时,你会不可避免的需要做类型转换。一个最初是&str的变量可能会转换成其它类型,如果新值仍然实现Display那么就会有一个.to_string()方法,编译器也不会有什么异议。另一方面,.to_owned()会将引用的变量变成拥有的,通常是通过克隆实现。将一个引用的非string的变量转化为拥有所有权的string,编译器会报错。如果你觉的述差异可以接受,那么用.to_owned()还是.to_string()都无妨

注意:如果你对&str类型的变量使用.to_string(),那么Clippy会给出警告

为什么不用something.into()

fn main() {
  let real_string: String = "string literal".into();

  needs_a_string("string literal".into());
}

fn needs_a_string(argument: String) {}

something.into()通过[dest_type]::from()来努力将某个变量转变成目标类型,例如String::from(something)。如果你想将&str变成String,你会得到想要的结果,不过稍后还有更好的方法

为什么不用String::from()

fn main() {
  let real_string: String = String::from("string literal");

  needs_a_string(String::from("string literal"));
}

fn needs_a_string(argument: String) {}

String::from(something).into()更加清晰,因为你可以明确的声明希望得到的类型,但是它和.to_string()的问题一样

为什么不用format!()

fn main() {
  let real_string: String = format!("string literal");

  needs_a_string(format!("string literal"));
}

fn needs_a_string(argument: String) {}

format!()是用来做格式化的,当只是简单的创建一个字符串时这是你最不该使用的方法

注意:Clippy有一个专门的规则 clippy::useless_format

实现细节

  • .to_owned()实现,调用String::from_utf8_unchecked(self.as_bytes().to_owned())
  • String::from()实现,调用.to_owned()
  • .to_string()实现,调用String::from()
  • .into()实现,调用String::from()
  • .format!()实现,调用Display::fmt 说明

总结

&str转换为String是字符串问题的前半部分。后半部分是使用函数参数时,如果你想创建一个好用的API,那么&strString你选哪个?

更多