Rust 2.8 组合数据类型
11 Jan 2015 by LelouchHe
同其他语言一样,Rust有很多内置类型.你已经接触过整型和字符串类型了,接下来,我们讨论下更加复杂的数据类型.
元组(tuple)
第一种数据类型是元组.元组是固定大小的有序列表.比如:
let x = (1, "hello");
小括号和逗号构成了长度为2的元组.下面类似,但增加了类型标注:
let x: (i32, &str) = (1, "hello");
可以看到,元组的类型和元组类似,只不过每个位置上都是类型名称,而不是其值.元组的元素可以是不同的:上面的元组中分别是i32
类型和&str
类型.以前没有见过&str
类型,这个我们后面介绍字符串时会提到.系统编程语言中,字符串是比较复杂的.现在的话,姑且将&str
称为”字符串片”(string slice),以后会详述.
你可以通过解析let(destructuring let)来获取元组内部元素的值.比如:
let (x, y, z) = (1, 2, 3);
println!("x is {}", x);
还记得么,我以前提到过let
的左边比单纯的绑定赋值的功能要强大很多?这就是明证.我们可以在let
的左边写成模式(pattern),当它和右边匹配(match)时,就会同时进行绑定.这里例子里,let
解析或拆分了元组,并进行了3次绑定.
这个模式非常强大,后面还会看到的.
我们还可以不解析元组,而是把它当作整体来处理.你可以把一个元组赋值给另一个,只要他们的元素的个数和类型匹配即可.
let mut x = (1, 2);
let y = (2, 3);
x = y;
你也可以用==
进行相等判断.而且只有当二者类型完全一致时,才能编译成功.(译者: 即类型如果不一致,连编译都无法通过,更无从比较相等了)
let x = (1, 2, 3);
let y = (2, 2, 4);
if x == y {
println!("yes");
} else {
println!("no");
}
上面的例子会输出”no”,因为二者类型一致,但值不相同.
另一个用途是通过元组返回多个值.
fn next_two(x: i32) -> (i32, i32) {
(x + 1, x + 2)
}
fn main() {
let (x, y) = next_two(5);
println!("x, y = {}, {}", x, y);
}
虽然函数只能返回单值,但元组本身是一个值,只不过元组内恰好有多个值罢了.上面例子也演示了如何解析返回的元组.
元组是非常简单的数据类型,有时并不能满足要求.下面就是更强大的类型,结构体(struct).
结构体(struct)
结构体和元组类似,是另一种记录类型(record type).结构体的不同是:结构体给每个元素有单独的名字,称为字段(field)或成员变量(member).比如:
struct Point {
x: i32,
y: i32,
}
fn main() {
let origin = Point{ x: 0, y: 0};
println!("The origin is at ({}, {})", origin.x, origin.y);
}
上面有很多内容,我们慢慢分析.我们用struct
声明了一个结构体,后面跟着它的名字(“Point”).按照惯例,结构体的名字要以大写字母开头,并且是CamelCase形式: “PointInSpace”, 而不是”Point_In_Space”.
可以使用let
创建一个结构体的实例,还可以使用key: value
的形式对每个字段赋值.赋值顺序和字段声明顺序不需要一致.
最后,因为字段有名字,所以可以通过点操作符(“.”)来获取字段: origin.x
结构体的值默认情况下,同Rust其他绑定类似,是不可变的.使用mut
表示可变:
struct Point {
x: i32,
y: i32,
}
fn main() {
let mut point = Point { x: 0, y: 0 };
point.x = 5;
println!("The point is at ({}, {})", point.x, point.y);
}
这个会输出”The point is at (5, 0)”.
元组结构体(tuple struct)和新类型
Rust还有叫做元组结构体(tuple struct)的类型,类似元组和结构体的混合类型.元组结构体有名字,但字段没名字:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
这两个类型不相同,就算他们的值一样:
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
通常来讲,结构体比元组结构体要好得多.我们可以重写下Color
:
struct Color {
red: i32,
blue: i32,
green: i32,
}
struct Point {
x: i32,
y: i32,
z: i32,
}
这样的话,我们就可以使用字段名称,而不仅仅是位置了.好的名称是很重要的,使用结构体,我们就能利用这个名称.
但有一种情况,元组结构体是有用的,即只有一个元素的元组结构体.我们称之为”新类型”(newtype),因为这让你可以创造一个名字不同的新类型:
struct Inches(i32);
let length = Inches(10);
let Inches(integer_length) = length;
println!("length is {} inches",integer_length);
你可以通过解析let获取到其中的值.
枚举(enum)
最后,Rust有枚举(enum)类型.枚举类型非常有用,标准库里使用的非常广泛.下面就是标准库提供的一个枚举类型:
enum Ordering {
Less,
Equal,
Greater,
}
一个Ordering
类型的变量,只能是Less
, Equal
或者Greater
.
因为这是标准库提供的,我们可以用use
来引入.后面我们会详述,此时只要知道,use
是引入名字的即可.
下面就是一个例子:
use std::cmp::Ordering;
fn cmp(a: i32, b: i32) -> Ordering {
if a < b { Ordering::Less }
else if a > b { Ordering::Greater }
else { Ordering::Equal }
}
fn main() {
let x = 5;
let y = 10;
let ordering = cmp(x, y);
if ordering == Ordering::Less {
println!("less");
} else if ordering == Ordering::Greater {
println!("greater");
} else {
println!("equal");
}
}
这里有一个符号我们没加过: 双冒号(“::”).这个是用来表示命名空间(namespace)的.在这里,Ordering
在cmp
模块里,而cmp
在std
模块里.后面我们会介绍模块(module).现在,知道可以使用use
引入标准库即可.
我们再看下真实的代码.cmp
比较两个整型大小,并返回Ordering
类型的值.根据二者的大小,分别返回Ordering::Less
, Ordering::Greater
或Ordering::Equal
.注意,每个枚举类型的值,都在这个枚举类型的命名空间里: 是Ordering::Greater
,而不是Greater
.
ordering
变量的类型是Ordering
,所以它包含上述3个值之一.我们可以使用一堆if/else
来判断.但是重复的if/else
很复杂.Rust有一个功能可以解除这个重复,同时避免遗漏某些情况.但在那之前,我们在看下另一种枚举: 单一值(one with value).
下面这个枚举有2个变量,但只有一个值:
enum OptionalInt {
Value(i32),
Missing,
}
这个枚举表示的是可选的整型.如果是Missing
,我们就没有值了,否则,如果是Value
,我们就有整型值.这个枚举是专门针对整型的,我们能让它适用于所有类型,但还有些内容后面才会学到,所以暂且不表.
你也可以在枚举中有任意数量的其他值:
enum OptionalColor {
Color(i32, i32, i32),
Missing,
}
或者像下面这个:
enum StringResult {
StringOK(String),
ErrorReason(String),
}
StringResult
可以是StringResult::StringOK
,其值是正常的计算结果字符串,或者是StringResult::ErrorReason
,其值为错误原因的字符串.这些枚举都很有用,在标准库中有广泛用途.
下面是一个简单的例子:
enum StringResult {
StringOK(String),
ErrorReason(String),
}
fn respond(greeting: &str) -> StringResult {
if greeting == "Hello" {
StringResult::StringOK("Good morning!".to_string())
} else {
StringResult::ErrorReason("I didn't understannd you!".to_string())
}
}
打的字太多了,使用use
简化下:
use StringResult::StringOK;
use StringResult::ErrorReason;
enum StringResult {
StringOK(String),
ErrorReason(String),
}
fn respond(greeting: &str) -> StringResult {
if greeting == "Hello" {
StringOK("Good morning!".to_string())
} else {
ErrorReason("I didn't understannd you!".to_string())
}
}
use
声明必须在最开始,所以这里看上去有些诡异,我们在类型定义之前就需要声明use
了.不管怎样,我们就可以直接使用StringOK
,而不是全称StringResult::StringOK
.引入名称非常方便,但有时会有冲突,所以需要谨慎.良好的风格是不进行引入.
可以看到,带值的枚举类型是非常有用的,而且如果引入泛型(generic)的话,会更有用.在那之前,我们将会学习使用模式匹配,通过模式匹配来处理枚举类型,会更加优雅,而且不用重复的if/else