この記事について
Rust初心者である著者がアプリ作成を通じてRustを勉強した事を共有していく記事です。最終的にはルービックキューブのアプリ作成を目指しています。
前回の投稿「https://qiita.com/etnk/items/795f52ffcc6001cb0723」の続きです。
状況
キューブオブジェクトの集合体を定義して、それの描画が出来るようになりました。次は回転です。
回転する対象のキューブ要素のリストを抽出して処理したいと思い、その関数の作成をしましたが、所有権に関係するエラーが多発。どうも配列を作って使用する時の参照権がどうも想定と違うっぽい感じです。闇雲にmutや&を付けても無駄な時間が過ぎていきそうです。
当たり前ですが、配列はプログラムを作成する際に重要な要素です。この振る舞いもちゃんと把握しておかなければいけないです。
配列にはVec型と配列型がある様ですが、今後多用するであろうVecに集中します。
公式情報チェック
まず以下の情報を確認します。この中の気になる部分をピックアップします。
ピックアップ
ベクタがドロップされると、その中身もドロップされます。
この部分ではプリミティブ型での例ですが、ちゃんと認識していないと後々混乱しそうです。
この部分の終わりの方ですが、ベクタ要素への参照をある変数に代入していると、ベクタ自体への変更(追加や削除)が出来なくなるという事が書かれています。
ここが重要そうです。Rust Playgroundでサンプルコードを変えて実行しながら確認してみます。
&mut v
はv.iter_mut()
と同義、&v
はv.iter()
と同義の様なので、明示的に変更しました。
データ型の表示はRustで型の名前を取得する方法を参考にさせて頂きました。
fn print_typename<T>(_: T) {
println!("{}", std::any::type_name::<T>());
}
fn main() {
let mut v = vec![100, 32, 57];
for i in v.iter_mut() {
println!("{}", i); // 加算前の値が出力されます。
print_typename(&i); // &&mut i32 が出力されます。借用が起こる為&をつけてるので、iそのものは&mut 32と思います。
*i += 50;
print_typename(*i); // i32 が出力されます。
}
for i in v.iter() {
println!("{}", i); // 加算された値が出力されます。
print_typename(i); // &i32 が出力されます。
}
for i in v.into_iter() {
print_typename(i); // i32 が出力されます。
}
}
可変参照が参照している値を変更するには、+=演算子を使用する前に、 参照外し演算子(*)を使用してiの値に辿り着かないといけません。
という文章も公式ページに書いてありました。また、3種のiterによって、ループ変数の型がどうなるかに関して、以下の公式ページに詳細が書いてありました。書かれている事の通りの結果になっているようです。
iter_mut - この関数はコレクションの各要素をミュータブル(変更可能)で借用するので、コレクションの要素をその場で変更できます。
iter - この関数は、各周回においてコレクションの要素を借用します。よってコレクションには手を加えないので、ループの実行後もコレクションを再利用できます。
into_iter - この関数はコレクションからデータを取り出すので、各周回において要素のデータそのものが提供されます。データを取り出してしまうと、データはループ内に「移動」してしまうので、ループ実行後にコレクションを再利用することはできません。
チャレンジ
前述例ではプリミティブ型でしたが、structを使って同じ事をやってみます。
基本宣言
以下の様に宣言し、for文を使ってtestvalの値を変更する事にチャレンジします。
struct MyTestStruct {
testval: i32
}
struct MyTestStructSet {
test_01: MyTestStruct,
test_02: MyTestStruct,
test_03: MyTestStruct
}
impl Default for MyTestStructSet {
fn default() -> Self {
Self {
test_01: MyTestStruct { testval: 1},
test_02: MyTestStruct { testval: -1},
test_03: MyTestStruct { testval: 0}
}
}
}
集合体を取得する関数を作成
impl MyTestStructSet {
fn all_val(&self) -> Vec<&MyTestStruct> {
vec![
&self.test_01,
&self.test_02,
&self.test_03
]
}
}
参照型のVec<&MyTestStruct>
になっているのはVec<MyTestStruct>
と直接セットしようとすると以下エラーが出た為です。
error[E0507]: cannot move out of `self.test_01` which is behind a shared reference
--> src/main.rs:32:13
|
32 | self.test_01,
| ^^^^^^^^^^^^ move occurs because `self.test_01` has type `MyTestStruct`, which does not implement the `Copy` trait
所有権の移動が起こっちゃうので、Copyトレイトを持っていないと駄目な様でした。今回のケースでは実体はそのままにしておきたいので、参照として宣言しました。
まず中身表示
fn main() {
let mytest = MyTestStructSet::default();
for mystruct in mytest.all_val().iter() {
println!("{}", mystruct.testval); // 各testvalの値が出力されます。
print_typename(mystruct); // &&playground::MyTestStruct が出力されます。
}
}
for文で表示する事は出来ました。
中身変更してみる
fn main() {
let mut mytest = MyTestStructSet::default();
for mystruct in mytest.all_val_mut().iter_mut() {
mystruct.testval = 10;
println!("{}", mystruct.testval);
print_typename(mystruct); // &mut &playground::MyTestStruct が出力される(エラー部分コメントアウト時)
}
}
以下エラーが出ました。
error[E0594]: cannot assign to `mystruct.testval`, which is behind a `&` reference
--> src/main.rs:39:9
|
39 | mystruct.testval = 10;
| ^^^^^^^^^^^^^^^^^^^^^ cannot assign
エラー部分をコメントアウトして確認したループ中データ型は&mut &playground::MyTestStruct
になっていて、MyTestStructの参照を可変参照しているという、ある意味の二重参照となっているようです。
中身変更チャレンジ
ベクタを返す関数の中身を、可変参照のリストとして定義しました。
fn all_val_mut(&mut self) -> Vec<&mut MyTestStruct> {
vec![
&mut self.test_01,
&mut self.test_02,
&mut self.test_03
]
}
そして、使う部分では、into_iter()
を使い、その参照を直接見る様にしました。
for mystruct in mytest.all_val_mut().into_iter() {
mystruct.testval = 2;
println!("{}", mystruct.testval); // 2 が出力されます
print_typename(mystruct); // &mut playground::MyTestStruct が出力されます
}
中身変更できました。
条件によって新しいVecを返す関数を作成
正の値を持つMyTestStructのリストを返却する関数です。
fn positive_values(&mut self) -> Vec<&mut MyTestStruct> {
let mut retvals = vec![];
for mystruct in self.all_val_mut().into_iter() {
if mystruct.testval > 0 {
retvals.push(mystruct);
};
};
retvals
}
for mystruct in mytest.positive_values().into_iter() {
println!("{}", mystruct.testval);
print_typename(mystruct.testval);
}
ちゃんと1
のみ出力されました。Vecの中身がプリミティブ型でない場合は、参照型を保持するVecとしておいて、Vecをfor文で使用する時にはinto_iter
を使って処理すると良さそうです。into_iter
を使うと消費されてしまいそうですが、参照型を保持している参照が消費される形になって、本体は安全という結果になっていると思います。
※上記positive_values
関数はワンライナー的な書き方ありそうですが、とりあえず初心者の私は無理せずに書きました。
終わりに
他の言語ではここまで参照の事を気にする必要はありませんでした。しかし、オブジェクトの中身などが知らない所で変更される可能性を追うのは大変です(中身の変更はsetterのみにするなど、その危険性のある変数は見えなくするなどの対応が必要かと)。Rustではその変数がどの様な使われ方をするのかが明示化され、コンパイラがチェックしてくれるので、コーディングしている時は少々大変かもしれませんが、後々のデバグなどの大変さは大分軽減されるのではないでしょうか。
C言語だと、メモリ管理や参照管理をするのは相当大変なはずで、セキュリティホールなどの対応の為C言語からRustに置き換えするケースが増えているというのも理解できます。
業務でRustを使っていなくても、コンピューターがメモリをどう使うか肌で感じる為に、Rustを触ってみるのも良いかもしれません。
進捗(おまけ)
今回調べた事を作成中アプリに適用して、以下の描画が出来ました。回転する対称面を引数にとって、27個あるキューブ要素のうちどの9つが回転対象か抽出する関数を作成しました。その関数の結果に対して回転処理を施しました。
画像としては、陰線処理(と言うらしいです)が中途半端で重なりが上手く処理出来てなかったり、内部部分の描画がされてなかったりしますが、そこはまたおいおい対応予定です。