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) の判定が可能です。あとは既存のコード中の String
を MyString
で置き換えれば万事解決...というわけにはいきません。
最初の例では 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 でそのような都合の良い機能があるのでしょうか?
ありました。それが Deref
と DerefMut
です。ドキュメント(の一部) を見てみると
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_str
は MyString
のものが、len
は String
のものが使われています。これで既存のコードへの変更量を最小限にしつつコンパイラを黙らせることができそうです。最高ですね 🎉
もう一度 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()
を呼んでいます。MyString
は Clone
トレイトを実装していないため、暗黙的に String::clone
を呼び出します。そのため、s
は MyString
ではなく String
になります。最低ですね 🤯
上述したように、もともと Deref
と DerefMut
はスマートポインタのために設計されたトレイトです。具体的にはポインタ型 P<T>
を T
に変換するためのものです。これらのトレイトがないと、例えば &mut T
の型を持つ値を &T
を引数にとる関数に渡すたびに、都度明示的な変換が必要になります。それはさすがにやっていられないので、 Deref
には通常の Rust の文脈では見られない強力な柔軟性が許されています。任意の型から任意の型への変換を自由に許すためのものではありません。利用者の良心が試されます。
非公式の Rust アンチパタン集にも Deref
を使った疑似的な継承がアンチパタンとして掲載されています。止めましょう。頑張って boilerplate 書きましょう。
とはいえ 大量の boilerplate を書き終えてから cargo test を実行 -> 実は自分のアプローチではうまくいかないことが判明 など発生すると大変つらいため、一時的な動作確認として Deref
を使ってコンパイラを騙すのはありかもしれません。
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 調べ)。
Deref
や DerefMut
を使って継承を実装しようとしない。