Skip to content

Instantly share code, notes, and snippets.

@topecongiro
Created December 2, 2019 14:37
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save topecongiro/1a2506be2bb0f879d211b8a80e0bdaaf to your computer and use it in GitHub Desktop.
Save topecongiro/1a2506be2bb0f879d211b8a80e0bdaaf to your computer and use it in GitHub Desktop.
Rust Advent Calendar 2019 の 2日目の記事

Rust の Deref と DerefMut で継承ができると思わない

Rust でオレオレ文字列型を定義して既存コードに散りばめられた String を置き換えたい気持ちになることがあります。

単純に API を追加して事足りる場合、必要な API をトレイトで定義した上で String に実装すれば十分です:

trait PushNewLine {
    fn push_newline(&mut self);
}

impl PushNewLine for String {
    fn push_newline(&mut self) {
        self.push('\n');
    }
}

しかし、「特定の処理を O(1) で実行できる文字列型が欲しい」といったことを考え始めると、自前の型を定義して必要な処理を書く他ありません。

例えば、「1行の長さが100 [0] を超える行が存在するかどうか」を文字列の長さ N に対して O(1) で判定できる文字列型を考えます。

Rust の String 型の場合、上の条件を判定する関数は以下のように書くことができます [1]:

trait LineWidthExceeding100 {
    fn has_line_exceeding_100(&self) -> bool;
}

impl<T: AsRef<str>> LineWidthExceeds100 for T {
    fn has_line_exceeding_100(&self) -> bool {
        self.as_ref().lines().any(|line| line.len() > 100)
    }
}

残念ながら String::has_line_exceeding_100 の計算量は O(N) です。

O(1) で判定できるよう、文字列を追加する際に改行の有無をチェックする文字列型を考えます:

#[derive(Default)]
struct MyString {
    inner: String,
    cur_line_width: usize,
    has_line_exceeding_100: bool,
}

impl LineWidthExceeds100 for MyString {
    fn has_line_exceeding_100(&self) -> bool {
        self.has_line_exceeding_100
    }
}

impl MyString {
    fn push_str(&mut self, s: &str) {
        for c in s.chars() {
            match c {
                '\n' => self.cur_line_width = 0,
                _ => {
                    self.cur_line_width += 1;
                    self.has_line_exceeding_100 = self.cur_line_width > 100;
                }
            }
            self.inner.push(c);
        }
    }
}

文字列の更新を push_str に限定すれば、上記の実装で O(1) の判定が可能です。あとは既存のコード中の StringMyString で置き換えれば万事解決...というわけにはいきません。

最初の例では String に直接トレイトを実装したため、既存コードへの変更は該当トレイトのインポートだけで完了します。 ところが、二つ目の例では MyString という新しい型を定義しているため、単純に置き換えただけでは MyString から直接 String のメソッドを参照することができません。このままでは大量のコンパイルエラーが発生します。

必要なメソッドを再定義するか as_str(), as_mut_str() のようなメソッドを定義して String への参照を提供する必要があります:

// 1 つ目の方法
impl MyString {
    fn width(&self) -> usize { self.inner.width }
    fn trim(&self) -> &str { self.inner.trim() }
    // ...必要なメソッドをすべて再定義する
}

// 2 つ目の方法
impl MyString {
    fn as_str(&self) -> &str { &self.inner }
    fn as_mut_str(&mut self) -> &mut String { &mut self.inner }
}
// ...このあと MyString から String のメソッドを参照したい場所で `.as_str()` や `as_mut_str()` を追記する

いずれの方法をとっても、大量の boilerplate が要求されます。つらい。

Rust には継承がないので、MyString extends String のようには書けません。Boilerplate 無しですませるためにはさくっと &MyString から &str, &mut MyString から &mut str に暗黙的に変換してくれる機能が必要ですが、i32 から u32 への変換すら明示的に行う必要がある Rust でそのような都合の良い機能があるのでしょうか?

DerefDerefMut

ありました。それが DerefDerefMut です。ドキュメント(の一部) を見てみると

  • Deref<Target = U> を実装する型 T は暗黙的に U のメソッドを実装する

とあります。つまり、impl Deref<Target = String> for MyString を実装してしまえば、暗黙的に MyString から String のメソッドを呼ぶことができます!

#[derive(Default)]
struct MyString {
    inner: String,
}

impl Deref for MyString {
    type Target = String;
    fn deref(&self) -> &Self::Target { &self.inner }
}

impl DefefMut for MyString {
    fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
}

fn main() {
    let s = MyString::default();
    println!("s.len() = {}", s.len()); // => s.len = 0
}

また メソッドのオーバーライド もできます:

impl MyString {
    fn push_str(&mut self, s: &str) {
        println!("MyString::push_str");
        self.inner.push_str(s);
    }
}

fn main() {
    let mut s = MyString::default();
    s.push_str("hello, world"); // => "MyString::push_str"
    println!("{}", s.len()); // => 12
}

上の例では push_strMyString のものが、lenString のものが使われています。これで既存のコードへの変更量を最小限にしつつコンパイラを黙らせることができそうです。最高ですね 🎉

DerefDerefMut テイク2

もう一度 Deref のドキュメントを読んでみます:

  • 混乱を避けるために Deref はスマートポインタにしか定義しないでください

はい、ごめんなさい。先ほどのコード例とよく似たものを考えてみます:

fn main() {
    let mut s = MyString::default();
    s.push_str("hello, world"); // => "MyString::push_str"
    println!("{}", s.len()); // => 12

    let mut s = s.clone(); // ???
    s.push_str("hello, world"); // => 何も表示されない
    println!("{}", s.len()); // => 24
}

この例では MyString 型の s に対して clone() を呼んでいます。MyStringClone トレイトを実装していないため、暗黙的に String::clone を呼び出します。そのため、sMyString ではなく String になります。最低ですね 🤯

上述したように、もともと DerefDerefMut はスマートポインタのために設計されたトレイトです。具体的にはポインタ型 P<T>T に変換するためのものです。これらのトレイトがないと、例えば &mut T の型を持つ値を &T を引数にとる関数に渡すたびに、都度明示的な変換が必要になります。それはさすがにやっていられないので、 Deref には通常の Rust の文脈では見られない強力な柔軟性が許されています。任意の型から任意の型への変換を自由に許すためのものではありません。利用者の良心が試されます。

非公式の Rust アンチパタン集にも Deref を使った疑似的な継承がアンチパタンとして掲載されています。止めましょう。頑張って boilerplate 書きましょう。

DerefDerefMut テイク3

とはいえ 大量の boilerplate を書き終えてから cargo test を実行 -> 実は自分のアプローチではうまくいかないことが判明 など発生すると大変つらいため、一時的な動作確認として Deref を使ってコンパイラを騙すのはありかもしれません。

DerefDerefMut テイク4

Deref によるややこしさの例として、もう一つ Rc を見てみます。

Rc のドキュメントを見てみると、Rc のトレイト実装以外のメソッドは全て self をとらない形で定義されています。 つまり rc_val.try_unwrap() ではなく Rc::try_unwrap(rc_val) の形で呼ぶ必要があります。これは Rc<T>Deref<Target = T> を実装しているため、普通にメソッドとして定義してしまうと T のメソッドを上書きしてしまうためです。

Clone トレイトなど &self を受け取るメソッドとしてでしか定義できない場合もあります。このようなケースでも、Rc::clone の形で呼び出す方が分かりやすいでしょう (clippy 調べ)。

まとめ

DerefDerefMut を使って継承を実装しようとしない。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment