LoginSignup
1
1

Rustでも実は「継承」(に似たこと)ができる - 自動参照外しの活用

Posted at

概要

Rustには構造体の「継承」機能がありません。しかしながら、参照外しを利用することでそれに近い振る舞いを実装することができます。この記事では、そのような参照外しの活用例を紹介します。

自動参照外し

Rustにはオブジェクト指向言語でいうところの「継承(inheritance)」に相当する機能はありません。そのため、あるクラスを継承したサブクラスを実装して、親クラスのフィールドやメソッドを継承させることはできません。

プログラミング言語がオブジェクト指向言語となるために継承の機能を持つ必要があるなら、Rustはそうではありません。親構造体のフィールドやメソッドの実装をマクロを使わずに継承する構造体を定義する方法はありません。(https://doc.rust-lang.org/book/ch17-01-what-is-oo.html?highlight=inheritance#inheritance-as-a-type-system-and-as-code-sharing) 1

しかしながら実は、参照外し(dereference)とDerefトレイトを利用することで継承に近い振る舞いを実装することができます。Rustの実践的なテクニックを紹介した書籍Rust for Rustaceansでは以下のように述べられています。

Rustでは古典的な意味でのオブジェクト継承を実装することはできません。しかし、Derefトレイトとそれに近しいAsRefトレイトは継承と少し似た機能を提供します。これらのトレイトを使うことで、ある型TがDerefを実装している場合にT型の値からU型のメソッドを直接呼び出すことができます。これはユーザーには魔法のように感じられ、一般的には素晴らしいものです。(Gjengset, Jon. Rust for Rustaceans (p.40). No Starch Press. Kindle 版.) 2

変数のフィールドにアクセスする際、Rustのコンパイラは自動的に参照外しを行います。そのため、参照外しされた先の型のフィールドにもアクセスすることができます。

型がDerefやDerefMutを実装している場合、そのオペランドが可変かどうかに応じて、フィールドへのアクセスが可能になるまでに必要な回数だけ自動的に参照外しされます。この工程は略して自動参照外しとも呼ばれます。(https://doc.rust-lang.org/reference/expressions/field-expr.html#automatic-dereferencing) 3

またメソッド呼び出しについても同様に自動的に参照外しを行います。そのため、参照外しされた先の型のメソッドも呼び出すことができます。

呼び出しメソッドを検索する際に、メソッドを呼び出すためにレシーバが自動的に参照外しや借用されることがあります。( https://doc.rust-lang.org/reference/expressions/method-call-expr.html) 4

これらの特徴を利用して、継承に近い振る舞いを実装することができます。以下でその例を見ていきましょう。

自動参照外しの活用例

例えば、Vec<T>は以下のようにDerefトレイトを実装しており、&[T](スライス)への参照外しが可能です。

#[stable(feature = "rust1", since = "1.0.0")]
impl<T, A: Allocator> ops::Deref for Vec<T, A> {
    type Target = [T];

    #[inline]
    fn deref(&self) -> &[T] {
        unsafe { slice::from_raw_parts(self.as_ptr(), self.len) }
    }
}

そのため、Vec<T>型の変数からスライスのメソッドを呼び出すことができます。例えば、chunks()メソッドを使ってベクトルを部分に分割することができます。

fn main() {
    let v = vec![1, 2, 3, 4, 5];
    for chunk in v.chunks(2) {
        println!("{:?}", chunk);
    }
}

このようにして、Vec<T>型は実質的に[T]型のメソッドを継承しているように振る舞います。

また関数の引数に変数を渡す際にも自動参照外しが行われます。例えばSQLxクレートのTransaction型を見てみましょう。このTransaction型には以下のようにDerefトレイトが実装されていて、Connection型に参照外しできるようになっています。

impl<'c, DB> Deref for Transaction<'c, DB>
where
    DB: Database,
{
    type Target = DB::Connection;

    #[inline]
    fn deref(&self) -> &Self::Target {
        &self.connection
    }
}

impl<'c, DB> DerefMut for Transaction<'c, DB>
where
    DB: Database,
{
    #[inline]
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.connection
    }
}

このため、例えばConnectionを実装したMySqlConnection型の引数を取る関数にTransaction型の変数を渡すことができます。

async fn get_record(
    mysql_connection: &mut MySqlConnection,
) -> Result<Record, OpaqueError> {
    ...
}

async fn main() {
    let mut mysql_connection = MySqlConnectOptions::new()
        .host(...)
        .port(...)
        .username(...)
        .password(...)
        .connect()
        .await
        .unwrap();
    let mut transaction = mysql_connection.begin().await.unwrap();
    let record = get_record(&mut transaction).await.unwrap();
}

こちらも実質的にTransaction型はMySqlConnection型のサブクラスのように振る舞います。

注意点

この自動参照外しの活用はややトリッキーな使い方であるため、乱用するとコードを読む際に混乱を生む可能性があります。

最も大事な点として、これは予期されないイディオムです。将来のプログラマーがこのコードを読んだ際には、これが起こるとは予想していないでしょう。それは意図された形で(またはドキュメント化されているように)Derefトレイトを使っていないからです。また、このメカニズムは完全に暗黙的です。5

しかしながら、Rustの標準ライブラリや有名なクレートでもこの特徴を活用している場合があります。そのためこの自動参照外しの仕組みを理解しておくことは重要だと考えます。&[T]Vec<T>のように明確な関係がある場合には自動参照外しを活用しても問題ないでしょう。

またRust for Rustaceansではある型Tと参照を外した先の型Uが同じ名前のメソッドを持っている場合には注意が必要だと述べられています。その場合は参照外しした先の型の同名メソッドを優先して呼び出すように実装することが推奨されています。

ドットオペレーターとDerefの魔法は混乱を生んだり、予期せぬ結果をもたらすことがあります。例えば、値T型のメソッドがselfを引数に取るときなどです。T型の値tについて、t.frobnicate()が型Tをfrobnicateするのか、参照外しした先の型Uをfrobnicateするのかは明確ではありません。... しかし、もしあなたが実装した型がユーザーが制御する型に参照外しする場合、追加した固有メソッドはそのユーザーが制御する型にも存在するかもしれません。この場合は問題になります。そのため、このような場合は常にfn frobnicate(t: T)のような静的メソッドを使うことをお勧めします。そのようにすることで、t.frobnicate()は常にU::frobnicateを呼び出し、T::frobnicate(t)を使ってT自体をfrobnicateすることができます。(Gjengset, Jon. Rust for Rustaceans (p.41). No Starch Press. Kindle 版.) 6

  1. If a language must have inheritance to be an object-oriented language, then Rust is not one. There is no way to define a struct that inherits the parent struct’s fields and method implementations without using a macro.

    https://doc.rust-lang.org/book/ch17-01-what-is-oo.html?highlight=inheritance#inheritance-as-a-type-system-and-as-code-sharing

  2. Rust does not have object inheritance in the classical sense. However, the Deref trait and its cousin AsRef both provide something a little like inheritance. These traits allow you to have a value of type T and call methods on some type U by calling them directly on the T-typed value if T: Deref. This feels like magic to the user, and is generally great.

    Gjengset, Jon. Rust for Rustaceans (p.40). No Starch Press. Kindle 版.

  3. If the type of the container operand implements Deref or DerefMut depending on whether the operand is mutable, it is automatically dereferenced as many times as necessary to make the field access possible. This process is also called autoderef for short.

    https://doc.rust-lang.org/reference/expressions/field-expr.html#automatic-dereferencing

  4. When looking up a method call, the receiver may be automatically dereferenced or borrowed in order to call a method.

    https://doc.rust-lang.org/reference/expressions/method-call-expr.html

  5. Most importantly this is a surprising idiom - future programmers reading this in code will not expect this to happen. That’s because we are misusing the Deref trait rather than using it as intended (and documented, etc.). It’s also because the mechanism here is completely implicit.

    https://rust-unofficial.github.io/patterns/anti_patterns/deref.html

  6. The magic around the dot operator and Deref can get confusing and surprising when there are methods on T that take self. For example, given a value t: T, it is not clear whether t.frobnicate() frobnicates the T or the underlying U! ... But if your type dereferences to a user-controlled type, any inherent method you add may also exist on that user-controlled type, and thus cause issues. In these cases, favor static methods of the form fn frobnicate(t: T). That way, t.frobnicate() always calls U::frobnicate, and T::frobnicate(t) can be used to frobnicate the T itself.

    Gjengset, Jon. Rust for Rustaceans (p.41). No Starch Press. Kindle 版.

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1