検索
連載

構造体を超える構造体――Rustの構造体と列挙型を理解する基本からしっかり学ぶRust入門(7)

Rustについて基本からしっかり学んでいく本連載。第7回は、Rustの「構造体」と「列挙型」について。

Share
Tweet
LINE
Hatena
「基礎からしっかり学ぶRust入門」のインデックス

連載:基礎からしっかり学ぶRust入門

 Rustについて基本からしっかり学んでいく本連載。今回は、構造体と列挙型などについて紹介していきます。

 本連載のサンプルコードをGitHubで公開しています。こちらからダウンロードしてみてください。具体的な利用方法は連載第1回を参考にしてください。


構造体とは?

 構造体は、C/C++ではおなじみの言語要素です。基本的な役割も一緒です。本連載2回目でタプルについて紹介しましたが、タプルは異なるデータ型の値を同時に持てるだけだったのに対して、構造体はそれらにも名前を付けて区別できる点が異なります。

構造体の定義

 早速、構造体の定義を見てみましょう。構造体は、以下のようにstruct文で定義します。

struct Person {         // 構造体の名前は「Person」
    name: String,       // String型のフィールド「name」
    birth: u32,         // 以下も同様
    sex: char,
    height: f64,
    weight: f64,
}
src/bin/struct1.rsのソースコード

 これは、Person構造体の定義です。中かっこ({ })の中に構造体を構成する要素(これをフィールドと呼びます)を並べていきます。

 フィールドとは、名前とデータ型の組み合わせです。これを、カンマで区切って必要なだけ並べていきます。この例では、

  • String型のname
  • u32型のbirth
  • char型のsex
  • f64型のheightとweight

を持つ構造体Personが定義されたことになります。

 最後のフィールドのあとのカンマはなくても間違いではありませんが、このように付けても構いません。あとからフィールドを加える際にカンマ漏れをなくすには、最後のフィールドにもカンマを付けるのが望ましいでしょう。

構造体のインスタンス化

 構造体を実体化することを、インスタンス化といいます。オブジェクト指向プログラミングにおいてクラスからオブジェクトを生成する「インスタンス化する」と同じ意味です。構造体のインスタンス化は、以下のように行います。ここでも、最後のフィールドのカンマが許容されます。これは構造体に限らず、Rustの共通した振る舞いです。

let nao = Person {
    birth: 1945,
    height: 160.0,
    name: String::from("山内直"),
    sex: 'm',
    weight: 80.0,
};
src/bin/struct1.rsのソースコード

 この例では、

  • 変数naoに対する型修飾が省略されていて、代わりにPerson構造体による値の初期化が行われていること
  • 初期化の順序が構造体の宣言と同じでなくてもよい

点に注目してください。インスタンス化においては、それぞれのフィールドの初期値を、任意の順番で指定できます。ただし、フィールド名を明示する必要があります。

 なお、初期化は全てのフィールドで必要です。基本的に、全部または一部を省略した場合にはコンパイルエラーとなります。

 上記は構造体をインスタンス化し、変数naoを初期化しましたが、以下のように構造体そのものを返す関数も定義できます。

fn main() {
    let nao = create_person(&"山内直".to_string(), 1960, 'f');
}
// Person構造体を返す関数
fn create_person(name: &String, birth: u32, sex: char) -> Person {
    Person {
        name: name.to_string(),
        birth: birth,
        sex: sex,
        height: 0.0,
        weight: 0.0,
    }
}
src/bin/struct2.rsのソースコード

 上記で注目すべきは、フィールド名と関数の仮引数の名前が同じでもよい、ということです。構造体の初期化では、コロン(:)の前はフィールド名というように認識されるので、名前が一致しても問題ないのです(もちろん、一致しないように書くことも問題ありません)。

 ただし、同じものを2回ずつ書かなければならないのは冗長です。このような場合は、初期化省略記法を用いて記述を簡略化できます。例えば、上記の例なら以下のように書き換えられます。

fn create_person(name: &String, birth: u32, sex: char) -> Person {
    Person {
        name: name.to_string(),
        birth,  // フィールド名だけを指定
        sex,    // フィールド名だけを指定
        height: 0.0,
        weight: 0.0,
    }
}
src/bin/struct3.rsのソースコード

 関数の仮引数にあるbirth, sexについて、初期化部分でその記述がコロンごと省略されています(nameについては、参照から文字列を生成するので、省略できません)。このように、フィールド名と関数の仮引数の名前が一致する場合、フィールド名だけ書いておけば同名の仮引数を自動的に参照します。

 初期化省略記法はこれだけではありません。先ほど、初期化においては全てのフィールドの指定が必要と書きましたが、以下のように他のインスタンスを参照するという形で、フィールドの初期化が省略できます。

let someone = Person {
    name: "名無しの権兵衛".to_string(),
    birth: 1945,
    sex: 'm',
    height: 160.0,
    weight: 80.0,
};
let nao = Person {
    name: "山内直".to_string(),
    birth: 1960,
    ..someone                   // インスタンスsomeoneを参照する
};
src/bin/struct4.rsのソースコード

 他のインスタンスの参照は、「..」に続けて変数名を記述します。この例では、nameとbirthは明示的に初期化されるので、残りのsex, height, weightをsomeoneから持ってきます。なお、このような記法を用いた場合に限っては、最後にカンマを置くとコンパイルエラーとなるので注意してください。

フィールドへのアクセス

 構造体をインスタンス化したら、それぞれのフィールドにはドット演算子(.)を使ってアクセスできます。

println!("名前は {} です。", nao.name);

 上記の例ではnaoはimmutable(不変)でしたが、mutable(可変)にすることで値の書き換えもできます。フィールド単位でのmutableとimmutableの使い分けはできません。

let mut nao = Person {
  …中略…
};
nao.sex = 'f';
println!("性別は {} です。", nao.sex);

【補足】構造体の参照によるアクセス

 構造体のインスタンスへの参照というものも、もちろんあります。参照によるフィールドのアクセスも、同じくドット演算子を使います。ポインタではアロー演算子(->)を使うといったC/C++のような区別はありません。これは、Rustには自動参照と参照外しという機能があるからです。

メソッドとは?

 構造体に実装した関数は、特別にメソッドと呼ばれます。これまでも、関数とメソッドの双方の呼称を用いてきましたが、メソッドは構造体にひも付けられている関数、ということになるわけです。この考え方も、オブジェクト指向プログラミングにおけるメソッドと同じです。

メソッドの実装

 メソッドは、impl文(implement〈実装〉の意)を使って実装します。フィールドはstruct文の中に記述しましたが、メソッドはstruct文の外に記述します。

struct Person {                 (1)
  …中略…
}
impl Person {                   (2)
    fn bmi(&self) -> f64 {
      self.weight /  ((self.height / 100.0) * (self.height / 100.0))
    }
}
fn main() {
    let nao = Person {
        name: "山内直".to_string()),
        birth: 1945,
        sex: 'm',
        height: 160.0,
        weight: 80.0,
    };
    println!("{}さんのBMIは{}です。", nao.name, nao.bmi());      (3)
    // 「山内直さんのBMIは31.249999999999993です。」
}
src/bin/struct_method.rsのソースコード

 (1)が構造体の定義、(2)がメソッドの実装です。implキーワードに続けて、メソッドを実装する構造体の名前を指定します。中かっこ({ })で囲った中に、メソッドの実装を記述していきますが、形式は通常の関数定義と同じくfnキーワードから始めます。

 通常の関数定義と異なるのは、

  • 引数に「&self」が必要なこと
  • メソッド内でフィールドを参照する際には、そのselfを使って行う

という点です。引数の&selfは、&が付いていることから参照となりますが、その実体はselfすなわちインスタンス自身です。また、フィールドの前のself.は、構造体内部のスコープであることを明示します。

 (3)では、「nao.name」にてnameフィールドを直接参照し、「nao.bmi()」でメソッドを呼び出していますが、引数には何も指定していないことに注意してください。これはつまり、&selfは暗黙の引数で、自動的に渡されるということです。

【補足】&selfの記法

 メソッドの引数の&selfは、一種のシンタックスシュガーです。正確には「self: &Self」と記述します。Self型への参照型変数となるわけです。この場合のSelfは、インスタンスを生成した構造体の型となります。

関連関数

 関連関数は、構造体にひも付けられているが、その呼び出しにおいて構造体のインスタンスが不要という関数です。C++/Javaにおける静的メソッド(staticなメソッド)に相当するものです。これまで、String型変数の初期化にString::from()というメソッドを使用してきましたが、これが関連関数です。関連関数はよくインスタンスの生成、すなわちコンストラクタとして使用します。

 関連関数を定義するには、通常のメソッドから引数の&selfを省略するだけです。以下は、これまで関数で行っていたPerson構造体の生成を関連関数で実装し直した例です。

impl Person {
    fn from_str(name: &str, birth: u32, sex: char) -> Person {          (1)
        Person {
            name: name.to_string(),
            birth,
            sex,
            height: 0.0,
            weight: 0.0,
        }
    }
}
fn main() {
    let nao = Person::from_str("山内直", 1945, 'm');    (2)
}
src/bin/relation_method.rsのソースコード

 (1)が定義、(2)が呼び出しです。引数に&selfはなく、呼び出し時にPerson::として構造体名を指定していることに注意してください。これは、from_str()メソッドが関連関数であり、Person構造体にひも付けられていることから必要な形式です。

特殊な構造体

 ここまで、Rustの構造体について、基本的な部分を取り上げてきました。このほかRustには、タプル構造体やユニット様構造体など、特殊な位置付けの構造体があります。

タプル構造体

 タプル構造体(Tuple Structs)は、タプルと構造体の中間のような構造体です。タプルの各値にはデータ型がありますが名前はなく、タプル自体にも名前はありません。タプル構造体は、タプルに名前を付けたようなもので、構造体のフィールド名自体にはさほど意味がないようなときに使い勝手の良い構造体となります。

struct Ipv4(u8, u8, u8, u8);
let address = Ipv4(192, 168, 1, 100);
src/bin/tuple_struct.rsのソースコード

 上記では、u8型のフィールドを4つ持つ構造体Ipv4を定義しています。これは、IPアドレスを保持するための構造体ですが、各フィールドの名前には意味がありません。インスタンスの生成も、定義と同じ順番で値を渡して行います。

ユニット様構造体

 ユニット様構造体(Unit-like Structs)とは、その名の通りユニット( () )のような構造体のことです。ユニットが、何も値を持たないのと同様に、ユニット様構造体には、フィールドが一切ありません。このような構造体が何の役に立つのかということですが、戻り値のない関数の暗黙の戻り値をユニットが代行しているのと同じように、構造体に値がまったくないときやトレイトの実装で役立ちます。詳しくは、トレイトを取り上げる回で紹介します。

列挙型

 構造体とは少し異なりますが、列挙型も取り上げておきます。列挙型はC/C++でenumと呼ばれていたものですが、Rustでも同様に値を列挙した型として使用できます。列挙型を活用すると、構造体を使うような場面をもっとスッキリと記述できます。

列挙子

 列挙型では、値そのものには意味はなく、区別できることが重要です。その値は列挙子と言います。enum文の中に列挙子を並べていくことで、列挙型を宣言します。以下は、Creatureという列挙型の使用例です。

enum Creature {
    ANIMAL,
    BIRD,
    INSECT,
    FISH,
}
fn main() {
    let lion = Creature::ANIMAL;
    match lion {
        Creature::ANIMAL => println!("ライオンはANIMALです。"),
        Creature::BIRD => println!("ライオンはBIRDです。"),
        Creature::INSECT => println!("ライオンはINSECTです。"),
        Creature::FISH => println!("ライオンはFISHです。"),
        _ => println!("どれでもないみたいです。"),
    }
        // 「ライオンはANIMALです。」
}
src/bin/enum.rsのソースコード

 列挙型Creatureの列挙子は、ANIMAL, BIRD, INSECT, FISHの4個です。main()関数の中では、変数lionをCreature::ANIMALで初期化し、そのあとmatch式によるマッチングを行っています。当然ながら、1番目の文がマッチします。ここで重要なのは、ANIMALが単なる定数ではなく、Creatureという型にひも付けられた値ということです。Creature::ANIMALは、Creatureによって他の名前空間から分けられます。これが列挙型の基本的な活用法です。

列挙子にデータを格納する

 Rustでは、列挙子にデータそのものを持たせることができるようになっています。これにより、複数の異なる構造体を一つの列挙型で使い分ける、といったC/C++における共用体のようなことが可能になります。以下は列挙体であるShapeが、その形状(列挙子)に応じた値を別々に保持しています。

fn main() {
    enum Shape {
        None,
        Line {x1: i32, y1: i32, x2: i32, y2: i32},
        Circle {x: i32, y: i32, r: i32},
        Rect(i32, i32, i32, i32),
        Text(String),
    }
    let _n = Shape::None;
    let _l = Shape::Line {x1: 100, y1: 200, x2: 300, y2: 400};
    let _c = Shape::Circle {x: 100, y: 200, r: 50};
    let _r = Shape::Rect(100, 200, 300, 400);
    let _t = Shape::Text(String::from("Hello."));
}
src/bin/enum_data.rsのソースコード

 ここでは、列挙型の柔軟性を見てもらうために、いろいろな方法で構造を定義しています。Noneには構造はありません。上記で見てきたCreatureと同じです。LineとCircleは構造体すなわちstructとして定義したのと同じです(構造体の名前がないので、匿名構造体といいます)。RectとTextは値を単純に含むタプル構造体です。

 変数の初期化は、それぞれの形式に基づいて行います。LineとCircleは、構造体のインスタンス化と同じ書式となります。RectとTextは、タプルの初期化と同じです。

列挙子にメソッドを定義する

 列挙型は、構造体と同様にメソッドを持つことができます。定義の方法も同じで、impl文を使用します。以下はその例で、Shape列挙子にdraw()というメソッドを定義して、呼び出しています。

impl Shape {
    fn draw(self: &Self) {
        match self {
            Shape::None => println!("None"),
            Shape::Line {x1, y1, x2, y2} => 
                println!("Line: ({}, {})-({}, {})", x1, y1, x2, y2),
            Shape::Circle {x, y, r} => println!("Circle: ({}, {}), {}", x, y, r),
            Shape::Rect(x1, y1, x2, y2) => 
                println!("Rect: ({}, {})-({}, {})", x1, y1, x2, y2),
            Shape::Text(s) => println!("Text: {}", s),
        }
    }
}
let n = Shape::None;
let l = Shape::Line {x1: 100, y1: 200, x2: 300, y2: 400};
let c = Shape::Circle {x: 100, y: 200, r: 50};
let r = Shape::Rect(100, 200, 300, 400);
let t = Shape::Text(String::from("Hello."));
n.draw();       // None
l.draw();       // Line: (100, 200)-(300, 400)
c.draw();       // Circle: (100, 200), 50
r.draw();       // Rect: (100, 200)-(300, 400)
t.draw();       // Text: Hello.
src/bin/enum_method.rsのソースコード

 draw()メソッドの中では、match式によるパターンマッチングを行い、列挙子の値に応じた表示を行っています。同じメソッドの呼び出しでもインスタンスに応じた動きにできることから、オブジェクト指向プログラミングにおける抽象メソッドのような振る舞いを実装できます。

まとめ

 今回は、既存の型のデータを集めて新たな型を定義できる構造体と列挙型を紹介しました。これらにはメソッドを定義できるので、オブジェクト指向プログラミングっぽいことができるのを理解していただけたと思います。

 次回は、実用的なプログラミングでは避けて通れないエラー処理を紹介します。

筆者紹介

WINGSプロジェクト

有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。

サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/
RSS(https://wings.msn.to/contents/rss.php
Twitter: @yyamada(https://twitter.com/yyamada
Facebook(https://www.facebook.com/WINGSProject


Copyright © ITmedia, Inc. All Rights Reserved.

ページトップに戻る