カテゴリー: BackEnd

Rust入門してみた その3 Enum / match / Option編

はじめに

前回に引き続き、Rustの入門編です。今回はEnumについて調べました。

Enum

Rustでは enumキーワードによってEnumを定義することが出来ます。

Enumの定義

まずは、簡単なEnumの例から見ていきます。

#[derive(Debug)]
struct User {
    name: String,
    role: UserRole,
}

// Enumの定義
#[derive(Debug)]
enum UserRole {
    Admin,
    User,
    Guest,
}

fn main() {
    let user = User {
        name: String::from("Taro"),
        role: UserRole::Admin,
    };
    println!("{:?}", user);
}

このように、取りうる値を列挙し、型として定義することが出来ます。
この他にも、Enumは値を関連付けて保持することも出来ます。

// チャットメッセージ構造体
struct Message {
    id: i32,
    content: Content,
}

// Enumの定義
enum Content {
    Text(String),
    Photo { url: String, caption: String },
    Video { url: String, length: i32 },
    Deleted,
}

fn main() {
    let text_message = Message {
        id: 1,
        content: Content::Text("Hello".to_string()),
    };
    println!("{:?}", text_message);

    let photo_message = Message {
        id: 2,
        content: Content::Photo {
            url: "https://example.com/photo.jpg".to_string(),
            caption: "Hello".to_string(),
        },
    };
    println!("{:?}", photo_message);

    let video_message = Message {
        id: 3,
        content: Content::Video {
            url: "https://example.com/video.mp4".to_string(),
            length: 15,
        },
    };
    println!("{:?}", video_message);

    let deleted_message = Message {
        id: 4,
        content: Content::Deleted,
    };
    println!("{:?}", deleted_message);
}

この例では、チャットメッセージのコンテンツの保持方法として、Enumを使用しました。対応するチャットメッセージのコンテンツとして、テキスト、画像、動画、削除済みの4種類をEnumで表現しています。
これらのコンテンツは、取りうる値が異なっています。

まず、テキストは Text(String)となっていて、単独のStringを関連付けて保持することが出来ます。
インスタンス化するには Content::Text("Hello".to_string())のようにします。
複数個とることもでき、例えば Text(String, String, i32)のように宣言します。

画像と動画はそれぞれ Photo { url: String, caption: String } Video { url: String, length: i32 }となっていて、匿名構造体を持ちます。

削除済みのメッセージは、特に関連する値は無いため、単に Deletedとなっています。

このように、1つのEnumの中に異なるデータ構造を持つことが出来るのがEnumの特徴です。

パターンマッチ

Enumのマッチングや、Enumの中の値を取り出す場合にはパターンマッチを行います。
先ほどのコードを少し変えて、Contentに応じて内容を出力するコードを書いてみます。

#[derive(Debug)]
struct Message {
    id: i32,
    content: Content,
}

#[derive(Debug)]
enum Content {
    Text(String),
    Photo { url: String, caption: String },
    Video { url: String, length: i32 },
    Deleted,
}

fn main() {
    let messages = [
        Message {
            id: 1,
            content: Content::Text("Hello".to_string()),
        },
        Message {
            id: 2,
            content: Content::Photo {
                url: "https://example.com/photo.jpg".to_string(),
                caption: "Hello".to_string(),
            },
        },
        Message {
            id: 3,
            content: Content::Video {
                url: "https://example.com/video.mp4".to_string(),
                length: 15,
            },
        },
        Message {
            id: 4,
            content: Content::Deleted,
        },
    ];
    for x in &messages {
        match x.content {
            Content::Text(ref text) => println!("text: {}", text),
            Content::Photo {
                ref url,
                ref caption,
            } => {
                println!("url: {}, caption: {}", url, caption)
            }
            Content::Video { ref url, length } => println!("url: {}, length: {}", url, length),
            Content::Deleted => println!("canceled"),
        }
    }
}

このように、match式を使うことで、Enumのパターンマッチングや、関連付けられた値の取り出しを行うことが出来ます。

ところで、String型の値を取り出す際に refを使用していますが、refを外すとエラーになってしまいます。
これは、String型にはCopyトレイトが実装されていないため、match式内で所有権を奪ってしまうためです。ちなみに、 Content::Videoのlengthはi32であり、こちらはCopyトレイとが実装されているため、所有権のムーブが発生しないためエラーにはなりません。
そのため、refを使うことで値を借用しています。

もちろん、 match &x.contentのようにして、はじめから借用することも考えられますが、refを使うことでenumごと借用するのではなく、中の値単位で必要なもののみ借用することが出来ます。

Enumへのメソッド実装

Enumに対してもimplキーワードでメソッドを実装することが出来ます。

// Content Enumへのメソッド実装
impl Content {
    fn print(&self) {
        match self {
            Content::Text(text) => println!("text: {}", text),
            Content::Photo { url, caption } => {
                println!("url: {}, caption: {}", url, caption)
            }
            Content::Video { url, length } => println!("url: {}, length: {}", url, length),
            Content::Deleted => println!("canceled"),
        }
    }
}

fn main() {
    let messages = [
        Message {
            id: 1,
            content: Content::Text("Hello".to_string()),
        },
        Message {
            id: 2,
            content: Content::Photo {
                url: "https://example.com/photo.jpg".to_string(),
                caption: "Hello".to_string(),
            },
        },
        Message {
            id: 3,
            content: Content::Video {
                url: "https://example.com/video.mp4".to_string(),
                length: 15,
            },
        },
        Message {
            id: 4,
            content: Content::Deleted,
        },
    ];
    for x in &messages {
        // メソッド呼び出し
        x.content.print()
    }
}

構造体へのメソッド実装と似ていますね。

よく使う標準Enum

Option

ところで、RustにはNull値はありませんが、Optionを使うことで、同じ概念を実現することが出来ます。
Optionは、実際には次のようなEnumです。

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

ちなみに、 <T>はジェネリクスです。
実際には次のように使用します。

fn some_sample() {
    let hoge = Some("some hoge");
    let fuga: Option<&str> = None;

    // some hoge
    match hoge {
        Some(ref x) => println!("{}", x),
        None => println!("none"),
    }

    // none
    match fuga {
        Some(ref x) => println!("{}", x),
        None => println!("none"),
    }
}

値がある場合にはSome、そうでなければNoneを使用します。
Someを使用した場合には、型推論が働くため型定義が不要ですが、Noneの場合には値を渡さないため、型定義が必要です。
仮にSomeで値が渡ってきたとしても、型としては Option<&str> なので、必ずパターンマッチして Someであることを確認しなければ、中の値を取り出すことは出来ません。
これによって、Nullと同じ仕組みを実現しています。

Result

以前の記事でResultについて紹介しました。そこにも出てきましたが、ResultもEnumです。

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

 

match式でのパターンマッチを行うことで、値を取り出すことができます。

use std::{fs::File, io::Read};
 
fn main() {
    read_file("hello.txt");
}
 
fn read_file(path: &str) {
    let mut f = match File::open(path) {
        Ok(file) => file,
        Err(e) => panic!("Problem opening the file: {:?}", e),
    };
 
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => println!("File contents: {}", s),
        Err(e) => panic!("Problem reading file: {:?}", e),
    }
}

おすすめ書籍

カイザー

シェア
執筆者:
カイザー
タグ: Rust

最近の投稿

フロントエンドで動画デコレーション&レンダリング

はじめに 今回は、以下のように…

3週間 前

Goのクエリビルダー goqu を使ってみる

はじめに 最近携わっているとあ…

1か月 前

【Xcode15】プライバシーマニフェスト対応に備えて

はじめに こんにちは、suzu…

2か月 前

FSMを使った状態管理をGoで実装する

はじめに 一般的なアプリケーシ…

3か月 前