Back
Featured image of post Rust 学习笔记04 - 结构体和枚举

Rust 学习笔记04 - 结构体和枚举

导航页

结构体

结构体允许你包装和命名多个相关的值,从而形成一个有意义的聚合。比较类似之前的元组,但有些许不同。

可以这样定义一个结构体:

struct User {
  active: bool,
  username: String,
  email: String,
  sign_in_count: u64,
}

其中每一行名字和类型构成一个字段(field)。

可以这样实例化它:

let user1 = User {
  email: String::from("someone@example.com"),
  username: String::from("someusername123"),
  active: true,
  sign_in_count: 1,
};

如果实例是可变的,可以通过点号更改它的值:

let mut user1 = User {
  email: String::from("someone@example.com"),
  username: String::from("someusername123"),
  active: true,
  sign_in_count: 1,
};
user1.email = String::from("anotheremail@example.com");

注意,Rust 要求所有字段的可变性一致。也就是说,要么整个实例都是可变的,要么都不可变。

可以使用一个函数填充初始值:

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

当参数名和字段名相同时,可以进一步简化:

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

基于其他实例创建

当我们想基于其他实例修改的时候,我们可以这样写:

let user2 = User {
  email: String::from("another@example.com"),
  ..user1
};
// 等价于
let user2 = User {
  active: user1.active,
  username: user1.username,
  email: String::from("another@example.com"),
  sign_in_count: user1.sign_in_count,
};

..user1 必须放在最后,其他字段的顺序不重要。

这种语法类似带有 = 的赋值,因为移动了数据。在这个例子中,我们创建 user2 后不再使用 user1,因为 user1 中的 username 字段被移到了 user2 中。如果我们为 usernameemail 都赋了新值,那么 user1 仍然有效。因为 activesign_in_count 都实现了 Copy trait。

元组结构体

使用元组结构体(tuple structs),可以达到为元组指定别名的效果。比如:

struct Color(i32, i32, i32)
struct Point(i32, i32, i32)

fn main() {
  let black = Color(0, 0, 0);
  let origin = Point(0, 0, 0);
}

注意,ColorPoint 的类型并不兼容。Rust 是强类型的,不会允许两个结构体长得像,就允许相互赋值。

在其他方面,元组结构体很像元组,也允许解构声明和点操作符访问。

类单元结构体

Rust 允许你这样做:

struct AlwaysEqual;

fn main() {
  let subject = AlwaysEqual;
}

定义一个结构体而没有字段,也叫类单元结构体(unit-like structs),因为这类似 ()

这通常用于想要在某个类型上实现 trait,但不想存储数据的场景。这将在后面涉及。

结构体的所有权

User 的结构体定义中,我们使用 String 而不是 &str 类型。因为我们希望结构体拥有该数据,只要结构体有效,对应的字段也有效。

可以使结构体储存引用,但需要生命周期(lifetimes)。生命周期保证字段有效性和结构体本身保持一致。这将在后面详细讲解。

方法

有以下代码:

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

我们可以将长宽信息存储至 Rectangle,并使用 area 函数计算。然而正如文章Kotlin - 面向 IDE 的编程语言中所说的:

最终 Kotlin 形成一长串调用 a.map {...}.sorted().toString,而 Python 不断嵌套 str(sorted(map(...)))

这有什么好处?只需要在表达式末尾添加一个点,IDE 的选择列表就会激活,并且帮你找到想要的内容。而在 Python 中,你需要「提前」知道所有函数名,并且要将所有圆括号、方括号、大括号一一匹配。

Kotlin 允许你使用拓展函数,Swift 也有 extension。而 Rust 更为激进——所有方法都是单独的实现,并与结构体关联。

使用 impl 我们可以声明结构体的实现,并且如之前所说,我们可以有多个实现:

struct Rectangle {
  length: u32,
  width: u32,
}

impl Rectangle {
  fn square(size: u32) -> Rectangle {
    Rectangle {
      length: size,
      width: size,
    }
  }

  fn area(&self) -> u32 {
    self.width * self.length
  }

  fn can_hold(&self, other: &Rectangle) -> bool {
    self.width > other.width && self.length > other.length
  }
}

impl Rectangle {
  fn can_hold_least(&self, other: &Rectangle, area: u32) -> bool {
    self.can_hold(other) && self.area() > area
  }
}

其中 &self 指名了方法的接收者,实质上是一个简写。可以理解为 self: &Rectangle。若没有 self,则代表该方法是静态的,仅仅是绑定在该命名空间下,可以通过 :: 访问:

fn main() {
  let sq = Rectangle::square(10);
  println!("The size of square is {}", sq.area());
}

-> 运算符呢?

在 C / C++ 中,我们需要有两个不同的运算符来调用方法:. 直接在对象上调用,-> 在对象指针上调用,这时还需要解引用指针。若 object_ptr 是一个指针,那么 object_ptr->something() 就和 (*objcet_ptr).something() 一样。

Rust 没有 -> 运算符,相反,Rust 会自动引用和解引用(automatic referencing and dereferencing)。方法调用是为数不多有该行为的地方。

Rust 会自动为 object 添加 & &mut* 以便和接收者签名匹配。这两者等价:

p1.distance(&p2);
(&p1).distance(&p2);

Rust 可以通过接收者签名,明确的知道方法是要只读(&self)、做出修改(&mut self)或是获得所有权(self)。

枚举

枚举(enumerations),也叫 enums,允许你列举可能的成员(varints)来定义一个类型。

Rust 的枚举更类似 Haskell 中的代数数据类型(algebraic data types),Kotlin 中的密封接口(sealed interface)。而不只是一个表示有限集合的对象。

要定义枚举很简单:

enum IpAddrKind {
    V4,
    V6,
}

可以通过 :: 创建它们:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

很容易想到这样来存储 IP 地址的值:

struct IpAddr {
  kind: IpAddrKind,
  address: String,
}

let home = IpAddr {
  kind: IpAddrKind::V4,
  address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
  kind: IpAddrKind::V6,
  address: String::from("::1"),
};

在大多数语言中,我们都可以这样做,即将枚举值作为属性。

Rust 允许一种更方便的写法:

enum IpAddr {
  V4(String),
  V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

我们可以直接将数据附加在枚举的每个成员上,就不需要额外的结构体了。

同时,每个枚举成员的签名不必相同:

enum IpAddr {
  V4(u8, u8, u8, u8),
  V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

标准库的写法类似这样:

pub enum IpAddr {
  V4(Ipv4Addr),
  V6(Ipv6Addr),
}

struct Ipv4Addr {
  // ...
}

struct Ipv6Addr {
  // ...
}

可以将任意类型的数据放入枚举,字符、数字、结构体,甚至另一个枚举,都是可以的。

枚举的可能性很多,来看下另一个例子:

enum Message {
  Quit, // unit-like
  Move { x: i32, y: i32 }, // struct
  Write(String), // tuple struct
  ChangeColor(i32, i32, i32), // tuple struct
}

对于枚举,我们也可以声明对应的实现:

impl Message {
  fn call(&self) { /* ... */ }
}

Option

Rust 中没有空值。而是使用该枚举替代。

它的定义很简单:

enum Option<T> {
  None,
  Some(T),
}

T 是泛型,这在后面详细讲解。这里只需要知道 T 可以装任何类型即可。

Option 会被自动导入,因此可以直接用 NoneSome 创建 Option

let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

Debug Trait

这里我们不具体介绍 trait,只讲解如何使用。

通过派生 Debug trait,我们可以打印调试信息:

#[derive(Debug)]
struct Rectangle {
  width: u32,
  height: u32,
}

fn main() {
  println!("rect1 is {:?}", rect1);
  // rect1 is Rectangle { width: 30, height: 50 }
  println!("rect1 is {:#?}", rect1);
  // rect1 is Rectangle {
  //    width: 30,
  //    height: 50,
  // }
}

{:?} 为简短 debug 样式,{:#?} 为较长 debug 样式。

Debug 对应的有 Display trait,用于显示给用户。

match

match 用于更好地处理枚举。包括 Option。如这样:

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

枚举要求分支穷尽,所以可以用于返回值/赋值:

impl Coin {
  fn to_value(&self) -> u8 {
    return match self {
      Coin::Penny => {
        println!("Penny!");
        1
      }
      Coin::Nickel => 5,
      Coin::Dime => 10,
      Coin::Quarter => 25,
    };
  }
}
fn main() {
  Coin::Penny.to_value();
}

在 match 遇到有值的枚举时,我们这样拿到对应的值:

#[derive(Debug)]
enum UsState {
  Alabama,
  Alaska,
}

enum Coin {
  Penny,
  Nickel,
  Dime,
  Quarter(UsState),
}

impl Coin {
  fn to_value(&self) -> u8 {
    return match self {
      Coin::Penny => {
        println!("Penny!");
        1
      }
      Coin::Nickel => 5,
      Coin::Dime => 10,
      Coin::Quarter(state) => {
        println!("State quarter from {:?}", state);
        25
      }
    };
  }
}

fn main() {
  Coin::Quarter(UsState::Alaska).to_value();
}

当我们不想处理所有分支时,可以使用 _(相当于 else):

impl Coin {
  fn is_penny(&self) -> bool {
    return match self {
      Coin::Penny => true,
      _ -> false
    };
  }
}

if let

if let 可以看作 match 的一种特殊形式,只处理一个操作。

let config_max = Some(3u8);
match config_max {
  Some(max) => println!("The maximum is configured to be {}", max),
  _ => (),
}

可以改写为:

let config_max = Some(3u8);
if let Some(max) = config_max {
  println!("The maximum is configured to be {}", max);
}

也可以添加 else

let mut count = 0;
if let Coin::Quarter(state) = coin {
  println!("State quarter from {:?}!", state);
} else {
  count += 1;
}

以上,这就是对 Rust 结构体和枚举的介绍了。

comments powered by Disqus