12、String(字符串)

String(字符串)

字符串的本质就是一种特殊的容器类型,在程序开发过程中,常被作为一个整体来处理,不同于其它语言的字符串,Rust官方库的String字符串会显得很复杂,不过没关系,再复杂也得一点一点学会

字符串的创建

现在我们学习两种字符串,一种是字符串字面量,另一种是可变长度的字符串对象

1.&str的创建

Rust内置的字符串类型是str,这种类型就是字符串字面量,字符串字面量在定义之后不可有任何形式的修改,通常以&str的形式出现。&str是字符的集合,代表的是不可变的UTF-8编码的字符串的引用。

创建&str有如下方式

let str="Hello Rust!";//直接赋值一个简单的字符串,str类型为&str

let Str=String::from("Hello Rust!");//创建String对象
let str=Str.as_str();//将String对象转换为&str

2.String的创建

String是由Rust标准库提供的、拥有所有权的UTF-8编码的字符串类型,创建后可以任意地修改。它的本质其实就是一个字段为Vec<u8>类型的结构体,它将字符内容存放在堆上,由三部分组成,分别是:

  • 指向堆上字节序列的指针(as_ptr方法)
  • 记录堆上字节序列的长度(len方法)
  • 堆分配的容量(capacity方法)

有三种创建String的方法:

//使用String::new()创建空的String对象
let mut str1=String::new();//不加mut无法修改

//使用String::from()从字符串字面量创建String对象
let str2=String::from("Hello Rust!");

//使用to_string方法将字符串字面量转换为String对象
let str="Hello Rust!";
let str3=str.to_string();

字符串的修改

String对象的常见修改操作有追加、插入、连接、替换、删除等。

1.追加

使用push在String后面追加字符,使用push_str在String后面追加字符串字面量。

这两个方法都是在原String上追加,不会创建新的String。

fn main(){
	let mut str=String::from("Hello ");//必须要加mut,否则无法修改
	str.push('R');
	str.push_str("ust!");
	println!("{}",str);
}

image-20250901201027033

2.插入

使用insert在String中插入字符,使用insert_str在String中插入字符串字面量。

这两个方法都接收两个参数,一个是插入位置的索引,一个是插入的字符或字符串字面量。

同样的,这两个方法都是在原String上追加,不会创建新的String

fn main(){
	let mut str=String::from("Hello !");
	str.insert(6,'R');
	str.insert_str(7,"ust");
	println!("{}",str);
}

image-20250901202623617

运行结果如下:

image-20250901202637024

一定要注意写mut还有注意方法参数啊,我写的时候就总是忘记写mut(

注意

insertinsert_str是基于字节序列的索引进行操作的,内部会通过is_char_boundary方法判断插入位置的索引是否在合法边界内,如果索引非法将会导致程序错误。

image-20250901203521820

运行结果如下:

image-20250901203533019

现在试一下越界的索引:

image-20250901203706150

运行结果如下:

image-20250901203716919

可以看到,不管是非法UTF-8插入边界还是非法越界的索引,is_char_boundary方法都可以准确地识别出错误并抛出Panic

总结

不要用索引去切中文等多字节UTF-8字符,可以避免很多问题

3.连接

使用++=运算符将两个字符串连接为一个新的字符串,运算符的右边必须是字符串字面量,运算符的左边必须是String。

不能对两个String类型字符串使用++=运算符,也不能对两个字符串字面量使用++=运算符。

连接会返回新的字符串,也就是使用++=运算符都会返回新的字符串。

image-20250901204551896

image-20250901204621075

fn main(){
	let mut str1=String::from("Hello");
	let str2=String::from("Rust");
	//let str3=' ';
	let str4="!";
	
	//let mut str=str1+str3+str2;
    let mut str=str1+str2;
	str+=str4;
	println!("{}",str);
}

image-20250901210641971

char不能直接拼接到String后面,Rust不支持这样的操作,所以上面的代码会报错

注意

+运算符的右边,也可以使用&s(s是String对象)拼接,这样操作,&s就是s这个String对象的“字符串字面量”,可以直接用于连接操作。

这是因为String类型实现了Deref trait[1],执行连接操作时会自动解引用为&str类型,使用as_str方法就可以将String类型转换为&str类型

 

对于较为复杂的或带有格式的字符串连接,可以使用格式化宏format!,它对于String和&str类型的字符串都适用

fn main(){
	let str1=String::from("Hello");
	let str2="Rust!";
	
	let str3=format!("{} {}",str1,str2);
	println!("{}",str3);
}

image-20250901212533064

运行结果如下:

image-20250901212559761

4.替换

使用replacereplacen方法,可以将主串中指定的字串替换为一个新的字串

replace(<要被替换掉的字符串子串>,<新字符串>);

replacen(<要被替换掉的字符串子串>,<新字符串>,替换的个数);

fn main(){
    let s=String::from("aaabbccdaa");

    let s1=s.replace("aa","ee");
    let s2=s.replacen("aa","ee",1);

    println!("{}",s1);
    println!("{}",s2);
}

image-20250901213204665

运行结果如下:

image-20250901213220969

5.删除

使用popremovetruncateclear方法删除字符串中的字符。

  • pop:删除并返回字符串的最后一个字符,返回类型为Option<char>。如果字符串为空,则返回None。
  • remove:删除并返回字符串中指定位置的字符,其参数是该字符的起始索引位置(注意remove方法是按照字节处理字符的,所以面对多字节的UTF-8字符时,如果给定的索引不是合法的字符边界,就会直接报错)。
  • truncate:删除字符串中从指定位置开始到结尾的全部字符,其参数是起始索引位置,无返回值(注意truncate方法也是按照字节处理字符,如果给定的索引不是合法的字符边界,就会直接报错)。
  • clear:删除字符串中的所有字符,无返回值(等价于将truncate的参数设置为0)。
fn main(){
	let mut str=String::from("Hello Rust!");
	
	println!("{:?}",str.pop());
	println!("{}",str);
    println!();
	
	println!("{:?}",str.remove(9));
	println!("{}",str);
    println!();
	
    str.truncate(5);
	println!("{}",str);
    println!();
	
    str.clear();
	println!("{}",str);
}

image-20250901214928806

运行结果如下:

image-20250901214949078

字符串的访问

1.Unicode

Unicode是一个全球统一的字符编码标准,他为世界上几乎所有的文字系统分配唯一的数字编号(甚至包含各类emoji),称为代码点Code Points)。

比如:

  • A的代码点是U+0041
  • 你好的代码点是U+4F60
  • 😄的代码点是U+1F604

Unicode只是一套“字符与数字”的映射表。

2.UTF-8

UTF-8(Unicode Transformation Format-8-bit)是一种具体的编码方式,用于将Unicode的代码点转换为字节序列,以便在计算机中存储和运输。

特点:

特征 说明
长度可以变化 1-4个字节表示一个字符
向后兼容ASCII 所有ASCII字符(0-127)用一个字节表示,与ASCII完全一致
自动纠错 有效的UTF-8序列有明确的格式,能自动检测错误

占 3 个字节,😄 占 4 个字节 —— 单个字符的所占字节数不同,所以我们不能简单地通过“字节数”来计算“字符数”。

3.访问UTF-8字符串

根据上面的简单说明,我们可以得到两个结论:

  1. 字符串是UTF-8的字节序列,不能直接使用索引来访问字符。
  2. 字符串操作可以分为按字节处理按字符处理两种方式,按字节处理使用bytes方法返回按字节迭代的迭代器,按字符处理使用chars方法返回按字符迭代的迭代器。

1.使用len方法获取字符串长度

使用len方法得到的并不是String的字符数,而是String所占的字节大小。

fn main(){
	let str="大风起兮云飞扬";
	println!("{}",str.len());
}

image-20250901221443602

运行结果如下:

image-20250901221457154

由此可知,中文字符在UTF-8中占三个字节

2.通过迭代器访问字符串的字符

fn main(){
	let str="安得猛士兮走四方";
	
	for b in str.bytes(){
		print!("{} ",b);
	}
    println!();
	
	for c in str.chars(){
		print!("{} ",c);
	}
}

image-20250901222003713

运行结果如下:

image-20250901222026994

练习

这次的练习要完成的内容比较多

pub struct Ticket {
    title: String,
    description: String,
    status: String,
}

impl Ticket {
    // TODO: implement the `new` function.
    //  The following requirements should be met:
    //   - Only `To-Do`, `In Progress`, and `Done` statuses are allowed.
    //   - The `title` and `description` fields should not be empty.
    //   - the `title` should be at most 50 bytes long.
    //   - the `description` should be at most 500 bytes long.
    //  The method should panic if any of the requirements are not met.
    //  Panic messages: "Only `To-Do`, `In Progress`, and `Done` statuses are allowed",
    //  "Title cannot be empty", "Description cannot be empty", "Title cannot be longer than 50 bytes",
    //  "Description cannot be longer than 500 bytes"

    // TODO:实现 `new` 函数。
    //  应满足以下要求
    //      - 只允许使用 `To-Do`、`In Progress` 和 `Done` 状态。
    //      - `title` 和 `description` 字段不能为空。
    //      -`title` 字段的长度不得超过 50 字节。
    //      - `description` 字段的长度不得超过 500 字节。
    //  如果有任何要求未满足,该方法应恐慌。
    //  panic 消息:"Only `To-Do`, `In Progress`, and `Done` statuses are allowed"、
    //  "Title cannot be empty", "Description cannot be empty", "Title cannot be longer than 50 bytes"、
    //  "Description cannot be longer than 500 bytes"。
    
    // You'll have to use what you learned in the previous exercises,
    // as well as some `String` methods. Use the documentation of Rust's standard library
    // to find the most appropriate options -> https://doc.rust-lang.org/std/string/struct.String.html
    pub fn new(title: String, description: String, status: String) -> Self {
    	if title./* TODO */ Self {
            title,
            description,
            status,
        }
        Self {
            title,
            description,
            status,
        }
    }
}
  1. status只能是To-DoIn ProgressDone三种字符串其一。
  2. titledescription字段不能为空。
  3. title字段的长度不能超过50字节。
  4. description字段的长度不能超过500字节。
  5. 如果以上的任何一条不满足,都应该触发Panic。

这个练习其实并不难,就是要写的代码比较多,我敲了一小会儿,也是完成了

pub fn new(title: String, description: String, status: String) -> Self {
    if status!="To-Do"&&status!="In Progress"&&status!="Done"{
        panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
    }
    if title.is_empty(){
        panic!("Title cannot be empty");
    }
    if description.is_empty(){
        panic!("Description cannot be empty");
    }
    if title.len()>50{
        panic!("Title cannot be longer than 50 bytes");
    }
    if description.len()>500{
        panic!("Description cannot be longer than 500 bytes");
    }
    Self {
        title,
        description,
        status,
    }
}

image-20250901222642117

代码无误!

 

这一节就先到这里了,后面学到所有权,生命周期估计会有很多补充。

阅读剩余
THE END