[TOC]
Rust基本数据类型
类型系统概述
什么是类型?类型是对二进制数据的一种约束行为。类型比起直接使用二进制数据,有许多优势:
- 减少开发者心智负担
- 安全
- 容易优化
常见的类型分类:
- 静态类型:在编译期对类型进行检查
- 动态类型:在运行期对类型进行检查
- 强类型:不允许隐式类型转换
- 弱类型:允许进行隐式类型转换
C 语言由于允许隐式类型转换因此是静态弱类型语言,许多人易将 C 语言误认为静态强类型,需要特别注意:
int main() {
long a = 10;
return a;
}
- Rust 是静态强类型语言
变量和可变性
创建和使用变量
在 Rust 代码中,可以使用 let
关键字将值绑定到变量:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
}
println
是一个宏,它是最常用的将数据打印在屏幕上的方法。目前,我们可以简单地将它视为一个拥有可变参数数量的函数,在后面的章节中我们会对宏进行详细的讨论。
可变性
在 Rust 中,变量默认是不可变的,一旦一个值绑定到一个名称,就不能更改该值:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6; // cannot assign twice to immutable variable `x`
println!("The value of x is: {}", x);
}
但有时候允许变量可变是非常有用的。通过在变量名前面添加 mut
来使它们可变:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
常量和变量
不可变变量容易让你联想到另一个概念:常量。在 Rust 中,常量使用 const
定义,而变量使用 let
定义:
- 不允许对常量使用修饰词
mut
,常量始终是不可变的 - 必须显示标注常量的类型
- 常量可以在任何作用域中声明,包括全局作用域
- 常量只能设置为常量表达式,而不能设置为函数调用的结果或只能在运行时计算的任何其他值
const A_CONST: i32 = 1;
隐藏(Shadowing)
可以声明一个与前一个变量同名的新变量,并且新变量会隐藏前一个变量,这种操作被称为隐藏(Shadowing)。
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
}
基础数据类型
Rust 是一门静态编程语言,所有变量的类型必须在编译期就被明确固定。
整数
Rust 中有 12 种不同的整数类型:
长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
- 对于未明确标注类型的整数,Rust 默认采用 i32.
- isize 和 usize 根据系统的不同而有不同的长度.
浮点数
Rust 有两种浮点数类型,为 f32
和 f64
,后者精度更高。对于未明确标注类型的小数,Rust 默认采用 f64
.
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
布尔值
与大多数其他编程语言一样,Rust 中的布尔类型有两个可能的值:true
和 false
。布尔值的大小是一个字节。
fn main() {
let t = true;
let f: bool = false;
}
字符
Rust 支持单个字符,字符使用单引号包装。
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
}
作业: 求两个无符号数的平均数
编写一个函数,它接收两个 u32
类型参数并返回它们的平均数。
fn avg(a: u32, b: u32) -> u32 {
// 补充你的代码
(a & b) + ((a ^ b) >> 1)
}
- 提示:必须考虑整数溢出问题。
一些有用的测试用例:
fn main() {
assert_eq!(avg(4294967295, 4294967295), 4294967295);
assert_eq!(avg(0, 0), 0);
assert_eq!(avg(10, 20), 15);
assert_eq!(avg(4294967295, 1), 2147483648);
println!("passed")
}
整数溢出
在电脑领域里所发生的溢出条件是,运行单项数值计算时,当计算产生出来的结果是非常大的,大于寄存器或存储器所能存储或表示的能力限制就会发生溢出。
在不同的编程语言中,对待溢出通常有以下几种不同的做法:
- 崩溃:当溢出被侦测到时,程序立即退出运行
- 忽略:这是最普遍的作法,忽略任何算数溢出
对于溢出的处理方法,Rust 在 debug 与 release 模式下是不同的。在 debug 模式下编译时,Rust 会检查整数溢出,如果发生这种行为,会导致程序在运行时终止并报出运行时错误。而如果在 release 模式下编译时,Rust 不会对整数溢出进行检查。
要显式处理溢出,可以使用标准库提供的一些 .overflowing_*
方法:
fn main() {
let a: u32 = 4294967295;
let b: u32 = 1;
let (r, is_overflow) = a.overflowing_add(b);
println!("r={} is_overflow={}", r, is_overflow);
}
元组
元组是将多个具有各种类型的值组合成一个复合类型的通用方法。元组有固定的长度:一旦声明,它们的大小就不能增长或收缩。
我们通过在括号内写一个逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型,元组中不同值的类型不必相同。
fn main() {
let a: i32 = 10;
let b: char = 'A';
// 创建一个元组
let mytuple: (i32, char) = (a, b);
// 从元组中读取一个值
println!(".0={:?}", mytuple.0);
println!(".1={:?}", mytuple.1);
// 解封装
let (c, d) = mytuple;
println!("c={} d={}", c, d);
}
数组
另一种拥有多个数据集合的方法是使用数组。与元组不同,数组中的每个元素都必须具有相同的类型。Rust 中的数组不同于其他一些语言中的数组,Rust 中的数组具有固定长度。
数组下标以 0 开始,同时 Rust 存在越界检查:
fn main() {
// 创建数组, [i32; 3] 是数组的类型提示, 表示元素的类型是 i32, 共有 3 个元素
let myarray: [i32; 3] = [1, 2, 3];
// 根据索引获取一个值, 数组下标从 0 开始
println!("{:?}", myarray[1]);
// 索引不能越界
println!("{:?}", myarray[3]);
// 如果数组的每个元素都有相同的值, 我们还可以简化数组的初始化
let myarray: [i32; 3] = [0; 3];
println!("{:?}", myarray[1]);
}
切片类型
切片类型是对一个数组(包括固定大小数组和动态数组)的引用片段,有利于安全有效地访问数组的一部分,而不需要拷贝数组或数组中的内容。切片在编译的时候其长度是未知的,在底层实现上,一个切片保存着两个 uszie
成员,第一个 usize
成员指向切片起始位置的指针,第二个 usize
成员表示切片长度:
fn main() {
let arr: [i32; 5] = [1, 2, 3, 4, 5];
let slice = &arr[0..3]; // 取前 3 个元素,.. 是 Rust Range 语法,& 是引用符号
println!("slice[0]={}, len={}", slice[0], slice.len());
}
结构体
结构体是多种不同数据类型的组合。它与元组类似,但区别在于我们可以为每个成员命名。可以使用 struct
关键字创建三种类型的结构:
- 元组结构
- 经典的 C 结构
- 无字段的单元结构
结构体使用驼峰命名:
// 元组结构
struct Pair(i32, f32);
// 经典的 C 结构
struct Person {
name: String,
age: u8,
}
// 无字段的单元结构, 在泛型中较为常用
struct Unit;
fn main() {
// 结构体的实例化
let pair = Pair(10, 4.2);
let person = Persion {
name: String::from("jack"),
age: 21,
};
let unit = Unit;
// 从结构体中获取成员
println!("{}", pari.0);
println!("{}", persion.name);
}
枚举
enum
关键字可创建枚举类型。枚举类型包含了取值的全部可能的情况。在 Rust 中,有多种不同形式的枚举写法。
无参数的枚举
enum Planet {
Mars,
Earth,
}
上面的代码定义了枚举 Planet,包含了两个值 Mars 和 Earth。
带枚举值的枚举
enum Color {
Red = OxffOOOO,
Green = OxOOffOO,
Blue = OxOOOOff,
}
带参数的枚举
Rust 还支持携带类型参数的枚举:
enum IpAddr {
IPv4(u8, u8, u8, u8),
IPv6(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8),
}
模式匹配
枚举通常与 match
模式匹配一起使用:
enum IpAddr {
IPv4(u8, u8, u8, u8),
IPv6(u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8, u8),
}
fn main() {
let localhost: IpAddr = IpAddr::IPv4(127, 0, 0, 1);
match localhost {
IpAddr::IPv4(a, b, c, d) => {
println!("{} {} {} {}", a, b, c, d);
}
_ => {} // 任何非 IPv4 类型走这条分支
}
}
各种注释类型
与许多现代语言一样,Rust 也支持丰富的注释种类,我们可以通过注释来了解一段代码干了什么工作,甚至可以直接通过注释生成文档。
普通的注释
// 使用 // 注释单行
/*
也可以使用 /* */ 注释多行, 这一点与 C 语言是一样的
*/
文档注释
文档注释是一种 Markdown 格式的注释,用于对文档中的代码生成文档。可以使用 cargo doc 工具生成 HTML 文挡。
//! 这是模块级别的文档注释, 一般用于模块文件的头部
/// 这是文档注释, 一般用于函数或结构体的说明, 置于说明对象的上方.
struct Person;
例子
下面的代码演示了斐波那契函数及其注释,使用 cargo doc 构建 HTML 文档:
//! A main project provides fibonacci function
/// In mathematics, the Fibonacci numbers, commonly denoted Fn form a sequence, called the Fibonacci sequence, such that
/// each number is the sum of the two preceding ones, starting from 0 and 1. That is
/// ```
/// F(0) = 0
/// F(1) = 1
/// F(n) = F(n − 1) + F(n − 2)
/// ```
fn fibo(n: u32) -> u32 {
if n== 0 || n == 1 {
n
} else {
fibo(n - 1) + fibo(n - 2)
}
}
fn main() {
// Calculate fibo(10)
println!("fibo(10) = {}", fibo(10));
/*
The result should be 55
*/
}
image.png
println函数
println!
用于将数据打印到标准输出,且在数据末尾自动带上换行符。在所有平台上,换行符都是换行符(没有额外的回车符)。
使用 println!
用于程序的正常输出,使用 eprintln!
打印错误或者进度条。前者数据被写入 stdout
,后者则是 stderr
。println!
宏常用的格式化语法如下所示:
fn main() {
// `{}` 会被变量内容替换, 这是最常见的一种用法
println!("{}", 42);
// 可以使用额外的位置参数.
println!("{0}{1}{0}", 4, 2);
// 使用命名参数.
println!("name={name} age={age}", name="jack", age=6);
// 可以在 `:` 后面指定特殊的格式.
println!("{} of {:b} people know binary, the other half don't", 1, 2);
// 可以按指定宽度来右对齐文本.
println!("{number:>width$}", number=1, width=6);
// 在数字左边补 0.下面语句输出 "000001".
println!("{number:>0width$}", number=1, width=6);
// println! 会检查使用到的参数数量是否正确.
println!("My name is {0}, {1} {0}", "Bond");
// 编译将会报错, 请补上漏掉的参数:"James"
}
在不同类型之间转换
Rust 是一门强类型语言,因此不支持隐式类型转换。Rust 为了实现类型之间的转换提供了几种不同的方法。
as 语法
as
语法是 Rust 最基础的一种类型转换方法,它通常用于整数,浮点数和字符数据之间的类型转换:
fn main() {
let a: i8 = -10;
let b = a as u8;
println!("a={} b={}", a, b);
}
数值转换的语义是:
- 两个相同大小的整型之间(例如:i32 -> u32)的转换是一个 no-op
- 从一个大的整型转换为一个小的整型(例如:u32 -> u8)会截断
- 从一个小的整型转换为一个大的整型(例如:u8 -> u32)会
- 如果源类型是无符号的会补零(zero-extend)
- 如果源类型是有符号的会符号(sign-extend)
- 从一个浮点转换为一个整型会向 0 舍入
- 从一个整型转换为一个浮点会产生整型的浮点表示,如有必要会舍入(未指定舍入策略)
- 从 f32 转换为 f64 是完美无缺的
- 从 f64 转换为 f32 会产生最接近的可能值(未指定舍入策略)
transmute
as
只允许安全的转换,例如会拒绝例如尝试将 4 个字节转换为一个 u32
:
let a = [0u8, 0u8, 0u8, 0u8];
let b = a as u32; // Four u8s makes a u32.
但是我们知道 u32
在内存中表示为 4 个连续的 u8
,因此我们可以使用一种危险的方法:告诉编译器直接以另一种数据类型对待内存中的数据。编译器会无条件信任你,但是,除非你知道自己在干什么,不然并不推荐使用 transmute
。要使用 transmute
,需要将代码写入 unsafe
块中:
fn main() {
unsafe {
let a = [0u8, 1u8, 0u8, 0u8];
let b = mem::transmute::<[u8; 4], u32>(a);
println!("{}", b); // 256
// Or, more concisely:
let c: u32 = mem::transmute(a);
println!("{}", c); // 256
}
}
网友评论