概要
RustにはC++やJavaにあるクラスの継承機能がありません。
この記事ではC++やJavaで継承を使っていた人がRustで同様の実装をしたいときにどうすればよいのかを説明します。
前提として、Rustでは継承(inheritance)よりも合成(委譲、composition)が推奨されています。
したがって最初の選択肢は合成を使うことです。
ただ、それでも公開メソッドが非常に多くあり、合成を使うとboilerplate(コピペするだけのコード)が大量に発生してしまう場合は迂回策を模索したくなります。
この記事では、継承でやりたいことの具体例と、それを合成で実装した場合を説明したうえで、その迂回策としてトレイトのデフォルト実装とトレイト継承を組み合わせることで継承っぽい書き方を実現する方法についても紹介します。
継承でやりたいこと
まず、継承でやりたいことの具体例としてRustには存在しない架空の継承を使ったコードを示します。
struct TaggedVec {
vec: Vec<i32>,
tag: String,
}
// !Rustには存在しない継承の文法!
struct DescriptiveVec: TaggedVec {
description: String,
}
// !Rustには存在しない継承の文法!
struct CountingDescriptiveVec: DescriptiveVec {
push_count: usize,
pop_count: usize
}
impl TaggedVec {
pub fn new() -> Self {
Self { vec: Vec::<i32>::new(), tag: String::new() }
}
pub fn push(&mut self, value: i32) {
self.vec.push(value);
}
pub fn pop(&mut self) -> Option<i32> {
self.vec.pop()
}
pub fn len(&self) -> usize {
self.vec.len()
}
pub fn set_tag(&mut self, tag: String) {
self.tag = tag;
}
pub fn get_tag(&self) -> &str {
self.tag.as_str()
}
}
impl DescriptiveVec {
pub fn new() -> Self {
Self { vec: TaggedVec::new(), description: String::new() }
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
pub fn get_description(&self) -> &str {
self.description.as_str()
}
}
impl CountingDescriptiveVec {
pub fn new() -> Self {
Self { vec: DescriptiveVec::new(), push_count: 0, pop_count: 0 }
}
// Override
pub fn push(&mut self, value: i32) {
self.vec.push(value);
self.push_count += 1;
}
// Override
pub fn pop(&mut self) -> Option<i32> {
self.pop_count += 1;
self.vec.pop()
}
pub fn get_push_count(&self) -> usize {
self.push_count
}
pub fn get_pop_count(&self) -> usize {
self.pop_count
}
}
ここではVecに加えてtagという文字列を持つクラス、そしてさらにdescriptionという文字列を加えた子クラス、そしてpush/popの回数を記録する機能を追加した子クラスを作成しています。
C++やJavaで継承機能を使ったことのある人にとっては簡潔で理解しやすいコードだと思います。
structのフィールドとメソッドは子クラスで追加された分だけが記述されています。
しかしRustではstructの継承機能が無いためこのコードはコンパイルできません。
これはRustがComposition over inheritance(継承より合成)というデザインパターンを尊重し、継承を濫用した場合に生じる様々なデメリットを回避しようとしているためです。
では、合成を使用してRustでコンパイルできるコードがどのようになるのか?次節で見てみましょう。
合成を採用する場合
前述のコードを合成で書く場合、以下のようになります。
pub struct TaggedVec {
vec: Vec<i32>,
tag: String,
}
pub struct DescriptiveVec {
vec: TaggedVec,
description: String,
}
pub struct CountingDescriptiveVec {
vec: DescriptiveVec,
push_count: usize,
pop_count: usize
}
impl TaggedVec {
pub fn new() -> Self {
Self { vec: Vec::<i32>::new(), tag: String::new() }
}
pub fn push(&mut self, value: i32) {
self.vec.push(value);
}
pub fn pop(&mut self) -> Option<i32> {
self.vec.pop()
}
pub fn len(&self) -> usize {
self.vec.len()
}
pub fn set_tag(&mut self, tag: String) {
self.tag = tag;
}
pub fn get_tag(&self) -> &str {
self.tag.as_str()
}
}
impl DescriptiveVec {
pub fn new() -> Self {
Self { vec: TaggedVec::new(), description: String::new() }
}
pub fn push(&mut self, value: i32) {
self.vec.push(value);
}
pub fn pop(&mut self) -> Option<i32> {
self.vec.pop()
}
pub fn len(&self) -> usize {
self.vec.len()
}
pub fn set_tag(&mut self, tag: String) {
self.vec.set_tag(tag);
}
pub fn get_tag(&self) -> &str {
self.vec.get_tag()
}
pub fn set_description(&mut self, description: String) {
self.description = description;
}
pub fn get_description(&self) -> &str {
self.description.as_str()
}
}
impl CountingDescriptiveVec {
pub fn new() -> Self {
Self { vec: DescriptiveVec::new(), push_count: 0, pop_count: 0 }
}
pub fn push(&mut self, value: i32) {
self.vec.push(value);
self.push_count += 1;
}
pub fn pop(&mut self) -> Option<i32> {
self.pop_count += 1;
self.vec.pop()
}
pub fn len(&self) -> usize {
self.vec.len()
}
pub fn set_tag(&mut self, tag: String) {
self.vec.set_tag(tag);
}
pub fn get_tag(&self) -> &str {
self.vec.get_tag()
}
pub fn set_description(&mut self, description: String) {
self.vec.set_description(description);
}
pub fn get_description(&self) -> &str {
self.vec.get_description()
}
pub fn get_push_count(&self) -> usize {
self.push_count
}
pub fn get_pop_count(&self) -> usize {
self.pop_count
}
}
fn main() {
let mut v = composition::CountingDescriptiveVec::new();
v.set_tag(String::from("foo"));
v.set_description(String::from("It is foo"));
v.push(1);
v.push(2);
println!("tag: {}", v.get_tag());
println!("description: {}", v.get_description());
println!("len: {}", v.len());
println!("popped: {}", v.pop().unwrap());
println!("push_count: {}", v.get_push_count()); // 2
println!("pop_count: {}", v.get_pop_count()); // 1
}
子クラスに相当するstructがそれぞれの親クラスに相当するオブジェクトをフィールドとして持っています。
これが合成の書き方です。
そして合成を採用した場合、上のコードのようにそれぞれのstructで同名のメソッドを実装することになります。
そのため、たとえばlenという関数名をsizeに変えようとなった時、3箇所修正する必要が出てきます。
このようにインターフェースの変更に手間がかかるのとコード行数が増えてしまう点が合成のデメリットです。
しかし合成は全てのフィールドとメソッドが明記されているため、継承のコードに比べて子クラスの内容が分かりやすい(親クラスの定義を辿らずに済む)というメリットもあります。
※2023/01/02追記: コメントにて@shigunodoさんから筋の良い合成の書き方のコード例を頂きました。本記事の結論では合成を推奨することになるのですが、その際にコメントのコード例を是非ご覧になってください。
トレイトで継承っぽく書く場合
トレイトではデフォルト実装といってインターフェースとしての関数宣言だけでなく、実装も書くことができます。
さらに、トレイトの継承機能によって子クラスを作る感覚でメソッドの実装を増やしていくことができます。
ただ、トレイトのデフォルト実装は特定のstructに紐付いているわけではないのでstructのフィールドに直接アクセスできません。
そこで、トレイトのデフォルト実装でもstructのフィールドにアクセスする抜け道を用意するというのがここでの大まかな戦略となります。
まずは上述したトレイトの基本的な性質を簡単なコードで確認しておきます。
トレイトでは以下のようにデフォルト実装を定義し、structに付与する(implする)ことができます。
pub struct Foo { }
pub trait FooTrait {
fn foo(&self) {
println!("foo!"); // デフォルト実装の定義
}
}
impl FooTrait for Foo {} // デフォルト実装を付与(追加の実装は無いので{}内は空欄)
fn main() {
let f = Foo {};
f.foo(); // foo!
}
さらに、トレイトには継承機能もあります。
pub struct Foo { }
pub trait FooTrait {
fn foo(&self) {
println!("foo!"); // デフォルト実装の定義
}
}
pub trait FooBarTrait: FooTrait { // トレイトの継承
fn foo_bar(&self) { // 追加のデフォルト実装の定義
print!("bar! of ");
self.foo();
}
}
impl FooTrait for Foo {} // 継承元のトレイトもimplが必要
impl FooBarTrait for Foo {} // 追加のデフォルト実装も付与
fn main() {
let f = Foo {};
f.foo(); // foo!
f.foo_bar(); // bar! of foo!
}
したがって、トレイトのデフォルト実装とトレイトの継承機能を組み合わせたらメソッドについては継承が実現できるということになります。
しかしトレイトのデフォルト実装ではstructのフィールドに直接アクセスすることができません。
pub struct Foo { n: i32 }
// コンパイルエラー!
// where句でSelf: Fooと指定することを思いつく人もいるかもしれませんが
// where句ではトレイトを指定する必要があり、structであるFooは指定できません
pub trait FooTrait {
fn foo(&self) {
println!("{}", self.n); // error[E0609]: no field `n` on type `&Self`
}
}
impl FooTrait for Foo { }
fn main() {
let f = Foo {n: 1};
f.foo();
}
そこで今回の核心となる抜け道です。
以下のようにFooの参照を取得する関数を追加すればこの問題を回避できます。
pub struct Foo { n: i32 }
pub trait FooTrait {
fn as_foo(&self) -> &Foo; // Fooの参照を返す関数の宣言のみ書いておく
fn foo(&self) {
println!("{}", self.as_foo().n); // 今度はOK
}
}
impl FooTrait for Foo { // ここはFooに対する実装なので、Fooのフィールドにアクセスできる
fn as_foo(&self) -> &Foo { self } // Fooの参照を返す関数の実装
}
fn main() {
let f = Foo {n: 1};
f.foo(); // 1
}
さらに、可変参照を返す関数も追加することでstructのフィールドを変更できるようにもなります。
pub struct Foo { n: i32 }
pub trait FooTrait {
fn as_foo(&self) -> &Foo; // Fooの参照を返す関数の宣言のみ書いておく
fn as_mut_foo(&mut self) -> &mut Foo; // Fooの可変参照を返すものも用意
fn foo(&self) {
println!("{}", self.as_foo().n); // 今度はOK
}
fn increment(&mut self) {
self.as_mut_foo().n += 1;
}
}
impl FooTrait for Foo { // ここはFooに対する実装なので、Fooのフィールドにアクセスできる
fn as_foo(&self) -> &Foo { self } // Fooの参照を返す関数の実装
fn as_mut_foo(&mut self) -> &mut Foo { self } // Fooの可変参照を返す関数の実装
}
fn main() {
let mut f = Foo {n: 1};
f.increment();
f.foo(); // 2
}
ただ、トレイトはpubを指定すると全ての関数が公開されるため、このままだと外部からas_mut_fooを自由に呼び出されてしまいます。
これは特にトレイトの継承をした場合に親クラスに相当するstructの可変参照を取得できてしまう場合に問題となる(具体的には最後に示す全体のコード例で分かると思います)ため、以下のように参照・可変参照の取得関数は別のトレイトに隔離しておくと良いでしょう。
pub struct Foo { n: i32 }
// モジュールを非公開にすることで外部から見られないようにしておきます
mod private {
// privateモジュールを見られる人からはトレイトを見られるようにpubを指定
pub trait AsFoo {
fn as_foo(&self) -> &super::Foo;
fn as_mut_foo(&mut self) -> &mut super::Foo;
}
}
// 単にpubを外したtraitだと、公開トレイトであるFooTraitから継承できません
/* error[E0445]: private trait `AsFoo` in public interface
trait AsFoo {
fn as_foo(&self) -> &Foo;
fn as_mut_foo(&mut self) -> &mut Foo;
}
*/
// privateモジュールの定義と同一ファイル内(同一モジュール内)ならprivate::でアクセス可能
pub trait FooTrait: private::AsFoo {
fn foo(&self) {
println!("{}", self.as_foo().n);
}
fn increment(&mut self) {
self.as_mut_foo().n += 1;
}
}
impl private::AsFoo for Foo {
fn as_foo(&self) -> &Foo { self } // Fooの参照を返す関数の実装
fn as_mut_foo(&mut self) -> &mut Foo { self } // Fooの可変参照を返す関数の実装
}
impl FooTrait for Foo { }
fn main() {
let mut f = Foo {n: 1};
f.increment();
f.foo(); // 2
}
これで準備が整いましたので、最後に全体のコード例を示します。
上述したトレイトのデフォルト実装、トレイトの継承、そして抜け道を利用することで、以下のようにTaggedVecと一連の子クラスを継承っぽく実装することができます。
pub struct TaggedVec {
vec: Vec<i32>,
tag: String,
}
pub struct DescriptiveVec {
vec: TaggedVec,
description: String,
}
pub struct CountingDescriptiveVec {
vec: DescriptiveVec,
push_count: usize,
pop_count: usize
}
mod private {
pub trait AsTaggedVec {
fn as_tagged_vec(&self) -> &super::TaggedVec;
fn as_mut_tagged_vec(&mut self) -> &mut super::TaggedVec;
}
pub trait AsDescriptiveVec {
fn as_descriptive_vec(&self) -> &super::DescriptiveVec;
fn as_mut_descriptive_vec(&mut self) -> &mut super::DescriptiveVec;
}
}
impl private::AsTaggedVec for TaggedVec {
fn as_tagged_vec(&self) -> &TaggedVec { self }
fn as_mut_tagged_vec(&mut self) -> &mut TaggedVec { self }
}
impl private::AsTaggedVec for DescriptiveVec {
fn as_tagged_vec(&self) -> &TaggedVec { &self.vec }
fn as_mut_tagged_vec(&mut self) -> &mut TaggedVec { &mut self.vec }
}
impl private::AsDescriptiveVec for DescriptiveVec {
fn as_descriptive_vec(&self) -> &DescriptiveVec { self }
fn as_mut_descriptive_vec(&mut self) -> &mut DescriptiveVec { self }
}
impl private::AsTaggedVec for CountingDescriptiveVec {
fn as_tagged_vec(&self) -> &TaggedVec { &self.vec.vec }
fn as_mut_tagged_vec(&mut self) -> &mut TaggedVec { &mut self.vec.vec }
}
impl private::AsDescriptiveVec for CountingDescriptiveVec {
fn as_descriptive_vec(&self) -> &DescriptiveVec { &self.vec }
fn as_mut_descriptive_vec(&mut self) -> &mut DescriptiveVec { &mut self.vec }
}
pub trait TaggedVecTrait: private::AsTaggedVec {
fn push(&mut self, value: i32) {
self.as_mut_tagged_vec().vec.push(value);
}
fn pop(&mut self) -> Option<i32> {
self.as_mut_tagged_vec().vec.pop()
}
fn len(&self) -> usize {
self.as_tagged_vec().vec.len()
}
fn set_tag(&mut self, tag: String) {
self.as_mut_tagged_vec().tag = tag;
}
fn get_tag(&self) -> &str {
self.as_tagged_vec().tag.as_str()
}
}
pub trait DescriptiveVecTrait: TaggedVecTrait + private::AsDescriptiveVec {
fn set_description(&mut self, description: String) {
self.as_mut_descriptive_vec().description = description;
}
fn get_description(&self) -> &str {
self.as_descriptive_vec().description.as_str()
}
}
impl TaggedVecTrait for TaggedVec { }
impl TaggedVecTrait for DescriptiveVec { }
impl TaggedVecTrait for CountingDescriptiveVec { }
impl DescriptiveVecTrait for DescriptiveVec { }
impl DescriptiveVecTrait for CountingDescriptiveVec { }
impl TaggedVec {
pub fn new() -> Self {
Self { vec: Vec::<i32>::new(), tag: String::new() }
}
}
impl DescriptiveVec {
pub fn new() -> Self {
Self { vec: TaggedVec::new(), description: String::new() }
}
}
impl CountingDescriptiveVec {
pub fn new() -> Self {
Self { vec: DescriptiveVec::new(), push_count: 0, pop_count: 0 }
}
// Override
pub fn push(&mut self, value: i32) {
self.vec.push(value);
self.push_count += 1;
}
// Override
pub fn pop(&mut self) -> Option<i32> {
self.pop_count += 1;
self.vec.pop()
}
pub fn get_push_count(&self) -> usize {
self.push_count
}
pub fn get_pop_count(&self) -> usize {
self.pop_count
}
}
fn main() {
let mut v = inheritance::CountingDescriptiveVec::new();
v.set_tag(String::from("foo"));
v.set_description(String::from("It is foo"));
v.push(1);
v.push(2);
println!("tag: {}", v.get_tag());
println!("description: {}", v.get_description());
println!("len: {}", v.len());
println!("popped: {}", v.pop().unwrap());
println!("push_count: {}", v.get_push_count()); // 2
println!("pop_count: {}", v.get_pop_count()); // 1
// let _vref = v.as_mut_descriptive_vec(); // エラー: privateなので可変参照は取得できない
}
トレイトと各structの参照及び可変参照を返すだけのメソッドを記述する分、冒頭で示した擬似的な継承コードと比べるとコード行数は増えてしまっていますが、合成の時のように同名のメソッドを実装する必要がなくなっています。
トレイトのデフォルト実装はオーバーライドできるため、CountingDescriptiveVecのpushとpopも問題なくオーバーライドできています。
なお、Derefを使用すると上記のコードをより短く書けるのですが、それはアンチパターンなので非推奨となります。(Derefはカスタムポインタのみに使用することが想定されているため。)
以上、ついにメソッドのコピペは回避できたものの、合成に比べるとメソッドの全貌が見づらくなってしまっており、やはりRustでは合成を採用するのが筋の良い実装なのだと思います。
まとめ
Rustにはstructの継承機能が無く、合成(委譲)を使用するかトレイトで継承っぽい書き方をする必要があります。
合成を使用するとメソッドのコピペが多く発生してしまう場合があるものの、Composition over inheritanceに従うことで継承の濫用による様々なデメリットを回避できます。
トレイトによる継承っぽい書き方は合成で生じるようなメソッドのコピペは防げるものの、structの参照と可変参照を取得するための余分なコードが必要になることで全体の見通しが悪化します。
したがって子クラス(に相当するもの)と共有するメソッドの数がよほど多くない限りはトレイトによる継承っぽい書き方は筋が良くない書き方だといえると思います。
結論としては、よほど困らない限りは継承を捨てて、たとえば各子クラスの公開メソッドの数を最小限にするなどして合成のデメリットを緩和し、合成とうまく付き合っていくのが良いのかなと思いました。