👨‍🏫

Rustでクラスを表すと

2022/11/21に公開

本稿の目的

Rustに存在しない「クラス」をRustの既存機能の組み合わせとして表現することで、一般的なOOP言語とRustのデータ表現に対する考え方の違いと、各概念がどのように対応しているかを理解しやすくすることが主な目的です。

主な想定言語

C++, Java, JavaScript, Rubyのクラスを主に想定しています。

方針

サブタイピング

Rustにはごく限定的なサブタイピングしかないため、クラスのサブタイピングに相当する変換は明示的に .as_ref() / .as_mut() として表現します。

Deref / DerefMut を使うことで、このような振る舞いを部分的に再現できる場合もあります。ただし、この用途でDeref / DerefMutを使うのは推奨されていません。

カプセル化

一般的なOOP言語では継承関係に基づいたアクセス制御 (protectedなど) が行われますが、これをRustにマップするのは難しいため、本稿では扱いません。

クラスの基本パーツ

継承と仮想関数を表現するために、クラスを以下の3つのセットで表します。

  • trait MyClass ... アップキャスト・仮想関数用
  • trait MyClassExt ... 非仮想関数用
  • struct MyClassImpl ... データ定義用
pub trait MyClass: AsRef<MyClassImpl> + AsMut<MyClassImpl> {
    // アップキャスト
    fn as_my_class(&self) -> &dyn MyClass;
    fn as_my_class_mut(&mut self) -> &mut dyn MyClass;
    // 仮想関数
    fn method1(&self) {}
}

pub trait MyClassExt: MyClass {
    // 非仮想関数
    fn method2(&self) {}
}

impl<T: MyClass + ?Sized> MyClassExt for T {}

pub struct MyClassImpl {
    // フィールド
    field1: i32,
}

impl AsRef<MyClassImpl> for MyClassImpl { ... }
impl AsMut<MyClassImpl> for MyClassImpl { ... }
impl MyClass for MyClassImpl { ... }

インスタンスは Pointer<dyn MyClass> 型 (ただし Pointer<> は場面ごとの適切なポインタ型) の値です。

アップキャスト用のメソッドについて

これはtrait-to-trait upcastingが安定化されていないというRust側の技術的な事情により必要ですが、本質的には重要ではありません。

仮想関数について

仮想関数が必要なければそれは実質的には構造体です。 MyClass / MyClassExt は削除し、 MyClassImplMyClass と呼び替えることができます。

ただし、後述するダウンキャストは実質的に仮想関数相当の仕組みが必要です。

非仮想関数について

非仮想関数はvtableに入れる必要がないほか、サブクラスによる再実装を効果的に禁止するためにトレイトを分け、extension trait patternとして実装します。

別の方法として、非仮想関数を MyClassImpl のメソッドとして実装することも可能です。しかし、この場合はopen recursion (非仮想関数から仮想関数を呼び出す) は使えません。

非仮想関数が必要なければ、 MyClassExt は削除できます。

データ定義について

データ定義用の構造体は具象クラスではインスタンス化のために必要です。しかし、抽象クラスでは必要ない場合があります。

継承

継承はデータの継承とインターフェースの継承に分けられます。

データの継承

データの継承は特に変哲のないコンポジションによって行います。

pub struct MySubclassImpl {
    base: MyClassImpl,
    // 追加のフィールド
    field2: i32,
}

このコンポジションを as_ref / as_mut として実装します。

impl AsRef<MyClassImpl> for MySubclassImpl {}
impl AsMut<MyClassImpl> for MySubclassImpl {}

インターフェースの継承

インターフェースの継承を行うにはsupertrait boundに親クラスのトレイトを追加します。

pub trait MySubclass: MyClass + AsRef<MySubclassImpl> + AsMut<MySubclassImpl> {
    // 追加の仮想関数
    fn method2(&self) {}
}

Extension trait側には特に工夫は必要ありません。

抽象クラス

抽象クラスは impl MyClass for MyClassImpl を実装しないことで表現されます。

データ定義について

抽象クラスにおいて、データ定義が必要なければそれは実質的にはインターフェース (Rustでいうところのtrait) そのものです。 MyClassImpl は削除し、 MyClassImpl を返すために必要だった AsRef<MyClassImpl> + AsMut<MyClassImpl> boundも削除できます。

静的な定義

クラスは定義をグルーピングしアクセス制御をする単位としても使われることがありますが、Rustでは基本的にモジュールがその役割を負っています。

静的フィールド

要求する並列性に応じて適切なstatic変数の亜種を選択します。

静的メソッド

要は関数です。 impl 内の関数 (関連関数) としてマウントすることもできますが、トレイトのimpl fnにすると <dyn MyClass>::foo() となって不恰好になるため、 MyClassImpl::foo() とすることになるかもしれません。

コンストラクタ

Rustにコンストラクタはなく、通常 Self::new という名前をもつ静的なメソッドとして提供されます。これにより以下のように自由度が向上しています。

  • 異なる意図をもつコンストラクタを複数定義できる。これをコンストラクタオーバーロードにより実現可能にしている言語もあるが、オーバーロードでは区別しにくい組み合わせもある。
  • コンストラクタは self (相当の値) をラップした状態で返してもよい。たとえば、 Self のかわりに Arc<Self>Result<Self, E> を返してもよい。
  • 安全性を保ったまま、インスタンス生成前の処理を特別な制限なく記述できる。OOP言語ではオーバーロードされた別のコンストラクタへの移譲を行うときやスーパーコンストラクタを呼ぶ必要がある場合に、それらの処理より前にできることに制限があることがある。

継承もインスタンス化もしないクラス

それはモジュールです。よくある例として、 Math のような名前を持つクラスがよくこれに相当します。

ダウンキャスト

一般的なOOP言語ではスーパークラスからサブクラスへのダウンキャストが可能です。これはRustではtrait-to-trait downcastingに当たりますが、これをRustで実現するのは大変です。詳しくはRustのopen traitシステム dyno (RFC3192) を読むを参照してください。

また、ダウンキャストのユースケースのひとつとしてDOMのような例がありますが、このような例ではtraitよりもenumを使うほうが適切な可能性があります。詳しくはenumはクローズド直和、traitはオープン直和を参照してください。

仮想継承

ダイアモンド継承問題を解決するには仮想継承を実装する必要があります。そのためには仮想継承を行うクラスにおいて、structをさらに2つに分けることになるはずです。

  • MyClassImpl ... このクラスを継承したときに追加で宣言するべきフィールドのみを切り出したもの
  • MyClassDirectInstance ... このクラスを直接インスタンス化するのに必要な全ての構造体を合成したもの

ポインタ

一般的なOOP言語では「クラス」と言ったときに自動的にある種のポインタ的な挙動が仮定されます。 (重要な例外としてC++があります)

これには以下が含まることがあります。

  • null
  • 参照が共有される
  • ガベージコレクション

これらの機能が必要なときは、クラス型を適切にラップする必要があります。たとえば、 Option<Arc<dyn MyClass>> などです。

また、参照が共有される場合、Rustの強力なエイリアシングルールが実装上の困難になる場合があります。これには本質的な制限のほか、フィールド非交借用を型で抽象化する仕組みが未実装であるという技術的な制限が含まれます。対応として以下が考えられます。

  • Cell, RefCell, Mutex, RwLockなどの内部可変性コンテナやアトミック変数を活用する。
  • クラスフィールドの一部をいくつかまとめて別の構造体に切り出す。

まとめ

  • 有り体に言ってしまえば、クラスとはstruct + trait + module + pointer + Derefである。
  • structとしてのクラス
    • フィールド定義
    • 具象クラスの具象部分
    • 非仮想関数 (仮想関数を呼び出す必要がないもの)
    • staticメソッド
    • コンストラクタ
  • traitとしてのクラス
    • 仮想関数
    • 非仮想関数 (仮想関数を呼び出す必要があるもの)
    • ダウンキャスト
  • moduleとしてのクラス
    • staticフィールド
    • staticメソッド
    • カプセル化・アクセス制御
  • pointerとしてのクラス
    • null
    • 参照の共有
    • ガベージコレクション
  • Derefとしてのクラス
    • サブタイピング (暗黙のアップキャスト)

Discussion