はじめに
オブジェクト指向言語の特徴へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習することができます。
この実験では、オブジェクト、カプセル化、継承などのオブジェクト指向言語の特徴を探り、Rust がこれらの機能をサポートしているかどうかを調べます。
オブジェクト指向言語の特徴
言語がオブジェクト指向と見なされるために必要な機能について、プログラミングコミュニティでは合意が得られていません。Rust は、OOP を含む多くのプログラミングパラダイムに影響を受けています。たとえば、第 13 章では関数型プログラミングに由来する機能を探りました。おそらく、OOP 言語は、オブジェクト、カプセル化、継承という特定の共通の特徴を共有しています。それぞれの特徴が何を意味するか、および Rust がそれをサポートしているかどうかを見てみましょう。
オブジェクトはデータと振る舞いを含む
エリッヒ・ガンマ、リチャード・ヘルム、ラルフ・ジョンソン、ジョン・ヴリッシデスによる著書『Design Patterns: Elements of Reusable Object-Oriented Software』(アディソン・ウェズリー、1994 年)は、俗に「四人組の本」と呼ばれるオブジェクト指向のデザインパターンの目録です。この本では、OOP を次のように定義しています。
オブジェクト指向プログラムはオブジェクトで構成されています。「オブジェクト」は、データとそのデータを操作する手続きの両方をパッケージ化します。手続きは通常、「メソッド」または「演算」と呼ばれます。
この定義を使えば、Rust はオブジェクト指向です。構造体と列挙体はデータを持ち、implブロックは構造体と列挙体に対するメソッドを提供します。メソッドを持つ構造体と列挙体は「オブジェクト」とは呼ばれませんが、四人組によるオブジェクトの定義によれば、同じ機能を提供します。
実装の詳細を隠すカプセル化
OOP と一般的に関連付けられるもう一つの側面は、「カプセル化」の概念です。これは、オブジェクトの実装の詳細が、そのオブジェクトを使用するコードにはアクセスできないことを意味します。したがって、オブジェクトとやり取りする唯一の方法は、その公開 API を通じることです。オブジェクトを使用するコードは、オブジェクトの内部にアクセスして、直接データや振る舞いを変更することはできません。これにより、プログラマはオブジェクトの内部を変更してリファクタリングする際に、オブジェクトを使用するコードを変更する必要がなくなります。
第 7 章では、カプセル化を制御する方法について説明しました。コード内のどのモジュール、型、関数、メソッドが公開されるべきかを決定するために、pubキーワードを使用できます。デフォルトでは、それ以外のすべては非公開になります。たとえば、i32値のベクトルを含むフィールドを持つAveragedCollection構造体を定義できます。この構造体には、ベクトル内の値の平均を含むフィールドもあります。つまり、平均値は誰かが必要になるたびに必要に応じて計算する必要はありません。言い換えれば、AveragedCollectionは計算済みの平均値をキャッシュしてくれます。リスト 17-1 にAveragedCollection構造体の定義を示します。
ファイル名:src/lib.rs
pub struct AveragedCollection {
list: Vec<i32>,
average: f64,
}
リスト 17-1: 整数のリストとコレクション内の要素の平均を保持するAveragedCollection構造体
この構造体はpubとマークされており、他のコードが使用できるようになっていますが、構造体内のフィールドは非公開のままです。この場合、これが重要なのは、リストに値を追加または削除するたびに、平均値も更新されることを確認するためです。これは、構造体にadd、remove、averageメソッドを実装することで行います。リスト 17-2 を参照してください。
ファイル名:src/lib.rs
impl AveragedCollection {
pub fn add(&mut self, value: i32) {
self.list.push(value);
self.update_average();
}
pub fn remove(&mut self) -> Option<i32> {
let result = self.list.pop();
match result {
Some(value) => {
self.update_average();
Some(value)
}
None => None,
}
}
pub fn average(&self) -> f64 {
self.average
}
fn update_average(&mut self) {
let total: i32 = self.list.iter().sum();
self.average = total as f64 / self.list.len() as f64;
}
}
リスト 17-2: AveragedCollectionにおける公開メソッドadd、remove、averageの実装
公開メソッドadd、remove、averageは、AveragedCollectionのインスタンス内のデータにアクセスまたは変更する唯一の方法です。addメソッドを使用してlistに項目を追加するか、removeメソッドを使用して削除すると、各呼び出しの実装は、averageフィールドの更新を処理する非公開のupdate_averageメソッドを呼び出します。
listとaverageフィールドを非公開にしておくことで、外部コードが直接listフィールドに項目を追加または削除する方法はありません。そうでなければ、listが変更されたときにaverageフィールドが同期しなくなる可能性があります。averageメソッドはaverageフィールドの値を返し、外部コードがaverageを読み取ることはできますが、変更することはできません。
AveragedCollection構造体の実装の詳細をカプセル化したため、将来的にデータ構造などの側面を簡単に変更できます。たとえば、listフィールドにVec<i32>の代わりにHashSet<i32>を使用することができます。add、remove、averageの公開メソッドのシグネチャが同じままであれば、AveragedCollectionを使用するコードは変更する必要がありません。もしlistを代わりに公開にすると、必ずしもそうではなくなります。HashSet<i32>とVec<i32>は、項目の追加と削除に異なるメソッドを持っているため、外部コードが直接listを変更している場合、おそらく変更する必要があります。
もしカプセル化がオブジェクト指向と見なされるための必須の側面であるならば、Rust はその要件を満たしています。コードの異なる部分にpubを使用するかどうかのオプションにより、実装の詳細のカプセル化が可能になります。
型システムとコード共有としての継承
「継承」は、オブジェクトが別のオブジェクトの定義から要素を継承し、それにより親オブジェクトのデータと振る舞いを再度定義することなく獲得できる仕組みです。
言語がオブジェクト指向であるためには継承が必要である場合、Rust はそのような言語ではありません。マクロを使用せずに、親構造体のフィールドとメソッドの実装を継承する構造体を定義する方法はありません。
ただし、プログラミングツールボックスに継承があることに慣れている場合、まず最初に継承を求める理由に応じて、Rust で他の解決策を使用できます。
継承を選ぶ主な理由は 2 つあります。1 つはコードの再利用です。特定の振る舞いを 1 つの型に対して実装し、継承によりその実装を別の型で再利用できます。これは、Rust コードでは、デフォルトのトレイトメソッド実装を使用して制限された方法で行うことができます。これは、第 10 章のリスト 10-14 で、Summaryトレイトにsummarizeメソッドのデフォルト実装を追加したときに見たことがあります。Summaryトレイトを実装するすべての型は、さらにコードを記述することなく、その型にsummarizeメソッドが利用可能になります。これは、親クラスがメソッドの実装を持ち、継承する子クラスも同じメソッドの実装を持つことに似ています。また、Summaryトレイトを実装する際に、summarizeメソッドのデフォルト実装をオーバーライドすることもできます。これは、子クラスが親クラスから継承したメソッドの実装をオーバーライドすることに似ています。
継承を使用するもう 1 つの理由は、型システムに関係しています。子型を親型と同じ場所で使用できるようにするためです。これは「ポリモーフィズム」とも呼ばれ、実行時に特定の特性を共有する場合、複数のオブジェクトを相互に置き換えることができることを意味します。
ポリモーフィズム
多くの人にとって、ポリモーフィズムは継承と同義語です。しかし、実際には、複数の型のデータと動作することができるコードを指す、より一般的な概念です。継承の場合、それらの型は一般的にサブクラスです。
Rust では代わりに、ジェネリクスを使用してさまざまな可能な型を抽象化し、それらの型が提供しなければならないものに制約を課すためにトレイト境界を使用します。これは、時々「境界付きパラメトリックポリモーフィズム」と呼ばれます。
最近、多くのプログラミング言語において、継承はプログラミングデザインの解決策として好まれなくなってきました。なぜなら、必要以上に多くのコードを共有するリスクがあるからです。サブクラスは必ずしも親クラスのすべての特性を共有する必要はありませんが、継承によりそうなってしまいます。これにより、プログラムのデザインが柔軟性に欠ける場合があります。また、サブクラスで意味がないメソッドを呼び出したり、メソッドがサブクラスに適用されないためにエラーが発生する可能性もあります。さらに、一部の言語では単一継承のみが許される場合があり(つまり、サブクラスは 1 つのクラスからのみ継承できる)、プログラムのデザインの柔軟性がさらに制限されます。
これらの理由から、Rust は継承の代わりにトレイトオブジェクトを使用する異なるアプローチを採用しています。Rust におけるトレイトオブジェクトがどのようにポリモーフィズムを可能にするか見てみましょう。
まとめ
おめでとうございます!オブジェクト指向言語の特徴の実験を完了しました。スキルを向上させるために、LabEx でさらに実験を行って練習してください。