简单聊聊Rust中的复合类型
复合类型
顾名思义, 复合类型就是其他类型组合而成的, 最典型的就是结构体struct和枚举enum.
字符串
rust中的字符串和我们平时说了解到的编程语言不一样, 比如下面这一段代码是会编译错误的:
fn main() {
let my_name = "Pascal";
greet(my_name);
}
fn greet(name: String) {
println!("Hello, {}!", name);
}
切片
切片在go中就已经很流行了, 它允许你引用集合部分内容, 而不是全部, 在字符串中, 我们可以这么写:
let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];
如果是从0开始, 就可以省略0:
let hello = &s[..5];
同理, 如果你要截取到尾部, 你可以这样省略:
let hello = &s[1..];
但是如果我们操作中文, 就需要格外注意utf-8啦, 一个中文是3个字节, 所以我们在做切片的时候, 如果切到第二个, 就会编译器报错:
实际上, 如果返回切片, 其实是一个字符串引用, 所以我们可以写出这样一段代码:
fn main() {
let mut s = String::from("hello world");
let word = first_word(&s);
s.clear(); // error!
println!("the first word is: {}", word);
}
fn first_word(s: &String) -> &str {
&s[..1]
}
不仅仅字符串可以进行切片, 数组也是可以的:
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3]);
如果我们通过字符串字面量创建字符串的话, 是这样的:
let a = "hello";
此时a就是&str类型, 我们完全可以这样编写代码, 因为此时a指向了可执行文件中的某个点, 也因此字面量创建的字符串是不可变的, 因为&str就是不可变的.
let a: &str = "hello";
字符串是什么
rust中的字符是由unicode编码实现的, 每一个字符占据了4bytes, 但是字符串是用utf-8实现的, 占用的字节数是1-4变化的, 所以更节省内存.
&str和String的区别
在语言层面上来说, 只有str一种类型, 是硬编码到可执行文件中的, 无法被修改; 但是在标准库中我们却可以使用String来创建一个不定长度的字符串类型; String同样也是utf-8编码也具有所有权特性;
如何互相转换
str → string
let a = String::from("hello world");
let b = "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);
}
字符串索引
在js中, 我们可以使用索引轻松的访问字符串, 但是在rust中这是不被允许的:
let s1 = String::from("hello");
let h = s1[0];
因为字符串的底层是使用[u8]字符数组实现的, 如果我们在字符串中使用中文, 一般一个中文是3个byte, 那么此时我们操作中文字符串时, 可能会得到预想不到的值; 所以在rust中, 字符串索引是一个容易造成误解的功能, 因此不支持;
如何正确操作utf-8字符串
如果你想要通过unicode方式操作字符串, 就可以使用.chars()函数
for c in "中国人".chars() {
println!("{}", c);
}
如果你想要查看字符串在底层的字节数组, 就可以使用bytes()函数
for b in "中国人".bytes() {
println!("{}", b);
}
228
184
173
229
155
189
228
186
186
ps: 如果要在rust中截取正确的子串, 就要使用一些库, 比如utf8_slice
深入理解字符串
为什么string可变而str不可变呢? 这很容易理解因为str是在我们编译期间就可以知道的内容, 即不可变, 直接编译到可执行文件中; 但是我们在开发中, 不可能都使用不可变的str, 通常我们的值都会在运行时经过逻辑处理得到的, 所以我们需要一个可变的string类型; 可变的string类型是要在堆上划分一块区域存储的, 等到需要被释放时才会归还给操作系统, 整个周期都是在运行时;
元组
元组是由多种类型组合一起形成的, 它的长度是固定的, 顺序也是固定的; 我们可以通过这个函数创建一个元组:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
可以用模式匹配来解构元组
let (x, y, z) = tup;
也可以通过.索引的方式来获取元组元素
tup.1
在函数中, 如果要返回多个值, 除了使用下面即将介绍的结构体之外, 还可以使用元组
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}
结构体
在元组的介绍中, 返回的calculate_length函数是一个元组类型, 这对于程序来说是不好维护的, 因为不清楚返回参数的任何意义; 那么rust中有一个复合类型叫做结构体可以解决; 那么在其他语言中结构体可以当作object, record(typescript); 结构体是由多个类型组合而成; 它可以给每个类型设置一个名称(key), 所以结构体对比元组更加灵活
我们定义一个结构体, 非常简单:
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
我们需要使用这个结构体构造一个实例, 就更简单啦:
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
和ts一样, 我们定义的结构体需要完全初始化, 即数据模型要和结构体完全匹配.
如果我们需要修改user1的某个字段, 必须把整个user1变为mut可变类型, 结构体不支持仅把某个元素变为可变:
user1.email = String::from("anotheremail@example.com");
我们在结构体中, 对于同名的key和value是可以做到省略简写的, 比如这样:
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
同样的, 我们在ts中经常使用es的扩张运算符来更新对象, 在rust中, 你完全可以使用类似的语法更新结构体 (必须写在尾部)
let user2 = User {
email: String::from("another@example.com"),
..user1
};
关于更新语句, 所有权也需要关注, 在rust所有权中, 部分类型如果支持copy, 那么数据在这里就会被拷贝, 比如bool和u64都实现了copy, 那么在此时的更新结构体的语句中, 仅仅只是把值copy了一份而已; 但是username是string类型, 在此处是转移了所有权到user2中, 那么在user1中就不能操作username了.
结构体不仅仅有key, value这种形式, 还可以有其他形式
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
枚举
枚举是一个类型, 有多个枚举成员, 枚举值是其中某个枚举成员的实例
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
在一些特殊场景下, 你不仅仅可以指定成员类型, 还可以代替结构体简化代码: 在ts中枚举是运行时类型(经过编译器编译之后为对象), 但是在rust中就是实打实的类型, 不能在声时指定值;
如果使用传统的结构体, 是这样写的
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}
struct PokerCard {
suit: PokerSuit,
value: u8
}
fn main() {
let c1 = PokerCard {
suit: PokerSuit::Clubs,
value: 1,
};
let c2 = PokerCard {
suit: PokerSuit::Diamonds,
value: 12,
};
}
我们通过枚举进行改造, 可以大大简化代码:
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(u8),
Hearts(u8),
}
fn main() {
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds(13);
}
指定成员类型
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
}
更复杂的枚举成员类型 (结构体)
struct Ipv4Addr {
// --snip--
}
struct Ipv6Addr {
// --snip--
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
相比较元组结构体, 从语法角度上来说枚举的方式更为简洁且高内聚
Option
在rust中避免了使用大多数语言经常用到的null概念, 改为option, 其类型本身就是一个枚举
enum Option<T> {
Some(T),
None,
}
some指的是有值(任意值), 类型为t, t是一个泛型参数; none则为空
值得注意的是, option并不需要显式的引入, 因为它本身就被包含在标准库中, 也不需要::调用some和none;
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
在rust语言设计中, 你如果想使用类似null的概念, 就必须告诉编译器这个null如果有值的话是什么类型, 即第三段代码; 反之如果使用了some, 就代表了值存在于some之中; 那rust为何这么做, 多此一举新增一个option枚举呢?
其实主要核心问题还是安全性, rust编译器要在编译器确保数据的安全, 不能因为null的滥用导致程序错误, 如果不使用option来标记可能为空的值, 那么在之后的代码中你可能就会忘记这个值的类型和其他类型做逻辑运算, 那么此时null就会被爆雷; 所以如果使用option标记值, 在编译期就可以让rust判断, 提醒开发者.
数组
数组很简单, 同样rust可以设置数组的类型
fn main() {
let a = [1, 2, 3, 4, 5];
// 这里的类型声明, ;之前是类型, ;之后是重复几次
let b: [i32; 5] = [1, 2, 3, 4, 5];
// 同样的也可以通过上述的语法, 让我们初始化数组更快速, 初始化值为3, 重复5次
let c = [3; 5];
}
访问数组
a[0]
可以进行切片
let slice: &[i32] = &a[1..3];