Rustでの抽象化プログラミングについてのメモ
ついでに静的ディスパッチと動的ディスパッチについても軽くまとめてみる...
自己満(腑に落ちる) > 正しい の記事なので, 言葉の使い方が間違っているところも大目に見てください. + 後半になるにつれて雑になります..
目次
- 抽象化プログラミングについて
- 静的ディスパッチと動的ディスパッチについて
抽象化プログラミング
なぜ必要なのか, 抽象化を行わない具体例 と 抽象化を行う具体例 を使って説明。
ユースケース
何らかのI/O処理が可能なデータから, データを読み込んで, 指定した文字列が含まれているかを判定する関数を作成する.
以下の3つのI/O処理が可能なデータ ※ 型名は擬似名称なもの を対象
- ファイルから読み取ったデータ: File型
- ネットワークから読み取ったデータ: NetWork型
- キーボードから読み取ったデータ: Keyboard型
いずれもI/O処理が可能なトレイト I/O を実装しているとする.
※ I/O トレイトは write()
や read()
を定義
抽象化を行わない場合
それぞれの型ごとに, 関数を作成する必要がある. ※ 以下は全て擬似的なコードです.
1. ファイルからデータを読み込んで, 指定した文字列が含まれているか判定する関数
fn grep_file(file: File, string: String) -> bool {
let buffer = file.read();
is_exit(buffer, String) //読み込んだデータに指定した文字列が含まれているか判定 真偽値を返す
}
2. ネットワークからデータを読み込んで(受け取って), 指定した文字列が含まれているか判定する関数
fn grep_network(nw: NetWork, string: String) -> bool {
let buffer = nw.read();
is_exit(buffer, String)
}
3. キーボードからデータを読み込んで, 指定した文字列が含まれているか判定する関数
fn grep_keyboard(keyboard: KeyBoard, string: String) -> bool {
let buffer = keyboard.read();
is_exit(buffer, String)
}
抽象化を行う場合
3つの型は, 全て I/O トレイトを満たしているので, トレイト境界 と ジェネリクス
を使って, 以下のように記述することができる.
fn grep<T: IO>(io: T) -> bool {
let buffer = io.read();
is_exit(buffer, String)
}
それぞれの型についての関数を書かずに済む ので 楽だし見やすいよね~_
... 以上!!
と終わらせたいところでけど, あとひと話題 ↓
静的ディスパッチと動的ディスパッチについて
抽象化を行った場合で, トレイト境界 と ジェネリクス を使ったけど, 他にも抽象化を行う方法がある. そのひとつが トレイトオブジェクト を利用した方法
さっきのコードをトレイトオブジェクトを利用して記述すると以下のようになる.
※Box
の説明は略
fn grep(io: Box<dyn I/O>) -> bool {
let buffer = io.read();
is_exit(buffer, String)
}
こんな感じに書くこともできる. ここで疑問 => 何が違うん? と
ここで 静的ディスパッチ と 動的ディスパッチ が出てくる.
結論
結論でもないけど.. うまくまとめられない
静的ディスパッチ
内部的 にそれぞれの型ごとの関数を作成している.
fn grep_file(file: File, string: String) -> bool {
let buffer = file.read();
is_exit(buffer, String) //読み込んだデータに指定した文字列が含まれているか判定 真偽値を返す
}
fn grep_network(nw: NetWork, string: String) -> bool {
let buffer = nw.read();
is_exit(buffer, String)
}
fn grep_keyboard(keyboard: KeyBoard, string: String) -> bool {
let buffer = keyboard.read();
is_exit(buffer, String)
}
さっきの抽象化しない時と変わんないじゃん... と思いきや内部的なので.. 書く分には楽だよね
メリット
中のメソッド2行目: .read()
の実行が速い
デメリット
内部的には型ごとに関数を作成しているので, 成果物のサイズは大きくなる
動的ディスパッチ
トレイトオブジェクトと仮想関数テーブルを介してメソッドの実行 2行目: io.read()
を実行する.... 何言うてんの?.. ということでまずは用語の説明
トレイトオブジェクト
簡単に説明.. 正しいかは置いておいて(冒頭見てください)
イメージとしてはこんな感じ
struct TraitObject {
data: *mut (), //具象型へのポインタ
vtable: *mut (), // 仮想関数テーブル
}
仮想関数テーブル (vtable)
簡単いうと, 関数ポインタのテーブル
この記事でのユースケースだと, こんなイメージ?
File型のwrite() : 0x7ff29bc02ce0 //実際のバイナリへのアドレス
File型のread() : 0x7ff29bc03ce0
NwtWork型のwrite() : 0x7ff29bc04ce0
NwtWork型のread() : 0x7ff29bc05ce0
KeyBoard型のwrite(): 0x7ff29bc06ce0
KeyBoard型のread() : 0x7ff29bc07ce0
ユースケース
例えば, Network型のオブジェクトがgrep関数に渡されて実行されると
fn grep(io: Box<dyn I/O>) -> bool {
let buffer = io.read();
is_exit(buffer, String)
}
fn main() {
let nw = NetWork::new();
let result = grep(Box::new(nw));
}
- grep()にNetWork型のオブジェクトが, トレイトオブジェクトとして渡される
- 2行目のread()は, 内部的には仮想関数テーブルを参照した後に実行
みたいなイメージ
なので.
メリット
成果物のサイズは小さい
デメリット
関数内のメソッドの実行が多少遅くなる(仮想関数テーブルを参照しているため)
まとめ
抽象化プログラミングをすることで, 楽だし見やすくなる
抽象化の方法には2つの方法(実際にはまだある. ex. Enumなど)があって,
- 静的ディスパッチは成果物のサイズは大きくなっちゃうけど, 速度は速い.
- 動的ディスパッチは成果物のサイズは小さいけど, 速度が落ちてしまう.
こんな感じでどうでしょう?