Skip to content

Latest commit

 

History

History
468 lines (343 loc) · 11.6 KB

课后8.md

File metadata and controls

468 lines (343 loc) · 11.6 KB

字符串和切片(slice)

首先我们先看看这个代码

fn main() {
  let my_name = "Pascal";
  greet(my_name);
}

fn greet(name: String) {
  println!("Hello, {}!", name);
}

这段代码会报错,因为 greet 函数需要一个 String 类型的参数,而 my_name 是一个 &str 类型的变量,这两种类型是不同的。

切片

切片(Slice)是一种数据类型,它引用了一段数据,但是并不拥有它,它是对一个已经存在的变量的引用。

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

helloworld 都是 &str 类型的变量,它们是 s 的切片。 创建切片的语法,使用方括号包括的一个序列:[开始索引..终止索引]

.. range 运算符,它会从第一个索引开始一直到最后一个索引(包括最后一个索引)。

// 索引从 0 开始
let s = String::from("hello");

let slice = &s[0..2]; // 等价于 he
let slice = &s[..2]; // 等价于 he

// 索引到结尾
let len = s.len();
let slice = &s[3..len]; // 等价于 lo
let slice = &s[3..]; // 等价于 lo

// 索引从 0 开始到结尾
let slice = &s[..]; // 等价于 hello
let slice = &s[0..len]; // 等价于 hello

字符串字面量是切片,类型是 &str,它是一个不可变的引用。

let s = "hello";
let s: &str = "hello";

什么是字符串?

  • 字符串是由字符组成的连续集合
  • Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间
  • 但是字符串不同,字符串是 UTF-8 编码,因此所占的字节数是不确定的(1-4 个字节)
  • 语言中str是类型,&str是引用,String是类型,&String是引用。String是标准库中提供的,str是语言的一部分。
  • str类型是无法修改的,String类型是可以修改的

如何转化Stringstr呢?

let s = String::from("hello");
let slice = &s[..]; // 等价于 hello

字符串索引

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

字符串的底层的数据存储格式实际上是[ u8 ],一个字节数组。对于 let hello = String::from("Hola"); 这行代码来说,Hola 的长度是 4 个字节,因为 "Hola" 中的每个字母在 UTF-8 编码中仅占用 1 个字节,但是对于下面的代码呢?

也就是说一个字符串并不是依据字符长度计算而是字节,例如中文字符占用 3 个字节,英文字符占用 1 个字节。

还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。

字符串切片

let hello = "中国人";

let s = &hello[0..2]; // error: byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`

因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃!

操作字符串

由于 String 是可变字符串,下面介绍 Rust 字符串的修改,添加,删除等常用方法:

Push

尾部追加字符

  • 在原有的字符串上追加,并不会返回新的字符串
  • 该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰
let mut s = String::from("hello");
s.push(' '); // 追加一个字符
s.push_str("world"); // 追加一个字符串

Insert

在指定位置插入字符

  • 由于字符串插入操作要修改原来的字符串
  • 字符串变量必须由 mut 关键字修饰
let mut s = String::from("hello");
s.insert(0, ' '); // 在索引 0 的位置插入一个字符
s.insert_str(0, "world"); // 在索引 0 的位置插入一个字符串

Replace

替换字符串

replace

  • 第一个参数是要被替换的字符串,第二个参数是新的字符串
  • 该方法是返回一个新的字符串,而不是操作原来的字符串。

replacen

  • 第一个参数是要被替换的字符串,第二个参数是新的字符串,第三个参数是替换的次数
  • 该方法是返回一个新的字符串,而不是操作原来的字符串。

replace_range

  • 第一个参数是要被替换的字符串的索引范围,第二个参数是新的字符串
  • 该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。
let mut s = String::from("hello");
s.replace("hello", "world"); // 替换字符串
s.replacen("hello", "world", 1); // 替换字符串,只替换第一个
s.replace_range(0..5, "world"); // 替换索引 0 到 5 的字符串

Delete

与字符串删除相关的方法有 4 个,他们分别是 pop(),remove(),truncate(),clear()。这四个方法仅适用于 String 类型。

pop

  • 删除字符串最后一个字符,并返回该字符
  • 存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None,否则返回 Some(value)
fn main() {
    let mut string_pop = String::from("rust pop 中文!");
    let p1 = string_pop.pop();
    let p2 = string_pop.pop();
    dbg!(p1);
    dbg!(p2);
    dbg!(string_pop);
}

remove

  • 删除指定索引的字符,并返回该字符
fn main() {
    let mut string_remove = String::from("测试remove方法");
    println!(
        "string_remove 占 {} 个字节",
        std::mem::size_of_val(string_remove.as_str())
    );
    // 删除第一个汉字
    string_remove.remove(0);
    // 下面代码会发生错误
    // string_remove.remove(1);
    // 直接删除第二个汉字
    // string_remove.remove(3);
    dbg!(string_remove);
}

truncate

  • 保留指定长度的字符串,超出的部分将被删除
  • 该方法是直接操作原来的字符串。无返回值。
fn main() {
    let mut string_truncate = String::from("测试truncate");
    string_truncate.truncate(3);
    dbg!(string_truncate);
}

clear

  • 清空字符串
  • 该方法是直接操作原来的字符串
fn main() {
    let mut string_clear = String::from("string clear");
    string_clear.clear();
    dbg!(string_clear);
}

连接 (Concatenate)

  1. 使用 +或是+= 运算符
  • 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰。

        fn main() {
        let string_append = String::from("hello ");
        let string_rust = String::from("rust");
        // &string_rust会自动解引用为&str
        let result = string_append + &string_rust;
        let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
        result += "!!!";
    
        println!("连接字符串 + -> {}", result);
    
    }
  1. 使用 format! 连接字符串

    format! 这种方式适用于 String 和 &str 。format! 的用法与 print! 的用法类似,

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

操作 UTF-8 字符串

//字符
let c = 'hello';
for b in c.chars() {
    println!("{}", b);
}

//字节
for b in c.bytes() {
    println!("{}", b);
}

课题

str and &str

  1. 🌟 正常情况下我们无法使用 str 类型,但是可以使用 &str 来替代
// 修复错误,不要新增代码行
fn main() {
    let s: &str = "hello, world";
}
  1. 🌟🌟 如果要使用 str 类型,只能配合 Box。 & 可以用来将 Box 转换为 &str 类型
// 使用至少两种方法来修复错误
fn main() {
    let s: Box<str> = "hello, world".into();
    greetings(&s)
}

fn greetings(s: &str) {
    println!("{}",s)
}

String

  1. 🌟
// 填空
fn main() {
    let mut s = String::from("");
    s.push_str("hello, world");
    s.push('!');

    assert_eq!(s, "hello, world!");
}
  1. 🌟🌟🌟
// 修复所有错误,并且不要新增代码行
fn main() {
    let mut s = String::from("hello");
    s.push(',');
    s.push_str(" world");
    s.push('!');

    println!("{}", s)
}
  1. 🌟🌟🌟
// 填空
fn main() {
    let s = String::from("I like dogs");
    // 以下方法会重新分配一块内存空间,然后将修改后的字符串存在这里
    let s1 = s.replace("dogs", "cats");

    assert_eq!(s1, "I like cats")
}
  1. 🌟🌟🌟
// 修复所有错误,不要删除任何一行代码
fn main() {
    let s1 = String::from("hello,");
    let s2 = String::from("world!");
    let s3 = s1 + s2;
    assert_eq!(s3,"hello,world!");
    println!("{}",s1);
}
  1. 🌟🌟🌟
// Solution 1
fn main() {
    let s = String::from("hello, world");
    greetings(s)
}

fn greetings(s:String) {
    println!("{}",s)
}

// Solution 2
fn main() {
    let s = "hello, world".to_string();
    greetings(s)
}

fn greetings(s: String) {
    println!("{}",s)
}
  1. 🌟🌟🌟
// 使用两种方法来解决错误,不要新增代码行
fn main() {
    let s = "hello, world".to_string();
    let s1: &str = s;
}

// 使用两种方法来解决错误,不要新增代码行
fn main() {
    let s = "hello, world";
    let s1: &str = s;
}
  1. 🌟
fn main() {
    // 你可以使用转义的方式来输出想要的字符,这里我们使用十六进制的值,例如 \x73 会被转义成小写字母 's'
    // 填空以输出 "I'm writing Rust"
    let byte_escape = "I'm writing Ru\x73\x74!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);

    // 也可以使用 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);
}
  1. 🌟🌟🌟 有时候需要转义的字符很多,我们会希望使用更方便的方式来书写字符串: raw string.
/* 填空并修复所有错误 */
fn main() {
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    // 修改上面的行让代码工作
    assert_eq!(raw_str, "Escapes don't work here: ? ℝ");

    // 如果你希望在字符串中使用双引号,可以使用以下形式
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // 如果希望在字符串中使用 # 号,可以如下使用:
    let  delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", delimiter);

    // 填空
    let long_delimiter = r####"Hello, \"##\"####;
    assert_eq!(long_delimiter, "Hello, \"##\"")
}
  1. 🌟🌟 你无法通过索引的方式去访问字符串中的某个字符,但是可以使用切片的方式 &s1[start..end] ,但是 start 和 end 必须准确落在字符的边界处.
fn main() {
    let s1 = String::from("hi,中国");
    let h = s1[0..1]; // 修改当前行来修复错误,提示: `h` 字符在 UTF-8 格式中只需要 1 个字节来表示
    assert_eq!(h, "h");

    let h1 = &s1[3..6];// 修改当前行来修复错误,提示: `中` 字符在 UTF-8 格式中需要 3 个字节来表示
    assert_eq!(h1, "中");
}
  1. 🌟🌟🌟
fn main() {
    // 填空,打印出 "你好,世界" 中的每一个字符
    for c in "你好,世界".chars() {
        println!("{}", c)
    }
}