rust笔记-字符串

字符串类型与切片

切片并不是 Rust 独有的概念,在 Go 语言中就非常流行,它允许你引用集合中部分连续的元素序列,而不是引用整个集合。
我们来看个例子:

fn main() {
    let s = String::from("hello world");
    let hello = &s[0..5];
    let world = &s[6..11];
    println!("{}, {}", hello, world);
}

这里我们创建了一个字符串变量 s,然后通过 s[0..5] 的方式截取了字符串的前 5 个字符,也就是 “hello”,接着通过 s[6..11] 的方式截取了字符串的后 5 个字符,也就是“world”。这就是使用切片的方式截取字符串的方法,切片是字符串的一部分,这就是创建切片的语法,使用方括号包括的一个序列:[开始索引..终止索引],其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引 - 开始索引 的方式计算得来的。

在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:

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

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

字符串

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

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

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

除了 String 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsString, OsStr, CsString 和 CsStr 等,注意到这些名字都以 String 或者 Str 结尾了吗?它们分别对应的是具有所有权和被借用的变量。

String与&str的转换

从 &str 类型生成 String 类型

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

String 类型转为 &str 类型直接取引用即可,如下例子:

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

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

这种灵活用法是因为 deref 隐式强制转换,Deref 特征后续在深入了解

字符串索引

字符串索引与切片索引类似,都是通过 [start_index..end_index] 的方式,但是字符串索引的边界是字符,而不是字节。字符串的底层数据存储是一个字节数组[u8],对于let = hello = String::from("hello"); 来说,hello的长度是5,因为每个字母在UTF-8编码中占一个字节,所以hello的长度是5。但是对于像let hello = String::from("你好呀"); 这样的字符串,hello的长度是9,因为汉字在UTF-8编码中占3个字节(大部分汉字是3个字节)。所以在这个字符串访问的时候使用&hello[0] 没有任何意义,因为你取不到 中 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的值。还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。

字符串切片

字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:

fn main() {
    let hello = String::from("你好");
    let slice = &hello[0..2];
    println!("{}", slice);
}

这个程序会报错,因为切片的索引是通过字节来进行,此时切片索引落在了“你”字内部。

字符串的操作

push

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

fn main() {
    let mut s = String::from("hello");
    s.push_str(",world");
    s.push('!');
    println!("{}", s);
}
insert

insert() 方法插入单个字符 char,也可以使用 insert_str() 方法插入字符串字面量,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串)。

fn main() {
    let mut s = String::from("hello");
    s.insert_str(5, "world");
    s.insert(5, ',');
    println!("{}", s);
}
replace

replace() 方法替换字符串的一部分,第一个参数是字符(串)替换位置的索引,第二个参数是要替换的字符(串),该方法会替换所有匹配到的字符串,返回一个新的字符串,而不是原来操作的字符串。该方法可适用于 String 和 &str 类型。

fn main() {
    let s = String::from("hello");
    let r1 = s.replace("l", "r");
    let r2 = s.replace("l", "rr");
    println!("{}, {}, {}", s, r1, r2);
}
replacen

replacen() 方法可以指定替换的次数,第一个参数是字符(串)替换位置的索引,第二个参数是要替换的字符(串),第三个参数是替换的次数,它跟replace() 方法一样,也是替换所有匹配到的字符串,返回一个新的字符串,而不是原来操作的字符串。该方法可适用于 String 和 &str 类型。

fn main() {
    let s = String::from("hello");
    let r1 = s.replacen("l", "r", 1);
    println!("{}, {}", s, r1);
}
replacereplace_range

replacereplace_range() 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。该方法仅适用于 String 类型。

fn main() {
    let mut s = String::from("hello");
    s.replace_range(1..3, "rr");
    println!("{}", s);
}
pop()

pop() 方法会删除字符串的最后一个字符,并返回该字符。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。返回值为一个option类型,如果字符串为空,则返回 None,否则返回 Some(char)。实例代码如下:

fn main() {
    let mut s = String::from("hello");
    let ch = s.pop();
    dbg!(ch);
    dbg!(s);
}

上面例子中使用了dbg! 这个宏,dbg! 宏可以将变量打印出来,并会输出变量信息。相较于使用println!在调试使用时更加方便,输出也更清晰,下面是示例函数的输出:

ch = Some('o')
s = "hell"
remove

remove() 方法会删除字符串中第一个匹配到的字符串,并返回一个新的字符串。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。如果参数给的位置不是合法的边界,会发生错误。

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

truncate() 方法会删除字符串中从开始位置到结束位置的字符,并返回一个新的字符串。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。如果参数给的位置不是合法的边界,会发生错误。

fn main() {
    let mut string_truncate = String::from("测试truncate方法");
    println!(
        "string_truncate 占 {}个字节",
        std::mem::size_of_val(string_truncate.as_str())
    );
    string_truncate.truncate(3);
    dbg!(string_truncate);
}
clear

clear() 方法会删除字符串中的所有字符,并返回一个新的字符串。该方法仅适用于 String 类型。该方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。

fn main() {
    let mut string_clear = String::from("测试clear方法");
    println!(
        "string_clear 占 {}个字节",
        std::mem::size_of_val(string_clear.as_str())
    );
    string_clear.clear();
    dbg!(string_clear);
}
连接字符串

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

字符串的连接

字符串的连接可以使用 + 操作符:

fn main() {
    let string_add = String::from("测试");
    let test: String = String::from("test string");
    let result = string_add + &test;
    dbg!(result);
}
format宏

format! 宏是标准库中提供的宏,它用于格式化字符串。format! 宏的语法如下:

fn main() {
    let s1 = String::from("hello");
    let s2 = "rust";
    let res = format!("{} {}", s1, s2);
    dbg!(res);
}
字符串转义

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

fn main() {
    // 通过 \ + 字符的十六进制表示,转义输出一个字符
    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);
}

可以使用在字符串前后加“#”保持字符串的格式,比如:

fn main() {
    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);

    // 如果还是有歧义,可以继续增加,没有限制
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

字符串的遍历

以字符方式遍历字符串,可以使用 for 循环配合char方法:

fn main() {
    for c in "字符串".chars() {
        println!("{}", c);
    }
}

以字节方式遍历字符串,可以使用 for 循环配合as_bytes方法,这种方式是返回字符串的底层字节数组:

fn main() {
    for b in "字符串".as_bytes() {
        println!("{}", b);
    }
}

字符串的截取

标准库暂时没有提供字符串的截取方法,但是crates.io 上搜索 utf8 来寻找想要的功能。可以用这个库:https://crates.io/crates/utf8_slice

posted @   NoodlesYang  阅读(94)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· .NET Core 中如何实现缓存的预热?
· 三行代码完成国际化适配,妙~啊~
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
点击右上角即可分享
微信分享提示