概要
Rustでなにか書いてRustに慣れたい。
いくつか参考書を片手に写経や追加機能を実装したりするのだが、やはり1から自分で考えてかけたほうが習熟度が違うのでは?特にRustの参考書は面白い題材が他の参考書よりおおいのだが、(なんでRustの言語参考書にOSやCPUシュミレータ、線形システムが題材に...?)重い題材が多くなかなか1から作って見ようというモチベーションまで上がらなかった。(そもそも私はそこまで低レベルをいじることが本職のエンジニアではないため)
そこで前々からちゃんとクラス図とか理解しながらコード書きたいな。とかオブジェクト指向的な事前設計をしたうえで抽象化するコードをかけるようになりたいなと考えていたためJava言語で学ぶデザインパターン入門で記載されているクラス図/デザインパターンをベースに各デザインパターンをRustで再実装してみようと考えた。
ちなみに10年くらい前(もっと前かも)に本書籍は購入したため3版ではなく、増補改訂版の方である。
参考書を丸コピーなどはしないが、クラス図は引用させていただくもしも問題だと感じる方がいらっしゃるならコメントなどで指摘いただけると助かります。
一応後から見たらRustってこうしたい場合こういうふうかけるんだなぁと言うのが理解できるように書くつもりだが個人ベースの備忘録なので、あまり多くを期待はしないでほしい。
Iterratorパターン概要
コンテナオブジェクトの要素を列挙する手段を独立させることによって、コンテナの内部仕様に依存しない反復子を提供することを目的とする。
クラス図
クラスに紐づく関数とかかけてないな...
コード
なんかよくわからなかった部分
dyn trait
利用しているコード
trait CustomAggregate {
fn iterator(&mut self) -> Box<dyn CustomIterator<'_> + '_>;
}
CustomAggregateはiterator関数はiteratorを利用したいオブジェクトに実装してそのオブジェクトを返すように強制するtraitとしたい
そのため戻り値の方はCustomIteratorを実装しているオブジェクトに限定したい。
その際に記載する戻り値をtraitにしたいのであれば頭にdynをつけるのがお作法っぽい。
これはBox
でラップして上げる必要があるなぜなら、Rustはコンパイル時にコンパイラがメモリ使用量を把握している必要があるが、traitはどのstructに実装されるかでメモリ使用量がかわるためBoxでヒープのメモリの参照を返すことで誤魔化して上げる必要がある。参考
なのでざっくり特定のtraitを実装したオブジェクトを戻り値に指定したい場合Boxって書いてあげる必要がありそう
構造体が可変参照を(&mut)保持する場合ライフタイム注釈をつけなければならない理由
今回ライフタイム注釈である<'a>はIterator traitを実装するBookShelfIteratorで初めて出てくる
struct BookShelfIterator<'a> {
bookShelf: &'a mut BookShelf, // ここは借用されたデータの可変参照となる。bookshelfitertorは値を触れるがそれ以外はさわれない。
index: usize
}
BookShelfIteratorが持つbookShelfはなぜbookShelf: BookShelf
じゃだめなのか
bookShelf: BookShelf
は所有権を持つということである。借用した場合元データは利用できなくなる。
元データを利用しない場合は所有権として実装可能だが、一般的にiteratorを利用した後も元データは利用したいことが一般的である。そのためここは可変参照である必要がある。
書いてて思ったけど可変である必要がはないかもしれない。
+ '_
impl CustomAggregate for BookShelf {
// &mut selfは可変参照のため所有権は本のデータに残る
// 'staticってなーに? -> プログラム全期間
fn iterator(&mut self) -> Box<dyn CustomIterator + '_> {
Box::new(BookShelfIterator::new(self))
}
}
これつけないと返されるiteratorの生存期間がstaticとなり非常に長くなる、そうなると例えば利用されるCustomAggregateを実装したBookShelf(self)のライフタイムよりも長くiteratorが生存しなくてはならなくてだめコンパイラに怒られる。
'_
を指定しない場合問題になるのは、selfが戻り値で指定した生存期間より短いからだめだよって言ってるように見える。
'_を指定するとselfにライフタイムがおなじになる
ライフタイム注釈とかmutとか&とかどういうタイミングで必要になるの?
他言語から北人がRustを書いたときに、すごく苦労するのは「なんか前の言語だと問題なく動くように書いたつもりなんだけど、Rustで書くとコンパイラに怒られるんだが」というところかなと感じている。
ここで今回書いたコードについてどういう理由でmutや&、ライフタイム注釈が必要なのかを細かく見ていく
BookShelf構造体
struct BookShelf {
books: Vec<Book>
}
こいつは本棚を表現する構造体で本の配列は所有権で表現される。
BookShelfオブジェクトにbookを追加するコードは以下のようになっている
fn append_book(&mut self, book: Book) {
self.books.push(book);
}
...
let mut book1 = Book::new("Bible".to_string());
bookshelf.append_book(book1);
次にこのbookshelfのiteratorを返すようにするためのAggregateの定義は以下
trait CustomAggregate {
fn iterator(&mut self) -> Box<dyn CustomIterator<'_> + '_>;
}
ここからわけわからんくなる。この&mut self
はbookshelfであり,iterator関数内だと可変参照として扱える。
その実装は以下である
fn iterator(&mut self) -> Box<dyn CustomIterator + '_> {
Box::new(BookShelfIterator::new(self))
}
戻り値として返す値はBookShelf自体を保持するBookShelfIterator
を返すつまりここでBookShelfオブジェクトはこいつが返すBookShelfIteratorオブジェクトの生存期間まで生きていないと値にアクセスできなくなるかもしれない。そこで'_
を指定して返す値はself(BookShelfオブジェクト)が生きている期間と一緒のライフサイクルやぞとコンパイラに伝えている
let mut bookshelf = BookShelf::new(4);
let mut iter = bookshelf.iterator(); //このiterの生存期間とbookshelfの生存期間は一緒やで!なぜならiterは内部的にbookshelfの可変参照を持っているからな!
while iter.has_next() {
let book = iter.next().unwrap();
println!("{}", book.get_name());
}
ここで出てきたBookShelf iteratorの実装は以下である
struct BookShelfIterator<'a> {
bookShelf: &'a mut BookShelf, // ここは借用されたデーのの可変参照となる。bookshelfitertorは値を触れるがそれ以外はさわれない。
index: usize
}
上記を踏まえるとなぜBookShelfIteratorに<'a>
が付くのかがなんとなく説明できる...きがする。
BookShelfIteratorで扱われるBookShelfはAggregateを実装したBookShelfのiteratorから渡され、BookShelfの可変参照を受け取る。つまりBookShelfIteratorが保持するBookShelfの可変参照の実体はAggregateを実装したBookShelfである。渡されたその際に元オブジェクト(BookShelf)の生存期間がわからないと、Iterator側からすると、いつ生存期間が切れるかわからないオブジェクトになるため、それを明示的に指定するためライフタイムを知らせてあげる仕組みが必要になる。それがライフタイム注釈'a
というわけだ。
つまりこれBookShelfが参照を持っている場合ライフタイム注釈が必須となる。
こういうめんどくさいことを考えたくなければ
struct BookShelfIterator {
bookShelf: BookShelf,
index: usize
}
って定義してBookShelfにClone実装しちゃえばいいんじゃね?と思ったがパフォーマンスなどを考慮するとそういうわけにはいかんのだろう。
一度Cloneを実装してどういう実装になるか確認してもいいかも。
が構造体がなんらか参照のデータを保持する場合ライフタイム注釈は必須と判断したほうがいいかも
頭の中と出力が追いついていない感が強いので後で書き直すかも