在先前的章節中我們已經稍微用過Result列舉了,在這個章節中,我們將會學著定義自己的列舉型別,以及使用另一個同樣是由Rust內建,也很常用的Option列舉。列舉和同樣我們先前已經用過的match關鍵字經常互相搭配著使用,在這個章節中,我們也會更深入地學習match關鍵字和if letwhile let語法的用法。



先舉一個例子來說明列舉的用途。現在我們需要用Rust程式來處理IP位址,而IP位址有兩個主要版本,分別是32位元的「IPv4」和128位元的「IPv6」,我們的程式也只要處理這兩種版本的IP位址。所有的IP位址都一定是屬於「IPv4」或是「IPv6」,但不能同時是「IPv4」和「IPv6」。我們可以定義出一個IP位址種類的列舉IpAddrKind,且這個列舉有V4V6兩種變體(variant)。程式如下:

enum IpAddrKind {
    V4,
    V6
}

列舉與結構體類似,定義出來後只是一個型別,要作為「值」使用的話一樣還要經過實體化。IpAddrKind列舉可以實體化出V4V6兩種不同類型的值,實體化的程式碼如下:

enum IpAddrKind {
    V4,
    V6
}

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

實體化出列舉值的方式十分簡單。以上程式,foursix變數的型別都是IpAddrKind,也就是說如果要將它們傳入函數作為參數使用,參數的型別可以使用IpAddrKind。例如:

enum IpAddrKind {
    V4,
    V6
}

fn route(ip_type: IpAddrKind) {}

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

    route(four);
    route(six);
}

將目前的IpAddrKind列舉搭配結構體使用看看吧!程式如下:

enum IpAddrKind {
    V4,
    V6
}

struct IpAddr {
    kind: IpAddrKind,
    address: String
}

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

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

程式第6行到第9行,定義了一個新的結構體IpAddr,用來表示一個IP位址種類和位址。種類的型別即為IpAddrKind列舉,而位址則使用字串型別來儲存。這樣的寫法並沒有什麼問題,但我們可以再寫得更精簡一點,只用一個列舉,不搭配其它結構體,就能夠完成一樣的事情。程式如下:

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

fn main() {
    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)
}

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

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

以上程式將V4從儲存一個字串改成儲存4個8位元的無號整數。雖然homeloopback變數都是屬於IpAddr型別,但它們攜帶的值卻可以是不同數量、不同型別。

接著再來看看其它列舉的例子。請看以下程式:

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

這個Message列舉有四個變體,能用來處理四種不同類型的訊息。這裡要注意的是變體Move定義的方式,它的語法類似結構體,使用大括號{},且擁有欄位名稱。如果把Message列舉的四個變體轉成結構體的寫法的話,程式如下:

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

使用結構體的方式改寫原先的Message列舉後,QuitMessageMoveMessageWriteMessageChangeColorMessage就都不是同一個型別了。

列舉的變體跟結構體很像,除了可以定義資料欄位之外,它也可以使用impl關鍵字來定義方法和關聯函數。如以下程式:

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
    }
}

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

impl關鍵字搭配列舉所實作出來的方法適用於該列舉底下的所有變體。也就是說,以上程式,不論是Message::WriteMessage::QuitMessage::Move還是Message::ChangeColor的實體,都可以使用call方法。在call方法可以利用self參數對實體本身做「型樣匹配」(pattern matching)來決定要執行什麼程式,這個部份將在這個章節後半部做介紹。

先來介紹一下Rust程式語言提供的一個常用的列舉型別OptionOption列舉和Result列舉都是在定義Rust程式語言的方法或是函數時,常使用的型別。Result列舉用來表示函數或是方法有沒有執行成功,而Option列舉則可以用來包裹函數或是方法的回傳值。Result列舉的Ok變體不是就可以包裹回傳值了嗎?為什麼還要用Option列舉呢?

在許多程式語言中有nullnil關鍵字可以使用,即「空值(沒有這個值)」的概念。而在Rust程式語言中,並不能直接使用nullnil關鍵字來代表「空值」。可是在很多種情況下,函數或是方法雖然在邏輯上是執行成功的,但是因某些條件的關係而不會回傳值出來,這時候要怎麼讓Rust的函數或是方法回傳「空值」呢?

Option列舉就是為了表示「空值」而存在的,其定義如下:

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

<T>是泛型的語法,這個在之後的章節會介紹。現在只要知道Option列舉有SomeNone兩種變體,且Some可以包裹其他型別的值,而None就是表示「空值」啦!實體化Option列舉的方式如下:

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

let absent_number: Option<i32> = None;

在使用Result列舉和Option列舉的變體時不需要撰寫Result::Option::來指定使用哪個列舉下的變體,因為Result列舉和Option列舉的變體都已經被包含在std::prelude模組中,會自動被引用進目前程式的scope。

另外,如果想要用類似的方式實體化Result列舉的話,必須要先定義好Result的泛型型別,才能通過編譯,因為Result的泛型有兩個參數,各自是OkErr變體所包裹的值的型別。Result列舉的定義如下:

enum Result<T, E> {
   Ok(T),
   Err(E),
}

舉例來說:

let ok_number = Ok(5);
let err_string = Err("a string");

以上程式會編譯錯誤,因為直接實體化Ok(5),只能推測Ti32,並不能推測E的型別;直接實體化Err(5),也只能推測E&str,並不能推測T的型別。

所以要明確定義Result列舉的型別所用的泛型型別,才可以通過編譯,程式如下:

let ok_number: Result<i32, &str> = Ok(5);
let err_string: Result<i32, &str> = Err("a string");

到這裡如果還是不懂<>所表示的「泛型」到底是什麼也沒關係,後面的章節會有詳細的介紹。

接下來介紹一下match關鍵字吧!雖然我們之前有用過,但並沒有將它徹底地瞭解。match關鍵字可以用來做任何值的「型樣匹配」(pattern matching),早在練習我們寫猜數字程式的時候就已使用過,複習一下,程式如下:

extern crate rand;

use std::io;
use std::cmp::Ordering;
use rand::Rng;

fn main() {
    println!("Guess the number!!");

    let secret_number = rand::thread_rng().gen_range(1, 101);

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin().read_line(&mut guess)
        .expect("Failed to read line");

        let guess: i32 = match guess.trim().parse() {
            Ok(-1) => {
                println!("Bye bye!");
                break;
            },
            Ok(num) => num,
            Err(_) => continue
        };

        println!("You guessed: {}", guess);

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

再舉一個新的例子,請看以下程式碼:

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

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

這個程式中的value_in_cents函數可以將Coin變體的實體轉成其對應的一個32位元的無號整數。matchif關鍵字不同的地方在於:if關鍵字必須使用布林值來進行流程判斷;而match關鍵字則可以判斷任何型別的值,經常與列舉型別的值一同使用。

match關鍵字來匹配Option列舉的值來舉例:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

fn main() {
    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);
}

以上程式,plus_one函數可以接受一個Option<i32>的值。如果傳入的值是None的實體的話,就回傳None;如果傳入的值是Some的實體的話,就將傳入的Some實體包裹在第一個參數的值加一之後,再用新的Some包裹起來回傳。

match關鍵字有一個很重要的特性,那就是他必須將要判斷的值的所有可能都列在「arm」,如果有遺漏,程式就會編譯失敗。舉例來說:

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}

以上程式會編譯失敗,因為x的型別為Option<i32>,它可能的值除了Some(i32)外,還有None,但是match中並未有arm去判斷當xNone時要做的事情。

當然,如果每次使用match關鍵字時都要將所有可能的值都寫成arm會很麻煩,如果我們要判斷的值是u8型別,那不就要寫256(28)條arm嗎?別緊張,對於我們想要統一用一樣的方式處理的其它值,可以使用底線_作為匹配樣本。舉例來說:

let some_u8_value = 0u8;
match some_u8_value {
    1 => println!("one"),
    3 => println!("three"),
    5 => println!("five"),
    7 => println!("seven"),
    _ => (),
}

_的概念就像是ifelse,當_先前的arm都匹配失敗時,就會執行_的arm。也就是說,_的arm應該放在最後一個,如果安插在arm跟arm的之間,編譯時將會出現警告訊息。

再來看一個match關鍵字和Option列舉的例子:

let some_u8_value = Some(0u8);
match some_u8_value {
    Some(3) => println!("three"),
    _ => (),
}

以上程式的目的是要判斷當some_u8_value值為Some的實體,且包裹著3時,就印出three,否則不做任何事情。在很多時候,我們在意的只有Some變體所包裹的值,而不想去理會當Option列舉的實體如果是None時應該要做什麼事。Rust程式語言另外還有一種語法能使用,也就是if let語法。以上程式,可以使用if let語法改寫為:

let some_u8_value = Some(0u8);
if let Some(3) = some_u8_value {
    println!("three");
}

先不要去想match關鍵字寫的程式敘述要怎麼轉成if let語法,而是去想如果我們要用之前學到的if關鍵字來判斷some_u8_value變數的值是不是Some(3)的話要怎麼寫,最直覺的程式碼如下:

fn main() {
    let some_u8_value = Some(0u8);
    if Some(3) == some_u8_value {
        println!("three");
    }
}

這樣的程式是可以編譯並執行的哦!但是如果我們只是要判斷some_u8_value變數的值是不是Some的實體的話,用if關鍵字要怎麼做到呢?拿剛才的plus_one函數來修改看看:

fn plus_one(x: Option<i32>) -> Option<i32> {
    if Some(i) == x {
        Some(i + 1)
    } else {
        None
    }
}

以上程式會編譯錯誤,因為i被當成是要作為參數傳入Some變體的變數了,而不是要代替Some實體第一個參數所包裹的值,因此編譯器會認為i沒有事先宣告出來,產生編譯錯誤。

為了要讓編譯器了解這邊的i是要代替Some實體第一個參數所包裹的值,我們必須使用if let語法來改寫。如下:

fn plus_one(x: Option<i32>) -> Option<i32> {
    if let Some(i) = x {
        Some(i + 1)
    } else {
        None
    }
}

除了在if關鍵字後面加上let關鍵字外,還需要將判斷值是否相等的==改為指派值用的=。請注意=是將右邊的值指派給左邊的變數,因此if let Some(i) = x不能夠寫成if let x = Some(i)

當然就和一般的if關鍵字組成的程式敘述一樣,if letelse if也是可以一起使用的。如下:

fn test(x: Option<i32>) -> Option<i32> {
    if Some(3) == x {
        Some(0)
    } else if Some(4) == x {
        Some(1)
    } else if let Some(5) = x {
        Some(2)
    } else if let Some(i) = x {
        Some(i + 1)
    } else {
        None
    }
}

if let語法就是原本的if關鍵字所組成的程式敘述再加上let關鍵字的特殊用法,不要用match關鍵字去想它會比較容易理解。如果想單獨將let關鍵字和列舉實體拆出來使用的話,是不行的。舉例來說:

let Some(i) = Some(3);

我們預期會宣告出一個變數i並指派3給它,然而這行程式會編譯錯誤。

除了if let語法之外,還有while let語法。舉例來說:

fn main() {
    let mut stack = Vec::new();

    stack.push(1);
    stack.push(2);
    stack.push(3);

    while let Some(top) = stack.pop() {
        println!("{}", top);
    }
}

程式第8行開始的while迴圈,會在stack變數的Vec實體呼叫pop方法的回傳值為Some變體的時候,才繼續執行迴圈。

if let或是while let語法也與match關鍵字一樣,可以使用|字元,來表示「或」(or)的邏輯。舉例來說:

enum Creature {
    Crab(String),
    Lobster(String),
    Person(String),
}

fn main() {
    let state = Creature::Crab("Ferris");

    if let Creature::Crab(name) | Creature::Person(name) = state {
        println!("This creature's name is: {}", name);
    }
}

事實上,型樣匹配不只限於在match關鍵字、if let或是while let語法才能使用。我們使用let關鍵字宣告變數,如let x = 5let (x, y) = (1, 2),甚至是函數或方法的參數,如(x, y): (i32, i32),或是for迴圈,其實也都是在做型樣匹配哦!舉例來說:

fn main() {
    let a = 5;
    let b = 6;

    let (x, y) = (a, b);

    println!("{}", x);
    println!("{}", y);

    let tuple = (10, 11);

    let list = vec![1, 2, 3];

    for (v, &i) in list.iter().enumerate() {
        println!("{}: {}", i, v);
    }

    f(tuple);
}

fn f((x, y): (i32, i32)) {
    println!("{}", x);
    println!("{}", y);
}

以上程式執行結果如下:

5
6
1: 0
2: 1
3: 2
10
11

Rust程式語言將型樣匹配分為「可辯駁」(refutable)和「不可辯駁」(irrefutable)兩種,let x = 5let (x, y) = (1, 2)(x, y): (i32, i32)這樣的用法都是屬於「不可辯駁」的型樣匹配,因為不論是xy都可以是任何值,匹配一定可以成功。而if let Some(x) = a_value這種的匹配方式為「可辯駁」的,因為「a_value」實際的值可能為Some變體,也可能為None變體,有可能會匹配失敗。

函數或方法的參數、let關鍵字和for迴圈,只能支援「不可辯駁」的型樣匹配,if letwhile let語法只支援「可辯駁」的型樣匹配,match的arm,可以同時支援「不可辯駁」和「可辯駁」的型樣匹配。

match關鍵字的型樣匹配,還可以有一些特殊的變化。例如我們可以在每個arm要匹配的樣本中,使用|字元,來表示「或」(or)的邏輯。舉例來說:

let x = 1;

match x {
    1 | 2 => println!("one or two"),
    3 => println!("three"),
    _ => println!("anything"),
}

也可以同時使用多個|字元。舉例來說:

let x = 5;

match x {
    1 | 2 | 3 | 4 | 5 => println!("one through five"),
    _ => println!("something else"),
}

以上程式,由於12345是連續的整數數值範圍,我們也可以在型樣匹配的樣本中使用先前提過的....=範圍語法(前者屬於exclusive_range_pattern特色,由於其還在實驗階段,所以需加上#![feature(exclusive_range_pattern)],並使用Rust的nightly版本來編譯程式)。舉例來說:

let x = 5;

match x {
    1 ..= 5 => println!("one through five"),
    _ => println!("something else"),
}

型樣匹配除了可以用在列舉之外,一般的結構體也能使用,可以快速地「解開」結構體。舉例來說:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    let Point { x, y } = p;

    println!("{}", x);
    println!("{}", y);
}

以上程式執行結果如下:

0
7

再舉一個例子:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let points = vec![
        Point { x: 0, y: 0 },
        Point { x: 1, y: 5 },
        Point { x: 10, y: -3 },
    ];

    let mut sum_of_squares = 0;

    for Point { x, y } in points.iter() {
        sum_of_squares += x * x + y * y;
    }

    println!("{}", sum_of_squares);
}

執行結果如下:

135

我們也可以對結構體使用可辯駁的型樣匹配。舉例來說:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x: 0, y } => {
            println!("x is zero, y is {}", y);
        }
        Point { x, y } => ()
    }
}

如果不想要直接使用結構體的欄位名稱作為變數名稱的話,也可以直接在:字元後面定義該欄位的值要匹配到的變數名稱。舉例來說:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x: 0, y: y_var } => {
            println!("x is zero, y is {}", y_var);
        }
        Point { x, y } => ()
    }
}

那麼如果我們想要對結構體使用可辯駁的型樣匹配,同時又想使用自己的變數名稱的話,要怎麼做呢?那就是在變數名稱後面使用@字元來串接要匹配的值。舉例來說:

struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 0, y: 7 };

    match p {
        Point { x: 0, y: y_var @ 5...7 } => {
            println!("x is zero, y is {}", y_var);
        }
        Point { x, y } => ()
    }
}

當然也可以讓多個結構體和列舉複合使用,舉例來說:

struct Point {
    x: i32,
    y: i32,
}


fn main() {
    let a: (Option<Point>, Option<Point>) = (Some(Point { x: 3, y: 4 }), None);

    if let (Some(Point { x, y }), None) = a {
        println!("{}", x);
        println!("{}", y);
    }
}

以上程式執行結果如下:

3
4

底線_其實不只能夠被單獨使用在匹配樣本中,它的匹配方式就相當於xy這樣的變數,只是_在匹配完成後就會忽略掉匹配成功的值了。舉例來說:

let v = Some(5);

match v {
    Some(_) => {
        println!("Some");
    }
    _ => {
        println!("None");
    }
}

Some(_)Some(x)可以說是完全一樣的匹配樣本,只是如果使用Some(_)來匹配的話,就不會有一個實際的變數來儲存匹配到的值。如果我們將以上程式的Some(_)改為Some(x)的話:

let v = Some(5);

match v {
    Some(x) => {
        println!("Some");
    }
    _ => {
        println!("None");
    }
}

程式在編譯時將會出現unused variable: `x`的警告訊息。不管在哪,如果我們宣告了變數卻完全沒去使用的話,Rust的編譯器都會出現這個訊息。如果想要讓編譯器不出現這個警告訊息的話,可以讓變數的名稱以底線_開頭,如下:

let v = Some(5);

match v {
    Some(_x) => {
        println!("Some");
    }
    _ => {
        println!("None");
    }
}

這樣一來編譯器就不會出現unused variable: `x`的警告訊息了!

我們除了能用_在型樣匹配時忽略不想要的值之外,也可以使用..語法來直接忽略掉資料結構中所有剩下來的值。要注意這邊的..語法並不是範圍語法哦!舉例來說:

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

fn main() {
    let p = Point { x: 0, y: 7, z: 9 };

    let Point { x, .. } = p;

    println!("{}", x);
}

以上程式執行結果如下:

0

再舉一個例子:

fn main() {
    let t = (3, 4, 5, 6, 7, 8);

    let (x, y, ..) = t;
    
    println!("{}", x);
    println!("{}", y);
}

以上程式執行結果如下:

3
4

..語法用在數組的時候,其位置不一定要放在最右邊,但是只能夠出現一次。舉例來說:

fn main() {
    let t = (3, 4, 5, 6, 7, 8);

    let (.., x, y) = t;

    println!("{}", x);
    println!("{}", y);
}

以上程式執行結果如下:

7
8

再舉一個例子:

fn main() {
    let t = (3, 4, 5, 6, 7, 8);

    let (x, y, .., z) = t;

    println!("{}", x);
    println!("{}", y);
    println!("{}", z);
}

以上程式執行結果如下:

3
4
8

接著來談談型樣匹配時的擁有權吧!先看以下的程式:

fn main() {
    let robot_name = Some(String::from("Magic Len"));

    match robot_name {
        Some(name) => println!("Found a name: {}", name),
        None => (),
    }
}

這個程式會編譯成功,並且在螢幕上印出:

Found a name: Magic Len

然而,如果我們在最後添加一些程式進去,來使用robot_name變數的話,會發生什麼事呢?程式如下:

fn main() {
    let robot_name = Some(String::from("Magic Len"));

    match robot_name {
        Some(name) => println!("Found a name: {}", name),
        None => (),
    }

    println!("robot_name is: {:?}", robot_name);
}

我們添加的程式第9行會編譯失敗,因為robot_name變數原本的值,已經移動給程式第5行match的arm所匹配好的變數name了。為了解決在型樣匹配過程中擁有權轉移的問題,我們可以在匹配樣本的變數前使用ref關鍵字來取得匹配到的值的參考並指派給變數,而不是直接將匹配到的值的本身指派給變數。至於為什麼不使用&就好,那是因為&在型樣匹配中代表的意義是「匹配參考型別」,而不是「取得值的參考」。

舉例來說:

fn main() {
    let s = String::from("Hello");

    let n = Some(5);

    let v = Some(&n);

    match v {
        Some(&Some(a)) => {
            println!("&Some!")
        }
        _ => {}
    }
}

&來做型樣匹配其實看起來蠻奇怪,雖然現在的Rust在做類似的型樣匹配時可以省略&,但在早期的Rust版本,&卻是必要的。不過總之,就是因為&在型樣匹配中已經有它既定的用途了,因此我們需要使用ref關鍵字來代替用來取得參考的&

將剛才的Robot Name,加上ref關鍵字,改寫如下:

fn main() {
    let robot_name = Some(String::from("Magic Len"));

    match robot_name {
        Some(ref name) => println!("Found a name: {}", name),
        None => (),
    }

    println!("robot_name is: {:?}", robot_name);
}

程式第5行的變數name,型別變成&String,並不會更動到原先String結構實體的擁有者,因此程式可以通過編譯。執行結果如下:

Found a name: Magic Len
robot_name is: Some("Magic Len")

如果要使用可變參考,也可以讓ref關鍵字和mut關鍵字搭配使用。例如:

fn main() {
    let mut robot_name = Some(String::from("Magic Len"));

    match robot_name {
        Some(ref mut name) => {
            println!("Found a name: {}", name);
            name.push_str(" (checked)");
        }
        None => (),
    }

    println!("robot_name is: {:?}", robot_name);
}

執行結果如下:

Found a name: Magic Len
robot_name is: Some("Magic Len (checked)")

另外,match關鍵字的型樣匹配還有「匹配守衛」(match guard)的用法,需要讓match的arm,搭配if關鍵字使用。利用這個匹配守衛語法,我們可以讓arm增加一些匹配條件。當匹配守衛的條件參數運算結果為true,這個arm就會匹配成功,執行其所對應的程式區塊。舉例來說:

let num = Some(4);

match num {
    Some(x) if x < 5 => println!("less than five: {}", x),
    Some(x) => println!("{}", x),
    None => (),
}

總結

這章節我們學會了如何使用列舉來撰寫Rust程式,並且也深入瞭解了Rust程式語言的型樣匹配了。我們現在的Rust程式功力已經足夠寫出各式各樣的運算程式(單執行緒限定),可以去找一些演算法的題目來練習看看。接下來的章節會介紹模組(mod)的用法,將我們寫好的程式包裝起來給別人使用!

下一章:模組