创建自定义类型的两种方式
结构体
1、基本概念
对比元组
- 二者每一个部分可以不同类型
- 结构体需要对各个部分命名,元组不用
通过定义的结构体创建实例
结构体实例可变的话,可以修改结构体中的值
-
2、结构体数据的所有权——有引用字段的结构体
结构体内部可以存引用,这个引用可能是其他对象拥有的数据
如果这么做,就需要有 生!命!周!期!
- 生命周期确保结构体引用的数据有效性和结构体本身保持一致
-
3、基本代码示例
简单定义
struct User {
username: String,
age: u32,
active: bool,
}
创建实例:可变和不可变
//不可变
let user1 = User {
username: String::from("xxx"),
age: 2,
active: false,
};
//可变
let mut user1 = User {
username: String::from("xxx"),
age: 2,
active: false,
};
user1.age = 2;
函数返回实例
字段初始化简写语法
fn build_user(username: String) -> User {
User {
username, //参数名和字段名相同时可以简化写法
active: true,
age: 1,
}
}
从其他实例创建实例
结构体更新语法
let user2 = User {
username: String::from("lpc"),
..user1 //其余实例字段同user1
};
使用无命名字段元组结构体
没有字段名,只有字段类型
用于给整个元组取名,让这个元组和其他元组不同
-
4、在结构体中使用方法
方法 和 函数
二者类似,使用fn关键字,拥有参数和返回值
- 方法是在结构体上下文中定义的(或者枚举和trait对象)
-
示例
&self是用来代替 rect: &Rect 的,表示不获取所有权,只想读取数据
&mut self可以在方法中修改调用方法的实例
#[derive(Debug)]
struct Rect {
width: u32,
height: u32,
}
impl Rect {
fn area(&self) -> u32 {
self.width * self.height
}
}
自动引用和解引用
obj.func() 时,Rust会自动给obj添加 &\&mut* 这些符号
因为我们已经有了obj实例self和方法名,Rust可以自动推断出是 只读 &self / 修改&mut self / 获取所有权self 中的哪一种
关联函数
之前我们说方法是要有impl+&self,于是我如果不想传入&self,光去impl行不行
- 关联函数和结构体关联,但仍然是函数,不做用于结构体实例,如String::from
关联函数常用于构造函数,返回新的实例
impl Rect {
fn square(size: u32) -> Rect {
Rect {width:size, height:size}
}
}
let sq = Rect::square(3);
枚举
通过列举可能的成员variants来定义一个类型
很多语言都有枚举,但区别挺大的
Rust枚举和haskell中的代数数据类型algebraic data types很像1、定义枚举
考虑场景,一个IP要么是v4要么是v6,经常需要区分开处理;但他们都是IP,也经常需要当成同一种类型
C语言常用做法:强行拆分开enum kind {v4, v6};
struct Ip {kind k; char* address};
Ip p4, p6;
p4.kind = kind.v4;
p6.kind = kind.v6;
...
Rust中可以很灵活
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
一个看上去奇怪但确实有用的枚举Message
创建一个枚举实例的时候看起来就有点像关联函数
enum Message {
Quit,
Move {x:i32, y:i32},
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self){}
}
let m = Message::Write(String::from("hello"));
m.call();
2、Option枚举,很强,类型安全
是标准库定义的另一个枚举
对应场景:一个值,他要么没值,要么有值
类型系统:编译器需要检查是否处理了所有应该处理的情况,这样可以避免常见bug
Rust中没有空值NULL,而是使用Option
Option太常用,放在了prelude中,Option和他的成员Some和None不需要Option::前缀来使用Option标准库定义
enum Option<T> {
Some(T),
None,
}
基本使用
let some_number = Some(5); //Option<i32>
let some_string = Some("a string"); //Option<&str>
let none:Option<i32> = None; //使用None的时候需要告诉Rust类型,因为只通过None无法推断Some保存的类型
类型安全示例
Option
和 i8 类型不同!!!太棒了!!! - i8总会有一个有效的值的,而Option可能没有值,于是我代码里就不需要假设非空值的情况
- 阻止了空值的泛滥!!!只要目标值不是Option,我就可以安全地认定它的值不会是空
- 小细节:编译器提示是,相应的Add trait没有实现
let x: i8 = 5;
let y:Option<i8> = Some(5);
let sum = x+y; //错误
但是程序员想要Option时,也不得不写出处理每种可能性的代码,毕竟no free lunch
3、match 控制流 运算符
match的力量来源于模式表现力 和 编译器检查,确保所有可能的情况都得到处理
对比match和if
对于if,表达式必须返回一个bool
match可以是任意类型的
基本内容
match执行是按分支顺序与每一个分支的模式相比较,如果模式匹配了,就执行本分支;否则继续尝试匹配下一个分支
每个分支的结果是:表!达!式!,表达式的结果也会作为match表达式的返回值
分支代码比较短的话可以不用大括号
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
匹配Option
比较Option的成员
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i+1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
Rust的匹配是穷尽的
必须穷举到所有可能性来使得代码有效
例如Option中不要忘了处理None的情况
不想列举所有可能性时,可以 _=> ()
- _ 匹配之前没指定的所有可能值
- ()是unit值,什么也不会发生
...
7 => println!("seven"),
_ => (),
if let简单控制流
获得更少的代码,增加简洁度;但失去match强制的穷尽性检查——权衡
if let可以认为是match的语法糖
前后对比 ```rust let someu8_value = Some(0u8); match some_u8_value { Some(3) => println!(“three”), => (), }
if let Some(3) = some_u8_value { println!(“three”); } ```