Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ประกาศ Enum

ที่ struct ให้คุณวิธีจัดกลุ่ม field และข้อมูลที่ผูกกัน อย่าง Rectangle กับ width และ height ของมัน enum ให้คุณวิธีบอกว่าค่าเป็นหนึ่งในชุดของ ค่าที่เป็นไปได้ เช่น เราอาจอยากบอกว่า Rectangle เป็นหนึ่งในชุดของรูปร่าง ที่เป็นไปได้ ที่รวม Circle และ Triangle ด้วย ในการทำสิ่งนี้ Rust ให้ เรา encode ความเป็นไปได้เหล่านี้เป็น enum

มาดูสถานการณ์ที่เราอาจอยากแสดงในโค้ด และดูว่าทำไม enum มีประโยชน์และเหมาะ สมกว่า struct ในกรณีนี้ สมมติว่าเราต้องทำงานกับ IP address ปัจจุบันมี มาตรฐานหลักสองตัวที่ใช้สำหรับ IP address — version สี่และ version หก เพราะ นี่คือความเป็นไปได้เดียวสำหรับ IP address ที่โปรแกรมเราจะเจอ เรา enumerate variant ที่เป็นไปได้ทั้งหมดได้ ซึ่งเป็นที่มาของชื่อ enumeration

IP address ใด ๆ เป็นได้ทั้ง version สี่หรือ version หก แต่ไม่ใช่ทั้งคู่ ในเวลาเดียวกัน คุณสมบัตินั้นของ IP address ทำให้โครงสร้างข้อมูล enum เหมาะสม เพราะค่า enum เป็นได้แค่หนึ่งใน variant ของมัน ทั้ง address version สี่และ version หกยังเป็น IP address โดยพื้นฐาน ดังนั้นพวกมัน ควรถูกปฏิบัติเป็น type เดียวเมื่อโค้ดจัดการสถานการณ์ที่ใช้กับ IP address ชนิดใดก็ได้

เราแสดงแนวคิดนี้ในโค้ดได้ โดยประกาศ enumeration IpAddrKind และระบุชนิด ที่เป็นไปได้ที่ IP address เป็น V4 และ V6 เหล่านี้คือ variant ของ enum:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

IpAddrKind ตอนนี้เป็น type custom data type ที่เราใช้ที่อื่นในโค้ดได้

ค่า Enum

เราสร้าง instance ของแต่ละ variant ทั้งสองของ IpAddrKind ได้แบบนี้:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

หมายเหตุว่า variant ของ enum อยู่ใน namespace ภายใต้ identifier ของมัน และ เราใช้ double colon คั่นทั้งสอง นี่มีประโยชน์ เพราะตอนนี้ทั้งค่า IpAddrKind::V4 และ IpAddrKind::V6 เป็น type เดียวกัน — IpAddrKind เราจึงประกาศฟังก์ชันที่รับ IpAddrKind ใด ๆ ได้ เช่น:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

และเราเรียกฟังก์ชันนี้ด้วย variant ใดก็ได้:

enum IpAddrKind {
    V4,
    V6,
}

fn main() {
    let four = IpAddrKind::V4;
    let six = IpAddrKind::V6;

    route(IpAddrKind::V4);
    route(IpAddrKind::V6);
}

fn route(ip_kind: IpAddrKind) {}

การใช้ enum มีข้อดีอีกมาก คิดเพิ่มเกี่ยวกับ type IP address ของเรา ตอนนี้ เราไม่มีวิธีเก็บ ข้อมูล IP address จริง ๆ — เรารู้แค่ว่ามันเป็น ชนิด อะไร เมื่อคุณเพิ่งเรียนเรื่อง struct ในบทที่ 5 คุณอาจอยากจัดการปัญหานี้ ด้วย struct ดังที่แสดงใน Listing 6-1

fn main() {
    enum IpAddrKind {
        V4,
        V6,
    }

    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"),
    };
}
Listing 6-1: เก็บข้อมูลและ variant IpAddrKind ของ IP address โดยใช้ struct

ที่นี่ เราประกาศ struct IpAddr ที่มีสอง field — field kind ที่เป็น type IpAddrKind (enum ที่เราประกาศก่อนหน้า) และ field address ที่เป็น type String เรามีสอง instance ของ struct นี้ ตัวแรกคือ home และมีค่า IpAddrKind::V4 เป็น kind พร้อมข้อมูล address ที่ผูกอยู่คือ 127.0.0.1 instance ที่สองคือ loopback มี variant อื่นของ IpAddrKind เป็นค่า kind คือ V6 และมี address ::1 ผูกอยู่ เราใช้ struct ห่อค่า kind และ address เข้าด้วยกัน ดังนั้นตอนนี้ variant ผูกกับค่า

อย่างไรก็ตาม การแสดงแนวคิดเดียวกันโดยใช้แค่ enum กระชับกว่า — แทนที่จะมี enum ภายใน struct เราใส่ข้อมูลลงในแต่ละ variant ของ enum ตรง ๆ ได้ การประกาศ enum IpAddr ใหม่นี้บอกว่าทั้ง variant V4 และ V6 จะมีค่า String ผูกอยู่:

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

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

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

เราติดข้อมูลกับแต่ละ variant ของ enum ตรง ๆ ดังนั้นไม่ต้องมี struct เพิ่ม ที่นี่ยังเห็นรายละเอียดอีกอย่างของวิธีที่ enum ทำงานง่ายขึ้น — ชื่อของ แต่ละ variant ของ enum ที่เราประกาศ ยังกลายเป็นฟังก์ชันที่สร้าง instance ของ enum นั่นคือ IpAddr::V4() เป็นการเรียกฟังก์ชันที่รับ argument String แล้ว return instance ของ type IpAddr เราได้ฟังก์ชัน constructor นี้อัตโนมัติเป็นผลของการประกาศ enum

มีข้อดีอีกอย่างของการใช้ enum แทน struct — แต่ละ variant มี type และจำนวน ข้อมูลที่ผูกต่างกันได้ Address IP version สี่จะมี component ตัวเลขสี่ตัว ที่มีค่าระหว่าง 0 ถึง 255 เสมอ ถ้าเราอยากเก็บ address V4 เป็นค่า u8 สี่ตัว แต่ยังแสดง address V6 เป็นค่า String หนึ่งค่า เราทำไม่ได้กับ struct Enum จัดการกรณีนี้ได้สบาย ๆ:

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

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

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

เราแสดงหลายวิธีในการประกาศโครงสร้างข้อมูลเพื่อเก็บ IP address version สี่ และ version หก อย่างไรก็ตาม ปรากฏว่าการอยากเก็บ IP address และ encode ชนิดเป็นเรื่องใช้บ่อยมาก standard library มี definition ที่เราใช้ได้! มาดูว่า standard library ประกาศ IpAddr อย่างไร มันมี enum และ variant เป๊ะ ๆ เหมือนที่เราประกาศและใช้ แต่ฝัง address data ภายใน variant ในรูป ของ struct สองตัวที่ต่างกัน ซึ่งประกาศต่างกันสำหรับแต่ละ variant:

#![allow(unused)]
fn main() {
struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

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

โค้ดนี้แสดงว่าคุณใส่ข้อมูลชนิดใดก็ได้ภายใน variant ของ enum — string, type ตัวเลข หรือ struct เป็นต้น คุณรวม enum อื่นเข้าไปก็ยังได้! standard library type ก็มักไม่ซับซ้อนกว่าสิ่งที่คุณคิดได้เองมากนัก

หมายเหตุว่าแม้ standard library จะมี definition ของ IpAddr เรายังสร้าง และใช้ definition ของเราเองได้โดยไม่ขัดกัน เพราะเราไม่ได้นำ definition ของ standard library เข้า scope เราจะพูดถึงการนำ type เข้า scope มากขึ้น ในบทที่ 7

มาดูตัวอย่างอีกตัวอย่างของ enum ใน Listing 6-2 — ตัวนี้มี type หลากหลาย ฝังใน variant

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}
Listing 6-2: enum Message ที่แต่ละ variant เก็บจำนวนและ type ของค่าต่างกัน

enum นี้มีสี่ variant ที่มี type ต่างกัน:

  • Quit: ไม่มีข้อมูลผูกอยู่เลย
  • Move: มี field ที่ตั้งชื่อ เหมือน struct ทำ
  • Write: รวม String หนึ่งตัว
  • ChangeColor: รวมค่า i32 สามตัว

การประกาศ enum ที่มี variant อย่างใน Listing 6-2 คล้ายกับการประกาศ struct หลายชนิด ยกเว้น enum ไม่ใช้ keyword struct และ variant ทั้งหมดถูกจัด กลุ่มภายใต้ type Message struct ต่อไปนี้จะเก็บข้อมูลเดียวกับที่ variant ของ enum ก่อนหน้าเก็บ:

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

fn main() {}

แต่ถ้าเราใช้ struct ต่างกัน ที่แต่ละตัวมี type ของตนเอง เราจะไม่สามารถ ประกาศฟังก์ชันให้รับ message ชนิดใด ๆ เหล่านี้ได้ง่ายเท่า เหมือนที่เราทำ ได้กับ enum Message ที่ประกาศใน Listing 6-2 ซึ่งเป็น type เดียว

มีอีกความคล้ายระหว่าง enum และ struct — เหมือนที่เราประกาศเมธอดบน struct ได้ด้วย impl เราประกาศเมธอดบน enum ได้ด้วย นี่คือเมธอดชื่อ call ที่ เราประกาศบน enum Message ของเราได้:

fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }

    impl Message {
        fn call(&self) {
            // method body would be defined here
        }
    }

    let m = Message::Write(String::from("hello"));
    m.call();
}

Body ของเมธอดจะใช้ self เพื่อรับค่าที่เราเรียกเมธอด ในตัวอย่างนี้ เรา สร้างตัวแปร m ที่มีค่า Message::Write(String::from("hello")) และนั่น คือสิ่งที่ self จะเป็นใน body ของเมธอด call เมื่อ m.call() รัน

มาดู enum อีกตัวใน standard library ที่ใช้บ่อยและมีประโยชน์มาก — Option

Enum Option

ส่วนนี้สำรวจ case study ของ Option ซึ่งเป็น enum อีกตัวที่ประกาศโดย standard library Type Option encode สถานการณ์ที่ใช้บ่อยมาก ที่ค่าอาจ เป็นบางอย่าง หรืออาจไม่มีอะไร

เช่น ถ้าคุณขอ item แรกใน list ที่ไม่ว่าง คุณจะได้ค่า ถ้าคุณขอ item แรกใน list ที่ว่าง คุณจะไม่ได้อะไร การแสดงแนวคิดนี้ในรูปของระบบ type หมายความว่า compiler เช็คได้ว่าคุณจัดการทุก case ที่ควรจัดการหรือยัง — functionality นี้ป้องกัน bug ที่ใช้บ่อยมากในภาษาโปรแกรมอื่นได้

การออกแบบภาษาโปรแกรมมักคิดในแง่ของฟีเจอร์ที่คุณรวม แต่ฟีเจอร์ที่คุณยกเว้น ก็สำคัญ Rust ไม่มีฟีเจอร์ null ที่ภาษาอื่นหลายภาษามี Null คือค่าที่ หมายถึงไม่มีค่าที่นั่น ในภาษาที่มี null ตัวแปรอยู่ในหนึ่งในสอง state เสมอ — null หรือไม่ null

ในการนำเสนอปี 2009 “Null References: The Billion Dollar Mistake” Tony Hoare ผู้คิดค้น null บอกไว้:

ผมเรียกมันว่าความผิดพลาดมูลค่าพันล้านดอลลาร์ของผม ในตอนนั้น ผมกำลังออกแบบ ระบบ type ที่ครอบคลุมตัวแรกสำหรับ reference ในภาษา object-oriented เป้า หมายของผมคือรับประกันว่าการใช้ reference ทุกครั้งต้องปลอดภัยอย่างสมบูรณ์ ด้วยการเช็คที่ทำโดย compiler อัตโนมัติ แต่ผมต้านทานความล่อใจไม่ได้ที่จะ ใส่ null reference เพียงเพราะมัน implement ง่ายมาก สิ่งนี้นำไปสู่ error, vulnerability และ system crash นับไม่ถ้วน ซึ่งอาจทำให้เกิดความเจ็บปวด และความเสียหายมูลค่าพันล้านดอลลาร์ในสี่สิบปีที่ผ่านมา

ปัญหากับค่า null คือ ถ้าคุณพยายามใช้ค่า null เป็นค่าที่ไม่ใช่ null คุณจะ ได้ error บางชนิด เพราะคุณสมบัติ null หรือไม่ null นี้แพร่ขยายไปทั่ว มัน ง่ายมากที่จะทำ error แบบนี้

อย่างไรก็ตาม แนวคิดที่ null พยายามแสดงยังมีประโยชน์ — null คือค่าที่ตอนนี้ invalid หรือไม่มีด้วยเหตุผลบางอย่าง

ปัญหาไม่ใช่อยู่ที่แนวคิดจริง ๆ แต่อยู่ที่ implementation เฉพาะ ดังนั้น Rust ไม่มี null แต่มี enum ที่ encode แนวคิดของค่าที่มีอยู่หรือไม่มีได้ enum นี้คือ Option<T> และ ประกาศโดย standard library ดังนี้:

#![allow(unused)]
fn main() {
enum Option<T> {
    None,
    Some(T),
}
}

enum Option<T> มีประโยชน์มากจนรวมใน prelude ด้วย คุณไม่ต้องนำเข้า scope แบบ explicit variant ของมันก็รวมใน prelude ด้วย — คุณใช้ Some และ None ตรง ๆ ได้โดยไม่มี prefix Option:: enum Option<T> ยังเป็นแค่ enum ปกติ และ Some(T) กับ None ยังเป็น variant ของ type Option<T>

syntax <T> เป็นฟีเจอร์ของ Rust ที่เรายังไม่พูดถึง มันเป็น generic type parameter และเราจะครอบคลุม generic ในรายละเอียดในบทที่ 10 ตอนนี้สิ่งที่ คุณต้องรู้คือ <T> หมายความว่า variant Some ของ enum Option เก็บ ข้อมูลหนึ่งชิ้นของ type ใดก็ได้ และแต่ละ type คอนกรีตที่ใช้แทน T ทำให้ type Option<T> โดยรวมเป็น type ต่างกัน นี่คือตัวอย่างการใช้ค่า Option เก็บ type ตัวเลขและ type char:

fn main() {
    let some_number = Some(5);
    let some_char = Some('e');

    let absent_number: Option<i32> = None;
}

type ของ some_number คือ Option<i32> type ของ some_char คือ Option<char> ซึ่งเป็น type ต่างกัน Rust infer type เหล่านี้ได้ เพราะ เราระบุค่าภายใน variant Some สำหรับ absent_number Rust กำหนดให้เรา annotate type Option โดยรวม — compiler infer type ที่ variant Some ที่สอดคล้องจะเก็บไม่ได้ โดยดูแค่ค่า None ที่นี่ เราบอก Rust ว่าเรา หมายถึง absent_number เป็น type Option<i32>

เมื่อเรามีค่า Some เรารู้ว่ามีค่าอยู่ และค่าถูกเก็บภายใน Some เมื่อ เรามีค่า None ในแง่หนึ่งหมายเหมือน null — เราไม่มีค่าที่ valid แล้ว ทำไม Option<T> ถึงดีกว่า null?

สั้น ๆ คือ เพราะ Option<T> และ T (โดย T เป็น type ใดก็ได้) เป็น type ต่างกัน compiler ไม่ให้เราใช้ค่า Option<T> ราวกับว่ามันแน่นอนเป็น ค่าที่ valid เช่น โค้ดนี้จะไม่ compile เพราะมันพยายามบวก i8 กับ Option<i8>:

fn main() {
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;
}

ถ้าเรารันโค้ดนี้ เราได้ error message แบบนี้:

$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`
  = help: the following other types implement trait `Add<Rhs>`:
            `&i8` implements `Add<i8>`
            `&i8` implements `Add`
            `i8` implements `Add<&i8>`
            `i8` implements `Add`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error

แรง! ผลคือ error message นี้หมายความว่า Rust ไม่เข้าใจวิธีบวก i8 กับ Option<i8> เพราะเป็น type ต่างกัน เมื่อเรามีค่า type อย่าง i8 ใน Rust compiler จะรับประกันว่าเรามีค่าที่ valid เสมอ เราดำเนินต่อไปอย่าง มั่นใจได้โดยไม่ต้องเช็ค null ก่อนใช้ค่านั้น เฉพาะเมื่อเรามี Option<i8> (หรือ type ของค่าที่กำลังทำงานด้วย) เราต้องห่วงว่าอาจไม่มีค่า และ compiler จะรับประกันว่าเราจัดการกรณีนั้นก่อนใช้ค่า

พูดอีกอย่าง คุณต้องแปลง Option<T> เป็น T ก่อนคุณทำ operation T กับ มันได้ โดยทั่วไป สิ่งนี้ช่วยจับหนึ่งในปัญหาที่ใช้บ่อยที่สุดของ null — การ สมมติว่าอะไรบางอย่างไม่ใช่ null ทั้งที่จริงเป็น

การกำจัดความเสี่ยงของการสมมติค่าที่ไม่ใช่ null อย่างไม่ถูกต้อง ช่วยให้คุณ มั่นใจมากขึ้นในโค้ด เพื่อให้มีค่าที่อาจเป็น null คุณต้อง opt in แบบ explicit โดยทำให้ type ของค่านั้นเป็น Option<T> จากนั้น เมื่อคุณใช้ค่า นั้น คุณถูกบังคับให้จัดการ case เมื่อค่าเป็น null แบบ explicit ทุกที่ที่ ค่ามี type ที่ไม่ใช่ Option<T> คุณ สามารถ สมมติได้อย่างปลอดภัยว่าค่า ไม่ใช่ null นี่เป็นการตัดสินใจออกแบบที่ตั้งใจของ Rust เพื่อจำกัดการแพร่ ขยายของ null และเพิ่มความปลอดภัยของโค้ด Rust

แล้วคุณดึงค่า T ออกจาก variant Some ยังไง เมื่อคุณมีค่า type Option<T> เพื่อให้คุณใช้ค่านั้นได้? enum Option<T> มีเมธอดจำนวนมากที่ มีประโยชน์ในสถานการณ์ต่าง ๆ คุณดูได้ใน documentation ของมัน การคุ้นเคยกับเมธอดบน Option<T> จะมีประโยชน์มากในการเดินทางกับ Rust ของ คุณ

โดยทั่วไป เพื่อใช้ค่า Option<T> คุณอยากมีโค้ดที่จัดการแต่ละ variant คุณอยากมีโค้ดที่รันเฉพาะเมื่อคุณมีค่า Some(T) และโค้ดนี้ใช้ T ภายในได้ คุณอยากมีโค้ดอื่นรันเฉพาะถ้าคุณมีค่า None และโค้ดนั้นไม่มีค่า T ให้ใช้ match expression คือโครงสร้าง control flow ที่ทำสิ่งนี้พอดี เมื่อใช้กับ enum — มันจะรันโค้ดต่างกันขึ้นกับว่ามี variant ไหนของ enum และโค้ดนั้นใช้ ข้อมูลภายในค่าที่ match ได้