はじめての Rust 入門 Part5 ~列挙型について学ぶまで~
link です。
高速でセキュリティ的にも安全な言語として Rust が注目を集めています。
今回はそんな Rust の勉強をしていきます。
本記事は Part4 の続きになっています。
想定環境
- Windows 11
- Rust 1.72
列挙型
Rust の列挙型は C 言語などと同様 enum
で宣言されます。
IP アドレスを使って、列挙型の例を記述します。
enum IpAddrKind {
V4,
V6,
}
IP アドレスは V4 か V6 のいずれかのバージョンです。 このように複数の値のうち、いずれか 1 つの値しか取りえない場合に列挙型は有効です。
IpAddrKind
を例にとると、列挙子のインスタンス生成は以下のようにできます。
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
列挙子のインスタンスは、 列挙型名::列挙子名
で表されます。
こうすることで、IpAddrKind::V4
と IpAddrKind::V6
という値は両方とも同じ型 IpAddrKind
になります。
これで、どんな IpAddrKind
型の引数を取る関数も定義できるようになります。
fn route(ip_type: IpAddrKind) { }
route(IpAddrKind::V4);
route(IpAddrKind::V6);
この列挙型と前回学んだ構造体を使って、 IP アドレスのバージョンと値を保持する方法を考えると以下のようなコードが浮かぶと思います。
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"),
};
ここでは、IpAddrKind
型の kind
フィールドと String
型の address
の 2 つのフィールドを持つ IpAddr
という構造体を定義しています。
また、この構造体のインスタンスを V4 と V6 の 2 つ定義しています。
しかし、同じことをもっと簡潔な方法で表現できます。
以下に示したコードは各列挙型の列挙子に直接データを格納して、列挙型を構造体内に使うというよりも列挙型だけを使います。
この新しい IpAddr
の定義は、 V4 と V6 の列挙子両方に String
型の値が紐付けられていることを示しています。
enum IpAddr {
V4(String),
V6(String),
}
let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));
各列挙子にデータを直接添付できるので、余計な構造体を作る必要はまったくありません。
また、各列挙子に紐付けるデータの型と量は、異なっても問題ありません。
たとえば、バージョン 4 のIPアドレスには、常に 0 から 255 の値を持つ 4 つの数値があります。
そのため、 V4 のアドレスは 4 つの u8
型の値として格納できます。
しかし、 V6 のアドレスは 6 つの 16 進数を保存する必要があり、 ::
を使った省略表記も存在します。
そこで、 V6 のアドレスを引き続き、単独の String
型の値で格納したい場合にも列挙型は容易に対応できます。
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
また、列挙型に対応付ける値の型は構造体であっても問題ありません。
struct Ipv4Addr {
// 省略
}
struct Ipv6Addr {
// 省略
}
enum IpAddr {
V4(Ipv4Addr),
V6(Ipv6Addr),
}
列挙型はいかなる種類のデータでも格納できます。例を挙げれば、文字列、数値型、構造体などです。他の列挙型を含むことさえできます。
列挙型と構造体に似通っている点として、 impl
を使って、 enum
にもメソッドを定義できます。
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();
Option
Rust には null
はありませんが、値が存在するか不在かという概念をコード化する列挙型があります。
この列挙型が Option<T>
で以下のように標準ライブラリーに定義されています。
enum Option<T> {
Some(T),
None,
}
Option<T>
は初期状態で宣言されています。つまり、明示的にスコープに導入する必要がありません。さらに、列挙子の Some
と None
を Option::
の接頭辞なしに直接使えます。
<T>
はジェネリック型引数です。これについては別の記事で学ぶことになりますが、 <T>
は、 Option
の Some
列挙子があらゆる型のデータを 1 つだけ持つことができることを意味しています。
以下のコードは Option
を使って、数値型や文字列型を保持する例です。
let some_number = Some(5);
let some_string = Some("a string");
let absent_number: Option<i32> = None;
Some
ではなく、 None
を使ったら、コンパイラに Option<T>
の型が何になるかを教えなければいけません。
なぜなら、 None
値を見ただけでは、 Some
列挙子が保持する型をコンパイラが推論できないからです。
Option<T>
型の値がある時、その値を T
型に変換する必要があります。
どのように Some
列挙子から T
型の値を取り出せばいいのかですが、 match
式を使うことでOption<T>
型の値のデータを使用できます。
match 制御フロー演算子
Rust には、一連のパターンに対して値を比較し、マッチしたパターンに応じてコードを実行させてくれる match
と呼ばれる制御フロー演算子があります。
match
式は値が適合する最初のパターンのコードブロック内で使用されます。
以下のコードではどの種類のコインなのか決定し、その価値をセントで返す関数を記述しています。
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()
内の match
を見ていきます。まず、 match
に続けて値を並べています。
次は、match
アームです。 1 本のアームにはパターンと何らかのコードがあります。
今回の最初のアームは Coin::Penny
という値のパターンであり、パターンと動作するコードを区別する =>
演算子が続きます。
この場合のコードは、 1
です。各アームはカンマで区切られています。
この match
式が実行されると、結果の値を各アームのパターンと順番に比較します。パターンに値がマッチしたら、そのコードに紐付けられたコードが実行されます。
パターンが値にマッチしなければ、次のアームが継続して実行されます。必要なだけパターンは存在できます。
各アームに紐付けられるコードは式であり、マッチしたアームの式の結果が match
全体の戻り値になります。
マッチのアームで複数行のコードを走らせたい場合、{}
を使用できます。
たとえば、以下のコードは、メソッドが Coin::Penny
とともに呼び出されるたびに「Lucky penny!」と表示しつつ、1 を返します。
fn value_in_cents(coin: Coin) -> u32 {
match coin {
Coin::Penny => {
println!("Lucky penny!");
1
},
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
match
のアームの別の有益な機能は、パターンにマッチした値の一部に束縛できる点です。これを利用することで、列挙型の列挙子から値を取り出すことができます。
すなわち Option<T>
を使用する際に、 Some
から中身の T
の値を取得したい場合、 match
を使って Option<T>
から T
を取り出すことができます。
たとえば、 Option<i32>
を取る関数を書きたくなったとし、中に値があったら、その値に 1 を足すことにします。
中に値がなければ、関数は None
値を返し、何も処理を試みるべきではありません。
match
を使うことで以下のように書けます。
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
また、 Rust には、すべての場合の処理を列挙したくない時に使用できるパターンもあります。
たとえば、u8
の有効な値のうち、 1, 3, 5, 7 の値にだけ設定し、それ以外の場合の処理を _
に記述できます。
let some_u8_value = 0u8;
match some_u8_value {
1 => println!("one"),
3 => println!("three"),
5 => println!("five"),
7 => println!("seven"),
_ => (),
}
_
というパターンは、どんな値にもマッチします。他のアームの後に記述することで、 _
はそれまでに指定されていないすべての可能性にマッチします。
()
は、ただのユニット値ですので、 _
の場合には何も起こりません。結果として、 _
プレースホルダーの前に列挙していない場合すべてに対しては、何もしないようになります。
ですが、 1 つのケースにしか興味がないような場面では、match
式はちょっと長ったらしすぎます。このような場面用に Rustには、 if let
が用意されています。
if let 記法
if let
記法で if
と let
をより冗長性の少ない方法で組み合わせ、残りを無視しつつ、 1 つのパターンにマッチする値を扱うことができます。
Option<u8>
にマッチするけれど、値が 3 の時にだけコードを実行したい場合について考えます。
let some_u8_value = Some(0u8);
match some_u8_value {
Some(3) => println!("three"),
_ => (),
}
Some(3)
にマッチした時だけ何かをし、他の Some<u8>
値や None
値の時には何もしたくありません。
match
式を満たすためには、列挙子を 1 つだけ処理した後に _ => ()
を追加しなければなりません。
ですが、 if let
を使用すればもっと短く書くことができます。
if let Some(3) = some_u8_value {
println!("three");
}
if let
記法は等号記号で区切られたパターンと式を取り、式が match
に与えられ、パターンが最初のアームになった match
と同じ動作をします。
if let
では、 else
を含むこともできます。 else
に入るコードブロックは if let
と else
に等価な match
式の _
の場合に入るコードブロックと同じになります。
let mut count = 0;
if let Coin::Quarter(state) = coin {
println!("State quarter from {:?}!", state);
} else {
count += 1;
}
match
を使って表現するには冗長的すぎるロジックが必要なシチュエーションに遭遇したら、 if let
のことを思い出してください。
参考サイト
まとめ
今回は Rust の列挙型について学びました。
次回は Rust のコレクションについて学んでいきたいと思います。
それではまた、別の記事でお会いしましょう。