Rustのenumの使い方【match文、エラーハンドリング】
- 作成日: 2024-02-16
- 更新日: 2024-02-16
- カテゴリ: Rust
Rustで列挙型を定義したい場合、Rustではenum
が使えます。
Rustのenum
は他言語のenum
に比べると非常にパワフルで表現が多彩です。
そのためエラーハンドリングでも便利に使うことができます。
Rustのenum
の性質を理解して使いこなせば柔軟なコードを書くことが出来るようになるでしょう。
この記事ではRustのenum
について解説します。
Enumを定義する - The Rust Programming Language 日本語版
enumの定義方法
Rustではenum
は以下のように定義します。
enum 列挙型名 {
列挙子,
列挙子,
列挙子,
...
}
ここでは簡単にAnimal
という列挙型名で列挙子Cat
, Dog
を定義してみます。
enum Animal {
Cat,
Dog,
}
enum
の各列挙子はカンマで区切ります。
このように複数の列挙子を列挙できるのがenum
の特徴です。
enum
列挙型名も列挙子も命名規則は全ての単語の先頭文字を大文字にするパスカルケースです。
enum MyAnimal {
Cat,
WaterDog,
GreenPullDog,
}
列挙子は複数の同じ名前で列挙することはできません。
enum Animal {
Cat,
Cat,
}
// error[E0428]: the name `Cat` is defined multiple times
上記のような定義はエラーになります。
enumのインスタンスの生成
enum
の列挙子はインスタンスとして変数にすることができます。
以下のようなコードを書きます。
let cat = Animal::Cat;
let dog = Animal::Dog;
enum
のAnimal
の列挙子を参照するには上記のようにコロンを2つ付けて参照します。
enumのifでの参照
Rustのenum
のインスタンスはC/C++などと違ってif
で参照することはできません。
たとえば以下のようなコードはエラーになります。
if cat == Animal::Cat {
}
// error[E0369]: binary operation `==` cannot be applied to type `Animal`
変数cat
はAnimal::Cat
のインスタンスなので直感的にはif
でも上記のように書けそうに見えます。
しかしRustのバイナリオペレーション==
はそのような式には対応していません。
ではどうやって条件分岐したらいいのか? というところですが、こういう場合はmatch
を使います。
enumのmatchでの参照
enum
のインスタンスはmatch
で参照できます。
let cat = Animal::Cat;
let dog = Animal::Dog;
match cat {
Animal::Cat => {
println!("Cat!");
}
_ => {}
}
// Cat!
match dog {
Animal::Dog => {
println!("Dog!");
}
_ => {}
}
// Dog!
上記のようにmatch
の分岐でAnimal::Cat
などの列挙子を指定します。
そうすると変数がその列挙子にマッチしたときにその分岐に移行します。
match
の実用的な使い方としては何の列挙子が返ってくるかわからない関数の返り値を処理する時などが例として挙げられます。
// 引数sに応じてenumのAnimalを返す
fn judge(s: &str) -> Animal {
match s {
"cat" => Animal::Cat,
"dog" => Animal::Dog,
&_ => Animal::Cat,
}
}
fn test() {
// Animalを返すjudge()の呼び出し
let var = judge("cat");
// 何の列挙子が返ってきたか調べる
match var {
Animal::Cat => {
println!("Cat!");
}
Animal::Dog => {
println!("Dog!");
}
}
}
上記の場合はjudge()
関数は引数の文字列リテラルに応じてAnimal
列挙子を返します。
judge()
の呼び出し側ではjudge()
の返り値をmatch
でマッチして、出力します。
enumにデータを持たせる
ここからRustのenum
のパワフルなところを見ていきます。
Rustのenum
にはデータを持たせることができます。
C/C++などではenum
と合わせてデータを持たせたい場合はenum
を構造体でラップして個別に変数を定義したりします。
しかしRustのenum
の場合はenum
だけで完結できます。
たとえば列挙子にString
を持たせたい場合は以下のように定義します。
// ロボットを表すenum
enum Robot {
MyRobo(String),
OtherRobo(String),
}
この定義では列挙子を変数にするときに文字列を変数に格納することができます。
let myrobo = Robot::MyRobo(String::from("myrobo"));
let otherrobo = Robot::OtherRobo(String::from("otherrobo"));
このenum
の大事な点はenum
の列挙子としての性格はそのままだということです。
つまりmatch
などでは列挙子でマッチさせることができます。
変数に文字列を入れているんだからif
などで文字列と比較できるのか?
というところですが、これはできません。
if myrobo == "myrobo" {
}
// error[E0369]: binary operation `==` cannot be applied to type `Robot`
変数myrobo
はあくまで列挙子ですので、文字列ではないからです。
またmatch
で文字列とマッチさせることもできません。
match myrobo {
"myrobo" => {
println!("MyRobo!");
}
}
// error[E0308]: mismatched types. expected `Robot`, found `&str`
myrobo
は列挙子です。
そのためmatch
でマッチさせる場合は以下のようにコードを書きます。
match myrobo {
Robot::MyRobo(inner_string) => {
println!("{inner_string}");
}
_ => {}
}
// myrobo
match otherrobo {
Robot::OtherRobo(inner_string) => {
println!("{inner_string}");
}
_ => {}
}
// otherrobo
inner_string
というのが列挙子が持っている文字列のデータです。
match
で参照するときにこのようにデータの識別子を書いておくことで参照できるようになります。
enumに複数のデータを持たせる
Rustのenum
には複数のデータを持たせることができます。
先ほどはString
1つだけでしたが以下のように複数の型を指定できます。
// クラスの年組を表すenum
enum Classroom {
Sakura(usize, usize),
Ume(usize, usize),
}
上記の例ではusize
だけにしていますが、もちろんusize
の他の型も指定できます。
また複数の型を混合して定義することもできます。
列挙子をインスタンスにするときにデータを渡して紐づけます。
これはString
の場合と一緒です。
let sakura = Classroom::Sakura(3, 2); // 桜 3年2組
let ume = Classroom::Ume(2, 7); // 梅 2年7組
match sakura {
Classroom::Sakura(nen, kumi) => {
println!("桜 {nen}年{kumi}組");
}
_ => {}
}
// 桜 3年2組
match ume {
Classroom::Ume(nen, kumi) => {
println!("梅 {nen}年{kumi}組");
}
_ => {}
}
// 梅 2年7組
列挙子としての表現の他にデータを紐づけられるので、Rustのenum
はかなり表現力が高いと言えます。
enumを使ったエラーハンドリング
Rustではエラーハンドリングをするときにenum
を使うことができます。
enum
にはデータをバインドできますので、これによって多彩なエラー表現が可能になります。
// enumでErrorを定義
enum Error {
InvalidArg(String), // エラー:不正な引数
}
fn func(n: i32) -> Result<i32, Error> {
if n < 0 {
// nが0より下ならエラーにする
return Err(Error::InvalidArg(format!("{n} is under 0")));
}
Ok(n * 2)
}
fn test() {
// エラーが起こる可能性のある関数を呼び出し
let result = func(-1);
// エラーハンドリング
match result {
Err(err) => {
// 列挙子からデータ(エラーメッセージ)を取り出す
match err {
Error::InvalidArg(s) => {
println!("Error! {}", s);
}
}
}
Ok(n) => {
println!("{n}");
}
}
}
まずエラーを表すenum
ですが以下のように定義します。
// enumでErrorを定義
enum Error {
InvalidArg(String), // エラー:不正な引数
}
列挙子InvalidArg
はString
を持てます。
次にこのError
を使った関数を定義します。
fn func(n: i32) -> Result<i32, Error> {
if n < 0 {
// nが0より下ならエラーにする
return Err(Error::InvalidArg(format!("{n} is under 0")));
}
Ok(n * 2)
}
Result<i32, Error>
は成功時にi32
を、エラー時にError
を返す返り値の型です。
関数の引数n
が0
より下ならErr()
でエラーを返します。
Err(Error::InvalidArg(format!("{n} is under 0")));
Error::InvalidArg
にはformat!()
で整形した文字列を渡しています。
こうすることで引数n
の値がエラーハンドリング時にわかるようになります。
また、Error
の列挙子にn
を格納するデータを持たせてもいいでしょう。
この辺は設計者の思想によって変わるかと思います。
// エラーが起こる可能性のある関数を呼び出し
let result = func(-1);
// エラーハンドリング
match result {
Err(err) => {
// 列挙子からデータ(エラーメッセージ)を取り出す
match err {
Error::InvalidArg(s) => {
println!("Error! {}", s);
}
}
}
Ok(n) => {
println!("{n}");
}
}
Result
を返す関数の返り値はmatch
でエラーハンドリングできます。
上記ではまずresult
をmatch
で分岐して成功時にはn
の値を出力し、エラー時にはエラーメッセージを出力します。
Err(err)
で取り出しているerr
は列挙子のインスタンスですのでこの段階では文字列はまだ取り出していません。
エラー文字列を取り出したい場合はさらにmatch
を入れ子にしてerr
からs
を取り出します。
この表現は実質、他言語の例外処理に近しいかまたは同等になると言えるでしょう。
enumを文字列にする
enum
のインスタンスを文字列にしたい場合はstd::fmt::Display
を実装してto_string()
メソッドを実装します。
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidArg(msg) => write!(f, "Invalid argument: {}", msg),
}
}
}
fn test() {
let err = Error::InvalidArg(String::from("error!"));
println!("{}", err.to_string()); // Invalid argument: error!
}