字符串

字符串是由字符组成的连续集合。

在 Rust 中,字符类型即 char 用 Unicode 表示,因此每个字符占据 4 个字节内存空间。而字符串则是使用 UTF-8 进行编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。

在 UTF-8 编码中,英文字符通常只占 1 字节,汉字通常占 3 字节,特殊的 Unicode 字符可能占 4 字节。

字符串字面量

字面量(Literal)是程序中直接表示固定值的数据。

let s = "hello";
// 实际上,s 的类型是 &str,因此你也可以这样声明:
let s: &str = "hello";

s 是被硬编码进程序里的字符串值,该值不可变。

Rust 提供了动态字符串类型 String,该类型被分配到堆上,因此可以动态伸缩。可以使用下面的方法基于字符串字面量来创建 String 类型:

let s = String::from("hello");
s.push_str(", world!"); // 在字符串后追加字面值
println!("{}", s); // 打印 hello, world!

:: 是一种调用操作符,这里表示调用 String 类型中的 from 关联函数。

切片

字符串字面量是切片。

**切片(Slice)**是一个对数组或向量的一部分的引用,对于字符串而言,切片就是对 String 类型中某一部分的引用。切片允许你借用数组或向量的一部分,而不需要复制数据。

fn main() {
    let s = String::from("hello world");
 
    let hello = &s[0..5]; // 索引位置是 [0, 5)
    let world = &s[6..11]; // 索引位置是 [6, 11)
}

我们可以使用 .. range 序列简化字符串操作

let s = String::from("hello");
 
// 从索引 0 开始
let slice = &s[0..2];
let slice = &s[..2];        
 
// 包含最后一个字节
let slice = &s[4..s.len()];
let slice = &s[4..];
 
// 截取完整的 String 切片
let slice = &s[0..s.len()];
let slice = &s[..];

字符串切片的类型标识是 &str,因此我们可以这样声明一个函数,输入 String 类型,返回它的切片:fn first_word(s: &String) -> &str

fn main() {
    let mut s = String::from("hello world");
    let word = first_word(&s);
    s.clear(); // error!
    println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
    &s[..1]
}

字符串

Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是字符串切片。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型。

str 类型是硬编码进可执行文件,也无法被修改,但是 String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码。

String 与 &str 的转换

  • &str String

    String::from("hello,world")
    "hello,world".to_string()
  • String &str

    let s = String::from("hello,world!");
    say_hello(&s);
    say_hello(&s[..]);
    say_hello(s.as_str());

字符串没有索引

在 Rust 中,字符串的底层存储实现是通过一个字节数组(类型为 [u8],即无符号 8 位整数数组)来完成的。例如,“你好”,它的底层存储是:[0xe4, 0xbd, 0xa0, 0xe5, 0xa5, 0xbd]

因此在 Rust 中无法通过索引的方式访问字符串的某个字符或者子串。

let s1 = String::from("hello");
let h = s1[0];// `String` cannot be indexed by `{integer}`

切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上。在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:

let s = "中国人";
 let a = &s[0..2];
 println!("{}",a);

因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。 因此,当你需要对字符串做切片索引操作时,需要格外小心这一点。

操作字符串

  1. 追加

    let mut s = String::from("Hello ");
     
    // 使用 push_str() 方法追加字符串字面量
    s.push_str("rust");// Hello rust
     
    // 使用 push() 方法追加字符 char
    s.push('!');// Hello rust!

    这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰。

  2. 插入

    let mut s = String::from("Hello rust!");
     
    // 使用 insert() 方法插入单个字符 char
    s.insert(5, ',');// Hello, rust!
     
    // 使用 insert_str() 方法插入字符串字面量
    s.insert_str(6, " I like");// Hello, I like rust!

    与追加一样,不会返回新的字符串,会修改原来的字符串

  3. 替换

    let old = String::from("哈xxx");
     
    // replace 
    // 适用于 String 和 &str 类型
    // 第一个参数是要被替换的字符串,第二个参数是新的字符串
    // 返回一个新的字符串,而不是操作原来的字符串
    let new = old.replace("x", "Y");// YYY
    let new = old.replace("哈", "Y");// Yxxx
     
    // replacen
    // 适用于 String 和 &str 类型
    // 前两个参数与 replace 方法一样,第三个参数则表示替换的个数
    // 返回一个新的字符串,而不是操作原来的字符串
    let new = old.replacen("x", "Y", 1);// 哈Yxx
     
    // replace_range
    // 仅适用于 String 类型
    // 第一个参数是要替换字符串的范围,第二个参数是新的字符串
    // 直接操作原来的字符串,不会返回新的字符串
    old.replace_range(4.., "Y");// 哈Y
  4. 删除

    let mut s = String::from("rust pop 中文!");
     
    // pop
    // 删除并返回字符串的最后一个字符
    // 直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,告诉你删除字符
    let p = s.pop();
     
    // remove
    // 删除并返回字符串中指定位置的字符
    // 只接收一个参数,表示该字符起始索引位置
    // 直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串
    s.remove(0);
     
    // truncate
    // 删除字符串中从指定位置开始到结尾的全部字符
    // 直接操作原来的字符串。无返回值
    s
    .truncate(3);
    // clear
    // 清空字符串
    // 直接操作原来的字符串
    s.clear();

    这四个方法仅适用于 String 类型。

  5. 连接

    使用 + 或者 += 连接字符串。

    右边的参数必须为字符串的切片引用类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 + 时,必须传递切片引用类型。不能直接传递 String 类型。+ 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰。

    let s1 = String::from("hello,");
    let s2 = String::from("world!");
     
    // &s2会自动解引用为&str
    // 在下句中,s1的所有权被转移走了,因此后面不能再使用s1
    let s3 = s1 + &s2;

    使用 format! 连接字符串。

    format! 这种方式适用于 String&str

    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);

操作 UTF-8 字符串

// 以 Unicode 字符的方式遍历字符串
for c in "你好".chars() {
    println!("{}", c);
}
 
// 返回字符串的底层字节数组
for b in "你好".bytes() {
    println!("{}", b);
}
 
// 变长的字符串中获取子串
// https://crates.io/crates/utf8_slice

字符串转义

通过转义的方式 \ 输出 ASCII 和 Unicode 字符。

// 通过 \ + 字符的十六进制表示,转义输出一个字符
let byte_escape = "I'm writing \x52\x75\x73\x74!";
println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);
 
// \u 可以输出一个 unicode 字符
let unicode_codepoint = "\u{211D}";
let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";
 
println!(
  "Unicode character {} (U+211D) is called {}",
  unicode_codepoint, character_name
);
 
// 换行了也会保持之前的字符串格式
// 使用\忽略换行符
let long_string = "String literals
can span multiple lines.
The linebreak and indentation here ->\
<- can be escaped too!";
println!("{}", long_string);

在某些情况下,可能你会希望保持字符串的原样,不要转义

println!("{}", "hello \\x52\\x75\\x73\\x74");
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);
 
// 如果字符串包含双引号,可以在开头和结尾加 #
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
 
// 如果字符串中包含 # 号,可以在开头和结尾加多个 # 号,最多加255个,只需保证与字符串中连续 # 号的个数不超过开头和结尾的 # 号的个数即可
let longer_delimiter = r###"A string with "# in it. And even "##!"###;
println!("{}", longer_delimiter);

元组(tuple)

Rust的元组(tuple)是一个可以存储多个不同类型的值的集合,它是一个固定大小的有序集合。

元组中的元素可以是不同类型,可以包含基本类型、结构体、数组甚至是其他元组等。元组不像数组那样所有元素必须是相同类型。

fn main() {
  let tup: (i32, f64, char) = (42, 3.14, 'A'); // 定义一个包含i32, f64, 和char的元组
 
  // 访问元组中的元素
  let (x, y, z) = tup; // 解构元组
  println!("x: {}, y: {}, z: {}", x, y, z);
 
  // 也可以直接通过索引访问元组元素
  println!("The first element of the tuple is: {}", tup.0); // 输出42
  println!("The second element of the tuple is: {}", tup.1); // 输出3.14
}

结构体(struct)

Rust中的结构体(struct)是用户定义的数据类型,用于将多个相关的数据项组合在一起。这些数据项称为字段(field),结构体的字段可以是同一类型,也可以是不同类型。

结构体非常适合表示具有不同属性的数据对象,例如一个 Person,它可能有 nameageaddress 等不同字段。

基本操作

定义结构体

struct Person {
  name: String,
  age: u32,
}

创建实例

let person = Person {
  name: String::from("Alice"),
  age: 30,
};

结构体实例化时需要提供所有字段的值。字段名必须与定义时一致,除非使用 .. 来指定其余字段。

let person1 = Person {
  name: String::from("Bob"),
  age: 25,
};
 
// 使用匿名结构体实例初始化
let person2 = Person {
  name: String::from("Charlie"),
  ..person1 // 会复制person1中未初始化的字段
};

可以直接使用缩略的方式。

let name = String::from("Alice");
let age = 30;
 
let person = Person { name, age };

访问结构体

let person = Person {
  name: String::from("Alice"),
  age: 30,
};
 
println!("Name: {}, Age: {}", person.name, person.age);

如果要修改则必须要将结构体实例声明为可变的,才能修改其中的字段。

let mut person = Person {
  name: String::from("Bob"),
  age: 25,
};
 
person.name = String::from("[email protected]");

也可以使用模式匹配,用于从 person 变量中解构出 name 和 age 字段。

let person = Person {
  name: String::from("Alice"),
  age: 30,
};
 
let Person { name, age } = person;
 
println!("Name: {}, Age {}", name, age);

结构体中所有权转移

  1. 直接赋值:将一个结构体赋值给另一个变量时,所有权会转移。原来的变量将不再拥有该结构体。

    struct Person {
      name: String,
      age: u32,
    }
     
    fn main() {
      let person1 = Person {
        name: String::from("Alice"),
        age: 30,
      };
     
      let person2 = person1; // person1 的所有权转移给 person2
     
      // 下面的代码会报错,因为 person1 的所有权已经转移给了 person2
      // println!("Person1: {} is {} years old", person1.name, person1.age);
     
      // 正确
      println!("Person2: {} is {} years old", person2.name, person2.age);
    }
  2. 函数传递参数:当结构体作为函数参数传递时,默认情况下会发生所有权转移(如果不是通过引用传递)。

    fn take_ownership(person: Person) {
      println!("Taking ownership: {} is {} years old", person.name, person.age);
    }
     
    fn main() {
      let person1 = Person {
        name: String::from("Bob"),
        age: 40,
      };
     
      take_ownership(person1); // 所有权转移到函数中
     
      // 这里会报错,因为 person1 的所有权已经转移到函数内部
      // println!("{}", person1.name);
    }

元组结构体(Tuple Struct)

结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体。

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
 
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);

单元结构体(Unit-like Struct)

在 Rust 中,单元结构体是一种没有任何字段的结构体。它类似于元组中没有任何元素的单元(()),因此也被称为“无字段结构体”或“空结构体”。

struct MyUnitStruct; // 定义一个简单的单元结构体
 
let _x = MyUnitStruct;  // 创建一个 MyUnitStruct 的实例

打印结构体

在需要打印的结构体定义前添加 #[derive(Debug)]

#[derive(Debug)]
struct Rectangle {
  width: u32,
  height: u32,
}
 
fn main() {
  let rect1 = Rectangle {
    width: 30,
    height: 50,
  };
 
  println!("rect1 is {:?}", rect1);// rect1 is Rectangle { width: 30, height: 50 }
 
  println!("rect1 is {:#?}", rect1);
  // rect1 is Rectangle {
  //     width: 30,
  //     height: 50,
  // }
  
  dbg!(&rect1);
}

枚举(enum)

枚举(enum 或 enumeration)允许你通过列举可能的成员来定义一个枚举类型。枚举类型是一个类型,它会包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员的实例。

enum Direction {
  Up,
  Down,
  Left,
  Right,
}

枚举的每个选项被称为变体。每个变体可以是一个简单的标记(如 Direction::Up)。

通过 :: 操作符来访问 Direction 下的具体成员。

let Up = Direction::Up;
let Down = Direction::Down;
let Left = Direction::Left;
let Right = Direction::Right;

枚举的变体不仅仅是“标签”,它们还可以携带数据。每个变体可以有不同的数据类型和数量。

enum Message {
  Quit,
  Move { x: i32, y: i32 },
  Write(String),
  ChangeColor(i32, i32, i32),
}
 
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write(String::from("Hello!"));
let msg4 = Message::ChangeColor(255, 0, 0);

Option 枚举

Rust 标准库中提供了一个非常常用的枚举 Option<T>,用于表示一个可能存在或者不存在的值。Option 枚举有两个变体:Some(T) 表示一个值,None 表示没有值。

fn find_item(id: u32) -> Option<String> {
  if id == 1 {
    Some(String::from("Item 1"))
  } else {
    None
  }
}

数组

在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array,第二种是可动态增长的但是有性能损耗的 Vector。