やっつけな日常(27) Rustに入ればRustに従え、それにしても#deriveて何よ

Joseph Halfmoon

Rust言語の勉強の前回は「所有権」というRust独特の仕組みに触ってみました。今回は「所有権」に再び足をすくわれ(復習)つつ、構造体に触手?を伸ばしてみたいと思います。やっぱりというか、当然というかRustの構造体もクセが強いです。だいたいね、トレイトとか他の言語ではない用語/概念多すぎ。カッコイイんだけれども。

まず度々登場する トレイト についていつもお世話になっております 「Rust By Example 日本語版」の以下のページから1か所引用させていただきます。

トレイト

トレイト(trait)とは任意の型となりうるSelfに対して定義されたメソッドの集合のことです。同じトレイト内で宣言されたメソッド同士はお互いにアクセスすることができます。

ほえほえ~、Selfって何よ、とか更なるツッコミを入れたくなりますがやめときます。私のアサハカな理解では、よくある言語のクラスにおける、インスタンス・メソッドみたいなもの? いいんだろうかな。

さて今回はRust言語の構造体を学んでいきたいと。当方、Cの構造体が頭にあるのですが、どうもそれとはいろいろ違う気がします。とりあえず一番「Cに似た」スタイルの構造体を1個定義して、使ってみることにいたしました。

Rustのエラーメッセージは分かり易い?

以下は最初に書いた「所有権の復習」用のコードでございます。エラーの箇所は一目瞭然で分かるかと。18行目です。ErrorSource

既に生成した構造体の「所有権」をMOVEしてしまっているのに、test.a、test.b参照しようとするんじゃないよ、と。

ちょっと文字が小さくて見ずらいですが、そのエラー表示の様子が以下に。適切な場所に適切なメッセージがでていて分かり易いです(個人の感想です。)

ErrorMessage

「所有権」の復習とともに、大事なことも分かりました。C言語と比較するならばこんな感じ。

    • Cで構造体を関数の引数にとると、スタック上に実体データも含めてコピーされて呼び出し先の関数に渡る。
    • Rustで構造体を関数の引数にすると、所有権が呼び出し先の引数に移転(MOVE)してしまう。つまり構造体はヒープ上にとられている。

また、ミュータブル、イミュータブルの識別についても1個分かりました。

    • test1という変数はイミュータブルとして宣言。構造体で初期化。当然test1のメンバへの代入はエラーとなる。
    • 関数 add11の仮引数 arg の実引数としてtest1 を与えた場合、arg をミュータブルとして宣言してあれば、構造体に代入できる。

どうも変数に対してミュータブル、イミュータブルの制御がなされるのであって、ヒープ上の構造体そのものに対して書き込みを制御しているわけではないのですかね。ホントか。

構造体をcloneしてみる

さて、構造体を関数引数に与えると、所有権が移転することが分かったのでそのつもりでコードを書きました。でも、ついでに、所有権を移転させず、構造体のクローンを作って関数に与えるということもやって見たかったです。しかし、構造体のクローン、どうしたら良いの? 再び「Rust By Example 日本語版」様の以下のページより1か所引用させていただきます。

継承(Derive)

コンパイラには、[#derive]アトリビュートを用いることで型に対して特定のトレイトの標準的な実装を提供する機能があります。

ザックリ言ってしまえば、構造体に付加した[#derive]アトリビュートで、Cloneを使うぜ、と宣言すれば良きに計らってくれるみたいです。Rustは他言語のようにアカラサマにクラスとか登場しないですが、オブジェクト・オリエンテッドな「考え方」はしっかりあるみたいです。

実験に使用したコード全文(実行可能)は以下に。

#[derive(Clone)]
struct Dual {
    a: i32,
    b: i32
}

fn add11(mut arg: Dual) -> Dual {
    println!("sub: a={:?} b={:?}", arg.a, arg.b);
    arg.a += 11;
    arg.b += 11;
    return arg;
}

fn main() {
    let test = Dual { a:33, b:44 };
    println!("  1: a={:?} b={:?}", test.a, test.b);
    let test2 = add11(test);
    println!("  2: a={:?} b={:?}", test2.a, test2.b);
//  println!("a={:?} b={:?}", test.a, test.b);
    let test3 = add11(test2.clone());
    println!("  3: a={:?} b={:?}", test3.a, test3.b);
    println!("  4: a={:?} b={:?}", test2.a, test2.b);
}

上記コードを実行した結果が以下に(WSL1内のUbuntu20.04上で走っている cargo 1.62.0での実行結果です。)

result

関数にtest2をcloneした構造体を与えた場合、所有権は失われず後でtest2を参照しても大丈夫だと。当たり前か。

やっつけな日常(26) Jsに入ればJsに従え、JavaScriptの「巻き上げ」、御無体な へ戻る

やっつけな日常(28) Raspberry Pi OS、32bit版から64bit版へ移行 へ進む