Closed3

Rustの基本事項まとめ

てりーてりー

特徴

  • memory safe(メモリ効率が良い)
  • no null(null安全性が保たれている)
  • no exceptions(エラーハンドリングで例外処理がない)
  • no data rules

メモリ管理

メモリの動作にはStackとHeapがある

参考:http://web.mit.edu/rust-lang_v1.25/arch/amd64_ubuntu1404/share/doc/rust/html/book/first-edition/the-stack-and-the-heap.html#:~:text=Memory management&text=The stack is very fast,explicitly allocated by your program.

分かりやすい解説記事
https://qiita.com/nirasan/items/35d13f6a65b41fce014f

Stack

メモリ領域。
関数実行時に変数をローカルのメモリに割り当てる。これを「スタックフレーム」という。
関数の実行は終わると自動で割り当てが解除される。

アクセスが高速。デフォルトはこっちに割り当てられる。

Heap

メモリ領域。
必要に応じてStackからHeapに割り当てる。アクセスは遅いが容量が多い。
容量の大きなデータはHeapに割り当てると良い。

スマートポインタ

Heapでメモリリークしないようにするラッパー。(だけではなく、以下の引用の様に様々あるらしい。)

スマートポインタの例は以下らしい。

ヒープ管理系: Box
リファレンスカウンタ系: Rc, Weak, Arc
ガード系; Ref, RefMut, MutexGuard, RwLockReadGuard, RwLockWriteGuard

基本的なデータ型

整数型

数学でいういわゆる整数を指す。
符号付きと符号なしで区別する。

具体的な値は以下の表を参照。

浮動小数型

少数まで含めた数字の型
f32f64がある。基準値はf64らしい。

論理値型

いわゆるboolean
Rustではboolと書く。もちろんtruefalseの2択。

文字型

数字メインのRustだが、文字型(char型)もある。
JavaScriptのstring型と一緒。

関数

変数

基本的にイミュータブルで、後からの変更不可。
変更する場合は

src/main.rs
let mut x = 5;

と書く必要がある。

{}

src/main.rs
fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    

上記のコードを実行すると、以下になります。

The value of x is: 5
The value of y is: 6

この様に{}には変数が代入される。

てりーてりー

software design 6月号:Rust入門 参考

1章:Rust「超」入門

Rustの用途

・コマンドラインツール
・WebAssembly
・ネットワーク
・組込みシステム

らしい。

WebAssembly でRustを使うメリット

・パフォーマンスが予測できる
・コードのサイズが小さいこと
・豊富なライブラリやゼロコスト抽象化などの現代的なエコシステム

ネットワークでRustを使うメリット

・メモリ管理をコンパイル時に行う部分が多い ため、実行時に余分なメモリや CPU 資源を 使わない
・パワフルな型チェッカーによって安全性と信 頼性が保たれる
・スケール時の並列化をサポートしやすい言語 設計になっている

ここからRustを利用している会社一覧が見れる。
https://github.com/fnwiya/japanese-rust-companies

実際にコードを書いてみる

cargo run で実行できる
cargo fmtでフォーマットしてくれる

for文

画像の様に1~9までを出力したい。

main.rs
fn main() {
    for i in 1..10 {
        println!("Hello,{} world!", i);
    }
}

{}は置換フィールド。

関数の基本形

fn 関数名(引数名: 型) -> 戻り値の型{関数の処理}

条件分岐

Hello,1st world!
Hello,2nd world!
Hello,3rd world!
Hello,4th world!
Hello,5th world!
Hello,6th world!
Hello,7th world!
Hello,8th world!
Hello,9th world!

の様に1~9の値に応じて、文字列の内容を少し変えたい。

fn to_original_string(n: usize) -> String {
    let s = match n % 10 {
        1 => "st",
        2 => "nd",
        3 => "rd",
        _ => "th",
    };

    format!("{}{}", n, s)
}

matchを使ってsの変数に条件ごとの文字を入れる。
最後にn、sを組み合わせて整形すれば完成。

てりーてりー

2章

変数

イミュータブル
letで定義。

シャドーイングする事で再定義は出来る。

関数型プログラミング

参照透過性
同じ引数を与えれば同じ値が返ってくる。
→副作用なし

所有権

Rust特有の概念、「値の所有権」
→メモリの安全性を担保する仕組み

以下のプログラムを書いてみる。

fn main() {
    let s = "hello".to_string();
    let t = s;
    printIn!("{}", s);
    printIn!("{}", t);
}

以下のエラーが出る。

   Compiling study-rust v0.1.0 (/Users/fujisawakazuki/Desktop/study-rust)
error[E0382]: borrow of moved value: `s`
  --> src/main.rs:10:20
   |
8  |     let s = "hello".to_string();
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
9  |     let t = s;
   |             - value moved here
10 |     println!("{}", s);
   |                    ^ value borrowed here after move

error: aborting due to previous error

このエラーについて解説していく。

所有者とは

まず、前提として所有権の理解が必要。

Rustにてメモリ上に確保された値には「所有者」が設定される。

  1. それぞれの値にはあるライフタイム(生存期間)を持ったただ 1 つの所有者(所有権 を持つ変数)がある
  2. どの値の所有者にもなっていない変数は「初 期化されていない状態」になり、参照できない
  3. ライフタイムが終了した値や所有者がいなくなった値はメモリ上からただちに破棄される

ライフタイムとは、大雑把に言えば、変数のスコープ。
(1),(2)はコンパイル時にチェックされる。

先程のエラーで、

  |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
9  |     let t = s;
   |             - value moved here

と出ていたが、ここで「hello」という文字列の所有権がsからtに移動した事を表している。
Rustでは常にstも同じ値を持っている状態が起きない。

10 |     println!("{}", s);
   |                    ^ value borrowed here after move

このエラー。
所有権が移動して所有権を失った変数を参照してはならない、ということになる。

このように、別の変数への束縛によって値の所有権が移動することを「ムーブセマンティクス」と呼んでいる。

別の変数への束縛によって値 所有権がコピーされることを「コピーセマンティクス」と呼んでいます。
例)

関数での所有権

関数実行時の引数渡しでも所有権の移動やコピーはおこる。

以下のサンプルコードでは関数myprint(s)の2回目の実行時にエラーになっている。

fn myprint<T: std::fmt::Display>(msg: T) {
 }   println!("{}", msg);
fn main() {
let s = "Hello".to_string();
myprint(s); // sの所有権が関数内の変数に移動
myprint(s); // sの所有権は移動し、初期化されていない変数になるのでエラーになる
}

これは1回目のmyprint(s)実行時にsの所有権を関数の引数のmsgに渡しているからである。

msgのライフタイムはスコープ内なので、myprint(s)の実行が終わったら、メモリから値は解放される。

参照による所有権の借用

&を付けると変数の所有権を変えずに「参照できる」

例)

fn myprint<T: std::fmt::Display>(msg: &T) {
// 参照によってmsgを受け取る
// 正確にはmsgという参照から値を取り出すので*msgとなるが、 // msgでもその参照が示す値と解釈される println!("{}", msg);
}
fn main() {
let s = "Hello".to_string();
myprint(&s); // 参照によって関数に渡している myprint(&s); // sが所有権を失わないので2回実行できる
}

参照には「共有参照」と「ミュータブルな参照」がある。
共有参照:参照はできるが、変更はできない
ミュータブルな参照:値を変更できる。書き方&mut

所有権の借用は「値がない参照が作られないこと」「変数が示す値が変更されることがあっても排他的に行われること」が守られている必要がある。

これに関するコンパイルエラーは多発する。

所有権とメモリ安全性

「所有権」の概念があることでなぜメモリ安全性が担保されるのか?

他言語の場合

C言語はメモリ管理をプログラマ側でやらないといけない
Java、Python、Ruby、PHP、JavaScriptなどの言語ではガベージコレクション機能がついている。
これはどこからも参照されなくなった値を勝手に解放してくれる機能。

欠点として、ガベージコレクションの実行タイミングが読めない事。
ゆえにアプリ起動中に実行されて、プログラムの動きが止まってしまう。

Rustの場合

ガベージコレクションが所有権という概念に沿って実行される。
→Java達の様に速度性能が低下しない。
→Cの様に自分たちで実行しなくて良い

しかし、所有権と仲良くならないと使いこなせない。
コンパイルエラーとの戦いが増える。

バッファーオーバーランの対策

バッファーオーバーランとは、メモリ領域を越えてデータの書き込みをして内容を破壊すること。
Rustはどの様に対策しているか。

実行時に時に異常終了してコンパイルエラーになる。

クロージャーとマルチスレッド

クロージャー

クロージャー。他言語での別名は「ラムダ式」、「無名関数」など。
要は名前のない関数。

例)

¦x¦ x + n

xを引数としてx+nを返す。

クロージャーの使い道

Rustにおいてクロージャーを使う場面が、「マルチスレッド化」である。

例)

fn main() {
// スレッド数
const N_THREADS : usize = 3;
// 処理する整数のRange
let series_range = 0..30; let add = 1;
// 1
let chunks = (0..N_THREADS)
        .map(¦ii¦ series_range.clone().skip(ii).step_by(N_THREADS));
// 2
let handles : Vec<_> = chunks
        .map(¦vv¦ std::thread::spawn(move ¦¦ {
        })  vv.for_each(¦nn¦ print!("{},", nn + add));
        ).collect();
// 3各スレッドの終了を待つ
handles.into_iter().for_each(¦hh¦ hh.join().unwrap());
}

このコードでは1、2の処理を並行して行っている。

このスクラップは2021/11/19にクローズされました