Rust 09: 字符串详解(String、&str、内存布局、常用方法)

字符串字面量

字符串字面量和全局变量、static变量一样位于程序运行之后虚拟地址空间中的代码区
关于虚拟地址空间,之前有一小节详细分析过:Rust 06: 变量分配在堆还是栈上 + 虚拟地址空间

const g_array: [i32; 5] = [10; 5];
static G_VAR: i32 = 1000;
#[test]
fn test09_string() {
    let s: &str = "test string";
    //字符串字面量,位于代码区的ROData段
    println!("&str: {:p}", s);//&str: 0x7ff77e4c6b88
    println!("{:p}", &g_array);//位于data段:0x7ff6c5fc6bb8
    println!("{:p}", &G_VAR);//位于data段:0x7ff77e4c6200
}

字符串字面量位于代码区的ROData段,是只读(Read Only)的,在Rust代码中字符串字面量会被处理成字符串切片类型,即&str。如果想要对字符串进行追加等操作,必须先将ROData段的字符串字面量复制一份到上,构造一个String类型出来。

字符串(String)

String内部其实是一个Vec,是一个可变长度的类型,末尾可以追加字符。String类型和Vec<u8>类型占用24个字节。

pub struct String {
    vec: Vec<u8>,
}
println!("size: {}", std::mem::size_of::<Vec<u8>>());//24
println!("size: {}", std::mem::size_of::<String>());//24

String对象的内存结构,也就是Vec<u8>的内存结构,为 8(ptr) + 8(capacity) + 8(length)


从字符串字面量构造出一个堆上的String有多种方式:

  1. "xxx".to_owned()方法内部掉clone(),将字符串字面量从ROData区复制一份到堆上,并返回堆上数据的所有权;
  2. String::from("xxx")内部实际调用的是"xxx".to_owned();
  3. "xxx".to_string()内部实际调用的是String::from(“xxx”);
    也就是说,下面3种方式最终都是调用了clone()
let s1: String = "Hello Rust!".to_owned();
let s2: String = String::from("Hello Rust!");
let s3: String = "Hello Rust!".to_string();
assert_eq!(s1,s2);
assert_eq!(s2,s3);
assert_eq!(s1,s3);
println!("s1: {:p}", &s1);//s1: 0x32a62fec08
println!("s2: {:p}", &s2);//s2: 0x32a62fec20
println!("s3: {:p}", &s3);//s3: 0x32a62fec38

可以看到,&s1、&s2、&s3是3个虚拟地址比较小,互不相同的栈地址。但是,实际上他们最终指向的字符串内容本身是位于堆上。
根据以上信息,我们尝试画一下let s: String = String::from("Hello");的内存布局:
String内存布局

字符串切片(&str)

let s1: String = "Hello Rust!".to_owned();
let s2: &String = &s1;
let s3: &str = "Hello Rust!";
let slice1: &str = &s1[0..5];//对String类型进行切片引用
let slice2: &str = &s2[0..5];//对&String类型进行切片引用
let slice3: &str = &s3[0..5];//对&str切片类型,进行切片引用
println!("{}", slice1);
println!("{}", slice2);
println!("{}", slice3);

String和&str的内存布局

还是先来做个小测试,验证一些想法:

// String内部其实是一个Vec<u8>,是一个可变长度的类型,末尾应该可以追加字符。
println!("size: {}", std::mem::size_of::<Vec<u8>>());//24
println!("size: {}", std::mem::size_of::<String>());//24

let s1: String = "Hello Rust!".to_owned();
let s2: String = String::from("Hello Rust!");
let s3: String = "Hello Rust!".to_string();
// 将占用的24个字节,当成3个usize取出
let raw1: [usize; 3] = unsafe { std::mem::transmute(s1) };
// raw1[0]是一个堆内存地址
// ptr: 0x2749d72c2d0, cap: 11, len: 11
println!("ptr: 0x{:x}, cap: {}, len: {}", raw1[0], raw1[1], raw1[2]);
let raw2: [usize; 3] = unsafe { std::mem::transmute(s2) };
// ptr: 0x2749d72c190, cap: 11, len: 11
println!("ptr: 0x{:x}, cap: {}, len: {}", raw2[0], raw2[1], raw2[2]);		
// ptr: 0x2749d72c1d0, cap: 11, len: 11
let raw3: [usize; 3] = unsafe { std::mem::transmute(s3) };
println!("ptr: 0x{:x}, cap: {}, len: {}", raw3[0], raw3[1], raw3[2]);

借助unsafe关键字和std::mem::transmute可以将变量对应的值的内存暴露出来。对比虚拟内存地址,我们发现
s1、s2、s3实际上是位于堆上不同的位置。也就是说s1、s2、s3分别是从ROData区的"Hello Rust"字面量复制了一份放到堆内存上面。

Vec<u8>转String(String::from_utf8_lossy())

&[u8]类型中构造一个str出来。

pub fn from_utf8_lossy(v: &[u8]) -> Cow<'_, str> { ... }

from_utf8_lossy()能够处理输入的u8序列中包含非法utf8字符的情况:

  1. 如果转换过程中遇到非法的序列(不能转成合法的utf8字符),在结果中用U+FFFD(�字符)替代。此时,返回结果是一个具有所有权的str类型。
  2. 如果转换一切顺利,返回的就是对原u8序列(构成的字符)的借用。
    因此from_utf8_lossy()的返回结果是一个 Cow<str>Clone-On-Write)枚举类型。
// 转换成功
let list = vec![b'o', b'k', b':', b'?', 0x44];
let str1 = String::from_utf8_lossy(&list);
println!("str1={:?}", str1.into_owned());//str1="ok:?D"

// 部分字符被�替代
let str2 = String::from_utf8_lossy(b"fail:\xF0\x90\x80").into_owned();
println!("str2={:?}", str2);//str2="fail:�"

Vec<u8>转String(String::from_utf8())

Vec<u8>类型中构造一个String出来。

pub fn from_utf8(vec: Vec<u8>) -> Result<String, FromUtf8Error> { ... }
  1. 若能转换成合法的utf8字符序列,返回String
  2. 若不能,返回FromUtf8Error
    即是说,转换结果是一个枚举类型:Result<String, FromUtf8Error>
use std::string::FromUtf8Error;
let list = vec![b'a', b'b', 0x3f, 0x44];
let str3: Result<String, FromUtf8Error> = String::from_utf8(list);
// 转换成功,通过unwrap()拿到Result中的合法值
println!("str3={:?}", str3.unwrap());//str3="ab?D"

// 转换失败
let str4: Result<String, FromUtf8Error> = String::from_utf8(vec![b'\xF0', b'\x90', b'\x80']);
println!("str4 is error: {:?}", str4.is_err());//str4 is error: true

字符串和数值类型的转换

//字符串转i32等数值类型
let x:i32 = "123".parse::<i32>().unwrap();
let x:u8 = "123".parse::<u8>().unwrap();
// 字符串转u8类型的切片
let x: &[u8] = "123".as_bytes();

// 数值类型转String
let s: String = 123i32.to_string();
let s: String = 123u8.to_string();

字符数组/列表合并为一个String

let s = ['a','b','c','d'].iter().collect::<String>();
println!("{s}");//abcd
let s = (vec!['a','b','c','d']).iter().collect::<String>();
println!("{s}");//abcd

字符串数组/列表合并(join/concat)为一个String

let s = ["ab","cd"].concat();
println!("{s}");//abcd
let s = (vec!["ab","cd"]).concat();
println!("{s}");//abcd

let s = ["ab","cd"].join("-");
println!("{s}");//ab-cd
let s = (vec!["ab","cd"]).join("-");
println!("{s}");//ab-cd

字符串遍历,判断字符是数字还是字母

is_alphabetic(),判断字符是否是A-Za-z
is_ascii_digit(),判断字符是否是0-9

let s: &str = "abc123";
for c in s.chars() {
    if c.is_alphabetic() {
        println!("{} 是字母", c);
    } else if c.is_ascii_digit() {
        println!("{} 是数字", c);
    }
}

String中追加和删除字符

// String的追加
let mut s1: String = String::from("Hello ");
s1.push_str("Rust!");
s1.push('!');
s1.write_char('!');
s1 += " Hello Rust!";
println!("{}", s1);//Hello Rust!!! Hello Rust!

//repeat()
let x = "Hello".repeat(20);//HelloHello

// String的insert和remove方法
let mut ss = String::with_capacity(256);
ss.insert(0, 'f');
ss.insert(1, 'o');
ss.insert(2, 'o');
assert_eq!("foo", ss);
ss.remove(0);
ss.insert(0, 't');
assert_eq!("too", ss);
ss.clear();
assert_eq!("", ss);

String遍历

// Chars是一个u8类型的迭代器,
// 遍历String中的u8字符
let chs: Chars = s1.chars();
for ch in chs {
    println!("{}", ch);
}
let byts: &[u8] = s1.as_bytes();
for b in byts {
    println!("{}", b);
}

大小写转换(uppercase/lowercase)

let x = "Hello".to_ascii_lowercase();
println!("{x}");//hello
let y = "Hello".to_ascii_uppercase();
println!("{y}");//HELLO

find()查找/contains()包含

let x = "hello".find("ll");//Some(2)
if "hello kitty".contains("kitty") {
    println!("haha kitty");
}

split()分割,返回一个迭代器

let letters = "a b c d".split(" ").for_each(|x: &str| {
    print!("{x} ");// a b c d 
});

lines()按行分割

根据换行符(\n或者\r\n)进行分割。

let s = "abc\ndef\r\nghi";
for x in s.lines() {
    println!("${}$", x);
}

split_ascii_whitespace()

根据1到多个ascii空白字符’ ’ \t \n \r进行分割。

let s = "a b\nc\r\nd\re ";
for x in s.split_ascii_whitespace() {
    println!("${}$", x);
}

split_whitespace()

根据1到多个Unicode空白字符’ ’ \t \n \r \u00a0 \u2009进行分割。

let s = "a b\nc\r\nd\re\u{00a0}f\u{2009}";
for x in s.split_whitespace() {
    println!("${}$", x);
}

starts_with()/ends_with()

assert_eq!(true, "lemon tree".starts_with("lemon"));
assert_eq!(true, "lemon tree".ends_with("tree"));

如何高效修改String中的一个字符?

借助remove()和insert()

可以借助remove()insert(),但是这2个方法都会导致字符串整体迁移,是O(n)复杂度的方法。如果字符串比较长,这样做代价很大。

// remove和insert都是O(n)复杂度的
// remove会导致删除位置后面的所有字符整体前移1个位置
// insert会导致插入位置后面的所有字符整体后移1个位置
let mut s1:String = String::from("Hello");
s1.remove(0);
s1.insert(0, 'X');
assert_eq!("Xello", s1);
更高效的方式

对于追求极致效率的rust而言,提供了unsafe关键字。这里有一个O(1)复杂度的unsafe方法:

// 如何高效修改String中的一个字符
let mut s1:String = String::from("Hello");
unsafe {
    let s1_bytes: &mut [u8] = s1.as_bytes_mut();
    s1_bytes[0] = b'X';
    println!("s1 new={}", s1);//s1 new=Xello
}
更进一步,自定义SetByIndex trait

为了调用更加方便,我们完全可以自定义一个根据位置修改字符的trait,并且为String类型实现这个trait

trait SetByIndex {
    fn set_by_index(&mut self, idx: usize, c: u8);
}
impl SetByIndex for String {
    fn set_by_index(&mut self, idx: usize, c: u8) {
        if idx<0 || idx>=self.len() {
            panic!("Index out of bounds: {}, expected: [0,{})", idx, self.len());
        }
        unsafe {
            let _buf: &mut [u8] = self.as_bytes_mut();
            _buf[idx] = c;
        }
    }
}
let mut ss = "Hello".to_owned();
ss.set_by_index(8, b'X');
println!("ss={}", ss);//ss=Xello
  • 4
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Rust中的字符串是一个动态可变的数据类型,称为`String`。通过`String`类型,我们可以创建、修改、操作和处理字符串。 在Rust中,有多种方法可以创建`String`。最常见的方法是使用`format!`宏来构建一个字符串,或者通过`to_string()`方法将其他类型转换为字符串。 ```rust let hello = "Hello".to_string(); let world = String::from("World"); let message = format!("{} {}", hello, world); ``` `String`类型与基本类型(`&str`)之间可以互相转换。使用`&`运算符可以获取`String`类型的引用,而通过使用像`to_string()`这样的方法,可以将`&str`转换为`String`。 ```rust let hello: &str = "Hello"; let hello_string: String = hello.to_string(); ``` 在Rust中,可以使用`+`运算符将两个字符串连接起来,或使用`push_str`方法将一个字符串附加到另一个字符串上。这两种方法都会创建一个新的`String`对象。 ```rust let hello = String::from("Hello"); let world = String::from("World"); let hello_world = hello + &world; // 可以使用&运算符获取world的引用 ``` 当需要修改字符串时,Rust提供了很多有用的方法。我们可以使用`replace`方法来替换子字符串,使用`trim`方法来去除字符串两侧的空格,使用`split`方法字符串拆分为多个部分等等。 ```rust let message = String::from("Hello World"); let replaced = message.replace("World", "Rust"); let trimmed = message.trim(); let mut parts = message.split_whitespace(); ``` 总之,Rust中的`String`类型提供了许多功能强大且安全的方法来创建、操作和处理字符串。它的灵活性和性能使得在处理字符串时可以更加方便、高效地进行操作。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值