各位兄弟姐妹,抱歉拖更了这么久,最近身体不适,加上年终各种绩效、总结、业务需求,有些忙,不过还是要道个歉,确实有自己懈怠了的原因~
还是要啰嗦一句,身体才是革命的本钱,身体好的时候这些乱七八糟的病根本想都不会去想~所以各位还是要多保重身体啊,其他都是假的,身体才是真的😁
这节课开始是集合类型,之前咱们接触的都是基础类型,集合类型有动态数组 、字符串 和哈希映射。
下面我们先来看字符串吧,虽然字符串的原理是动态数组(Vec<u8>)的封装类型,不过咱对String比较熟,就从String先学起。
创建字符串
js中创建字符串的方式大致有如下几种:
javascript
// 字面量
var a = "123";
var b = '123';
// 摸板字面量
var c = 2;
var c1 = `1${c}3`;
// 全局对象String创建
var d = String("123");
// String构造函数
var e = new String("123");
需要注意的是a === b === c1 === d !== e,这是因为String对象直接创建的是一个基础类型字符串,而e是一个String的实例对象。
在rust中创建字符串有如下几个方式:
- String的命名空间函数,这样会创建一个空的字符串,然后可以往里添加字符,不过这个方式不是很方便,没法定义初始数据
rust
let mut s = String::new();
- 基于字符串字面量使用to_string方法创建字符串,如下所示,需要注意,s是字符串字面量,即是一个字符串切片,类型为&str,而不是一个字符串,需要用to_string()转成字符串。
rust
let s: &str = "Hello world";
let s1: String = s.to_string();
- 使用String::from()方法创建字符串,我个人比较喜欢这种方式
rust
let s = String::from("Hello, String!");
rust的字符串是基于UTF-8编码的,记住,后面要考。
更新字符串
- 使用push_str向字符串尾部添加字符串切片
rust
// push_str向String中添加字符串切片
let mut s = String::from("foo");
s.push_str(" bar");
println!("{}", s); // foo bar
- 使用push向字符串尾部添加一个字符,注意只能是一个字符,即char类型
rust
// push向String中添加字符
let mut s = String::from("lo");
s.push('l');
println!("{}", s); // lol
如果我们需要将两个已经定义好的字符串拼接在一起呢?这时候可以使用 + 或者 format! 宏来拼接
- 使用+拼接字符串
rust
/*使用+来拼接字符串 */
let s1 = String::from("Hello");
let s2 = String::from("world");
let s3 = s1 + ", " + &s2;
println!("{}", s3);
看出什么问题来了么?这里+的左边是s1,而右边是&s2,s1移动,而s2是借用,执行到s3那行之后s1就没法再使用了,因为已经移动了,这个前面讲过,不过,为啥要这么使用呢,why???
跟js不一样,js我们直接就s1+s2得到新的字符串了,而rust有所有权的概念,且跟+的拼接的原理有关,使用+操作符其实会调用一个add的函数,这个函数的签名如下:
rust
fn add(self, s: &str) -> String
噢,看到这你就懂了,其实add方法是就是将自身与s字符串切片拼接,而不能两个字符串相加。
这里又有一个问题,add的s的参数类型是字符串切片&str,而我们代码里的s2是String类型,其实这就跟js中的强转类似,执行这个函数的时候,编译器会自动将String类型强转成&str类型。
所以+其实并没有执行多次复制,其实只是做了拼接,这比单纯复制要高效。
但是感觉这个+好麻烦,让s1变成一次性的了,移动后就没法使用了,很不符合咱js程序员的习惯。没有关系,咱可以用format!。
- 使用format!宏
rust
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("使用format!将三个字符串相加{}", s);
使用方式跟println!宏一样,简单易懂,而且s1、s2、s3的所有权还在~
字符串索引
rust
/*字符串索引 */
let s: String = String::from("hello world");
let h = s[0];
看上面这段代码,有问题么?感觉木有问题,js咱就是这么用的。
那就执行一下。噢,不用执行,编译就通不过~
what? rust的字符串不支持索引??why???
回到开头,有介绍到,String其实是一个Vec<u8> 的封装类型,且是UTF-8编码的,所以其实一个字符串在内存中的格式其实是 [232, 128, 129, 229, 137, 141] 这样,我们打印两个字符串长度看一下
rust
let s = String::from("hello world");
println!("s的长度: {}", s.len()); // 11
let s = String::from("老前端");
println!("s的长度: {}", s.len()); // 9
这是因为在utf-8编码中,中文占3个字节,英文字符占一个字节,这就导致了,如果使用下标索引,在非1个字节的字符串中,会取不到整个字符,比如"老",在vec<u8>中表示为[232, 128, 129],s[0]拿的不是"老",而是232,这样就会引起歧义,毕竟我们想要的不是一个字节值,所以rust干脆就不让这么使用,避免意外的返回值或者在运行时才暴露问题,毕竟rust都是为了你好🐶~
字符串切片
那上面的字符串,我到底该如何拿到"老"呢?rust提供了一种方法,使用索引来创建字符串切片。
rust
let s = String::from("老前端");
println!("s的长度: {}", s.len()); // 9
let s1 = &s[0..3];
println!("s1: {}", s1); // 老
&s[0..3]这个方法我应该在前面文章有使用过,就是通过索引,从字符串的引用中根据索引获取字符串切片,索引区间是一个左闭右开的区间,即其实是0-2这3个索引。那中文是3个字节,我索引尾部到4呢,会怎么样?
rust
let s = String::from("老前端");
println!("s的长度: {}", s.len()); // 9
let s1 = &s[0..4];
println!("s1: {}", s1);
运行时会报错:
thread 'main' panicked at 'byte index 4 is not a char boundary; it is inside '前' (bytes 3..6) of 老前端
', src/main.rs:46:15 note: run with RUST_BACKTRACE=1
environment variable to display a backtrace
所以我们使用索引方式获得字符串切片的话,需要明确知道索引区间内的下标起始位置是否正确✅,而且这是运行时才能报错的,没法在编译期查出来,所以你需要特别注意。
遍历字符串的方法
我们还有其他方式来访问字符串中的元素,类似js,rust中的字符串也是可以遍历的。
rust
let s: String = String::from("老前端");
for c in s.chars() {
println!("字符: {}", c);
}
// 字符: 老
// 字符: 前
// 字符: 端
for b in s.bytes() {
println!("字节: {}", b);
}
// 字节: 232
// 字节: 128
// 字节: 129
// 字节: 229
// 字节: 137
// 字节: 141
// 字节: 231
// 字节: 171
// 字节: 175
如果需要在某个索引下处理某些逻辑,可以使用s.chars().enumerate()函数遍历字符串
rust
for (idx, c) in s.chars().enumerate() {
println!("idx: {}, 字符: {}", idx, c);
}
// idx: 0, 字符: 老
// idx: 1, 字符: 前
// idx: 2, 字符: 端
这样就方便了,可以在idx===1时处理某些逻辑。
总结
看下来,确实Rust的字符串比js的字符串复杂一些,使用也没那么方便和自由。不同的语言做的抉择不同,rust为了安全、性能,从而牺牲了自由度。
也许,自由都是需要代价的吧
感谢你的阅读,并希望对你有所帮助,让我们下节课再见👋🏻👋🏻👋🏻