はじめに
前回に引き続き、Rustの入門編です。今回はEnumについて調べました。
Enum
Rustでは
enum
キーワードによってEnumを定義することが出来ます。
Enumの定義
まずは、簡単なEnumの例から見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | #[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は値を関連付けて保持することも出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | // チャットメッセージ構造体 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に応じて内容を出力するコードを書いてみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 | #[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キーワードでメソッドを実装することが出来ます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | // 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です。
1 2 3 4 | enum Option<T> { Some(T), None, } |
ちなみに、
<T>
はジェネリクスです。
実際には次のように使用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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です。
1 2 3 4 | enum Result<T, E> { Ok(T), Err(E), } |
match式でのパターンマッチを行うことで、値を取り出すことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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), } } |