トレイトで振る舞いを共有する

RustBeginner
オンラインで実践に進む

はじめに

関連のないデータ型でも、同じ振る舞いを保証する必要が生じることがあります。温室のセンサーと荷物は異なるフィールドを持ちますが、どちらも現在の状態を説明できます。Rust のトレイトを使うと、データの構造を同じにすることなく、その共通する機能に名前を付けられます。

この実験では、小さなトレイトを 1 つ定義し、用意された 2 つの構造体に実装して、両方の値を 1 つのジェネリックなレポート関数に渡します。長い構造体定義とサンプルデータはすでに作成されているため、各編集作業に集中できます。

共通のステータス契約を定義する

このステップでは、それぞれは無関係でも共通して持つ振る舞いに名前を付けるトレイトを定義します。

用意されたパッケージへ移動し、完全なひな形を確認します。sed -n コマンドは、指定した行を編集せずに表示します。

cd /home/labex/project/status-reporter
sed -n '1,160p' src/main.rs

SensorShipment は異なるフィールドを持っています。用意されているテキストフィールドの型は &'static str です。これは、プログラム全体で有効な借用文字列リテラルを意味します。'static ライフタイム注釈はこのトレイトの学習内容ではないため、意図的に後回しにしています。ここでは編集したり、その意味を考えたりする必要はありません。

プレースホルダーの下には、show_status がすでにジェネリック関数として用意されています。T: Status を 3 つの部分に分けて読みましょう。T は具体的な型を表し、コロンは「満たす必要がある」という意味で、Status は必要なトレイトです。つまり、「型 TStatus を実装している場合に、任意の型 T に対して処理する」という意味です。この保証があるため、関数内で安全に item.status() を呼び出せます。

Rust がこの境界を理解するには、先にトレイトが存在していなければなりません。ソースを開きます。

nano src/main.rs

// STATUS_TRAIT を次の内容に置き換えます。

trait Status {
    fn status(&self) -> String;
}

メソッドの行がセミコロンで終わっているのは、トレイトが必要なメソッドを宣言するだけで、具体的な実装は選ばないためです。&self を使うと、所有権を取得せずに値を調べられます。String によって、それぞれの型が独自のステータス文字列を作成できます。後で行う各実装では、このメソッド名、レシーバー、戻り値の型を正確に繰り返し、その型に合わせた本体を記述する必要があります。

Ctrl+O で保存し、Enter を押してから、Ctrl+X で終了します。続いて、パッケージを確認します。

cargo check

まだトレイトや用意された項目を使用していないため、警告が表示される場合があります。重要なのは、最後に Finished の行が表示されることです。これは、共通の契約とジェネリック境界が有効であることを示します。次の 2 つのステップで、実装と呼び出しを追加します。

Sensor に Status を実装する

このステップでは、Sensor に対してトレイト契約を満たし、センサーを共通のレポーターに渡します。

impl Trait for Type ブロックは、既存のトレイトを 1 つの具体的な型に関連付けます。メソッドのシグネチャはトレイトと一致している必要がありますが、本体ではその型独自のフィールドを使用できます。

ソースを開きます。

nano src/main.rs

// SENSOR_IMPLEMENTATION を、次の短い実装に置き換えます。

impl Status for Sensor {
    fn status(&self) -> String {
        format!("{}: {}°C", self.location, self.celsius)
    }
}

format!String を作成して返します。単独では出力しません。self が借用された Sensor であるため、この実装では locationcelsius を使用できます。

次に、main 内の // SENSOR_REPORT を次の内容に置き換えます。

    show_status("Sensor", &sensor);

&sensor 引数は、レポーターが受け取る借用された &T パラメーターに一致するため、sensor の所有権は main が保持したままになります。保存して Nano を終了し、プログラムを実行します。

cargo run --quiet
Sensor: Greenhouse: 24°C

この行は、SensorStatus を満たし、トレイト境界付きのジェネリックレポーターに渡せることを示しています。まだ Shipment は接続されていません。

Shipment に Status を実装する

このステップでは、同じトレイトを Shipment にも実装し、1 つのレポーターが両方の具体的な型を受け入れられることを確認します。

もう一度ソースを開きます。

nano src/main.rs

// SHIPMENT_IMPLEMENTATION を次の内容に置き換えます。

impl Status for Shipment {
    fn status(&self) -> String {
        format!("#{} {}", self.id, self.stage)
    }
}

このメソッドも同じトレイト契約に従いますが、荷物独自の idstage フィールドを使用します。次に、// SHIPMENT_REPORT を次の内容に置き換えます。

    show_status("Shipment", &shipment);

保存して Nano を終了します。完成したパッケージを実行します。

cargo run --quiet
Sensor: Greenhouse: 24°C
Shipment: #204 in transit

どちらの行も show_status<T: Status> から出力されています。境界は関数が必要とする振る舞いを示し、各実装はそれぞれの値がその振る舞いをどのように提供するかを決めます。

まとめ

トレイトを使って共通の振る舞いを定義し、それを 2 つの構造体に独立して実装しました。また、基本的なトレイト境界を使って、1 つのジェネリックレポーターが両方の型を受け入れられるようにしました。