1K Views
March 10, 25
スライド概要
- Rust の基本
- 型安全性
- メモリ安全性
- スレッド安全性
機械学習・音声認識・プログラミングに関する書籍を執筆しています。
プログラミング〈新〉作法 ~これからプログラムを書く人のために ~ 7. Rust: 高性能と安全性の追求 の基本 型安全性 メモリ安全性 スレッド安全性 Rust 1
7.1 Rust の基本 の歴史 2006年頃 Mozilla Research により開発開始 2015年に1.0版がリリースされ,仕様が安定化 オープンソースで開発が継続され,2025年2月現在の最新版は 1.85.0 Rust の特徴 ランタイムを持たないのでシステムプログラムや高い性能を要求されるアプ リケーションに適している 新しいプログラミングパラダイムを取り入れている 所有権・借用のシステムや強い静的型付けの原則によって高い安全性を確保 Rust 2
の基本 7.1 Rust パッケージマネージャ Cargo による Rust プロジェクトの作成・ビルド・実行 cargo new myproj myproj myproj src src main.rs cargo build Cargo.toml main.rs target 実⾏に必要 な情報 Cargo.toml Finished dev [unoptimized + debuginfo] ... Running `target/debug/hello_world` cargo run Hello, world! 3
7.1 Rust
の基本
基本的な文法 : 関数とマクロ
関数定義 : fn 関数名(仮引数並び)-> 返却値型 {本体}
C言語と同じく,プロジェクト内の main() 関数から処理が始まる
ライブラリやモジュールは, use キーワードを使って読み込む
例)標準入出力モジュールは, use std::io; で読み込む
標準ライブラリ以外は Cargo.toml にクレート名とバージョンを記述する
マクロはコンパイル時に実際のコードに展開される
例) println! マクロは可変長引数が扱えるように展開される
名前の後の ! の有無で通常の関数と区別する
4
7.1 Rust の基本 変数と型 変数の宣言 let 変数名: 型 = 初期値; デフォルトはイミュータブル(不変) let mut 変数名: 型 = 初期値; でミュータブル(可変)にできる 型は,型推論により省略することもできる 型 数値型 : 符号つき整数型( i8, i16, ..., isize ),符号なし整数型 ( u8, ... , usize ),浮動小数点数型( f32, f64 ) 参照型 : & で参照を表す 文字列 : String は可変文字列. &str はリテラルや部分文字列への参 照なので値の変更はできない 5
7.1 Rust
の基本
データ構造
配列とタプル : イミュータブル
ベクタとマップ : 要素数が可変
配列
ベクタ
let array: [i32; 5] = [0, 1, 2, 3, 4]; //
let tuple: (i32, f64, &str) = (1, 2.0, "Masa"); //
let vec: Vec<i32> = vec![1, 2, 3]; //
let map: HashMap<&str, i32> = HashMap::from([("Hiro", 85)]); //
タプル
マップ
要素数が0のタプルはユニット型 () と呼ばれ,C言語の void に相当する
6
7.1 Rust
の基本
構造体( struct )
C言語の構造体に似ているが,メソッドを持つことができる
メソッドは関数と同じように定義し,第1引数を &self とすることで,その
構造体のメンバにアクセスできるようになる
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 { self.width * self.height }
}
7
7.1 Rust
の基本
トレイト( trait )
Rust にはクラスという概念がないが,構造体とトレイト(trait)を組み合わ
せることで,Java のインタフェースに似た機能を実現できる
トレイトの定義 : trait トレイト名 { fn メソッド名(&self) -> 返却値型; }
トレイトの実装 : impl トレイト名 for 構造体名 { ... }
8
7.1 Rust の基本 列挙型( enum ) 列挙型の特徴 それぞれの列挙子が異なる型を持つことができる 列挙型に対してメソッドを定義することができる 構造体と列挙型の違い 構造体は各要素が AND の関係を持つもの(すべての要素の値が揃わなけ れば全体の値とならない) 列挙型は各要素が OR の関係を持つもの(この型の値は列挙子のいずれ でもよい) 9
7.1 Rust
の基本
制御構造 : 条件分岐
if 式
条件判定後の分岐部分の値を返す
変数への代入などを行う場合, else 節は必ず必要で,返却値の型は一
致している必要がある
例) let result = if number > 5 {"large"} else {"small"};
match 式
if 式の複数条件版
すべての条件にマッチするデフォルトは, _ として,分岐の最後に書く
match 式は列挙型と組み合わせて用いることが多く,パターンマッチン
グにより列挙子の値に応じた処理を行う
10
7.1 Rust
の基本
制御構造 : 繰り返し
for : イテレータを使った繰り返し
while : 条件が満たされる間の繰り返し
loop : 無限ループを表し, break でループを抜ける
break の後に値を書くことで,その値を loop から返すことができる
let mut count = 0;
let result = loop {
println!("count = {}", count);
count += 1;
if count == 10 {
break count;
}
};
11
7.1 Rust
の基本
数値計算プログラムの例 : ニュートン法で平方根を求める
use std::io;
fn input_number() -> i32 {
loop {
println!("Enter a positive integer: ");
let mut input = String::new();
io::stdin().read_line(&mut input).expect("Read error");
match input.trim().parse::<i32>() {
Ok(num) if num > 0 => return num,
_ => println!("Input error!"),
}
}
}
12
fn calculate_squareroot(x: f64) -> f64 {
let mut rnew = x;
while (rnew - x / rnew).abs() > 1.0E-5 {
rnew = (rnew + x / rnew) / 2.0;
}
rnew
}
fn main() {
let x: i32 = input_number();
let sq: f64 = calculate_squareroot(x as f64);
println!("Square root of {} is {:7.5}", x, sq);
}
Enter a positive integer:
5
Square root of 5 is 2.23607
13
7.2 型安全性 型安全性とは プログラムの実行時に型の不整合が発生しないようにすること コード中の各部分で扱う型について矛盾がないことをコンパイル時にチ ェックする 変数の値が null である可能性や関数がエラーを返す可能性も,型の概念を使 って表現できる 14
7.2 型安全性 型の分類 静的・動的 静的型付け : 宣言時に型を明示する 動的型付け : 変数に値が代入されることによって型が決まる 強弱 弱い型付け : 異なる型間の演算において,暗黙的に型変換を行う 強い型付け : 型が異なるものの演算は明示的な型変換を必要とする 静的型付け 動的型付け 強い型付け Java, Rust Elixir, Python 弱い型付け C JavaScript 15
7.2
型安全性
型推論
Rust
は静的型付け言語であるが,型推論により宣言時に型を省略できる
fn main() {
let number = 50;
// i32
let text = "Hello world!";
// &str
let numbers = vec![1, 2, 3]; // Vec<i32>
let first = numbers.first(); // Option<&i32>
let add_one = |x| x + 1;
//
let result = add_one(5);
...
}
型と推論される
型と推論される
型と推論される
型と推論される
下行の右辺からクロージャ |x:i32|->i32 と推論される
16
7.2 型安全性 列挙型による値の限定 列挙型 enum を使って,決められたいくつかの型のみを値として取る型を定 義することができる 列挙型と match 文を組み合わせることで,すべての値についての処理が定義 されていることをコンパイラに保証させることができる この仕組みの典型的な活用例が Option 型と Result 型 17
7.2 型安全性 単純な列挙型の定義 enum Signal { Red, Yellow, Blue, } 列挙型の取り得る値を列挙子とよぶ 定義した列挙子を値として扱うときは, Signal::Red のように型名と値をス コープ区切りの :: でつないで表す 18
7.2
型安全性
値を持つ列挙型と match 文
列挙子はそれぞれ異なる型・個数の値を持つことができる
enum Command {
Move {x: i32, y: i32},
ChangeColor (u8, u8, u8),
Quit,
}
match
文で列挙子に応じた処理を行うことができる(値による分岐も可)
let cmd = Command::Move {x: 10, y: 20};
match cmd {
Command::Move {x, y} => println!("Move to ({}, {})", x, y),
Command::ChangeColor (r, g, b) => println!("Change color to ({}, {}, {})", r, g, b),
Command::Quit => println!("Quit"),
}
19
7.2 型安全性 型 値が存在するかどうかを表す列挙型 値がある場合は「Some(値)」を,値がない場合は None を返す 値の型はジェネリックス Option enum Option<T> { Some(T), None, } 20
7.2
型安全性
Option
型を用いたコード例
struct Person {
name: String
}
impl Person {
fn get_name(&self) -> &String {&self.name}
}
fn get_person() -> Option<Person> {
//Some(Person{name: "Taro".to_string()})
None
}
21
7.2
型安全性
fn main() {
let optional_person = get_person();
match optional_person {
Some(person) => println!("
: {}", person.get_name()),
None => println!("Person
"),
}
}
名前
オブジェクトは存在しません.
オブジェクトは存在しません.
Person
22
7.2 型安全性 型 関数の戻り値としてエラーの有無を表す列挙型 正常終了の場合は「Ok(値)」を,エラーの場合は「Err(エラー値)」を返 す 値やエラー値の型はジェネリックス Result enum Result<T, E> { Ok(T), Err(E), } 23
7.2
型安全性
Result
型を用いたコード例
use std::fs::File;
use std::io::{self, Read};
ファイルの内容を読み込み,結果を
型で返す関数
//
Result
fn read_file_contents(file_path: &str) -> Result<String, io::Error> {
let mut file = File::open(file_path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
24
7.2
型安全性
fn main() {
match read_file_contents("data.csv") {
Ok(contents) => {
println!("File contents: {}", contents);
},
Err(e) => {
println!("An error occurred: {}", e);
}
}
}
//
ファイルの内容を出力
//
エラーを出力
80, Alice
65, Bob
70, Caroline
93, David
77, Eve
25
7.2 型安全性 のエラー処理 : match 文によるパターンマッチ以外の方法 if let Some(x) = y { ... } else { ... } で代入が成功した場合と,エ ラーが返ってきた場合の処理をそれぞれ記述することができる y は Option 型 Result 型の場合は if let Ok(x) = y { ... } else { ... } 関数末尾に ? を付ける 値が Some または Ok の場合はその値を取り出す None または Err の場合はエラーを表す列挙子を返す unwrap() メソッドは,値を強制的に取り出す Rust 26
7.3 メモリ安全性 言語のメモリ管理 メモリの確保と解放はプログラマが明示的に行う プログラマのミスでメモリリークや二重解放が発生する可能性がある ソースコード中のどこでメモリ解放が行われるかが確定的にわかる Java のメモリ管理 ガベージコレクタにより,不要なメモリを自動的(=暗示的)に解放する ガベージコレクタが動作するタイミングは非確定的 メモリ解放処理でプログラムが停止するタイミングが予測できない C 27
7.3 メモリ安全性 のメモリ管理手法 エラーがなく,実行性能が高いプログラムのために,暗示的・確定的なメモ リ管理を実現させたい 所有権と借用によるメモリ安全性の実現 所有権 : ヒープ上のメモリを所有する変数が1つだけ存在する 代入や関数呼び出しにより,所有権が移動する 借用 : 所有権を持つ変数が他の変数に対して参照を貸し出す 参照はイミュータブル(不変)またはミュータブル(可変) Rust 28
7.3
メモリ安全性
Rust
におけるメモリの確保・解放の例
fn main() {
let my_vec = vec![1, 2, 3, 4, 5]; //
consume_vector(my_vec); // my_vec
}
ヒープ上にメモリを確保
の所有権が移動
// println!("{:?}", my_vec); // コメントを外すとコンパイルエラー
fn consume_vector(v: Vec<i32>) {
println!("Consuming vector: {:?}", v);
// v
}
のスコープが終了すると,自動的にメモリが解放される
Consuming vector: [1, 2, 3, 4, 5]
29
7.3
メモリ安全性
Rust
における借用
fn main() {
let my_vec = vec![1, 2, 3, 4, 5];
consume_vector(&my_vec); // my_vec
の参照をわたす
println!("{:?}", my_vec);
}
fn consume_vector(v: &Vec<i32>) {
println!("Borrowing vector: {:?}", v);
}
Borrowing vector: [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
30
7.3
メモリ安全性
ライフタイム : 参照が有効である期間を示す
'a のように ' に続く小文字の識別子で表現する
ライフタイムの指定が必要になる例
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y } //
}
fn main() {
let s1 = String::from("short");
let s2 = String::from("longer");
let result = longest(s1.as_str(), s2.as_str());
println!("The longest string is {}", result);
}
返却値によってライフタイムが異なっては困る
The longest string is longer
31
7.4 スレッド安全性 から見たプログラムの実行単位 プロセス メモリ空間やファイルシステムなど,リソースを独立して持つ ひとつのプロセスが異常終了しても他のプロセスに影響を与えない スレッド プロセス内で実行されるプログラムの実行単位 プロセス内のリソースを共有する スレッド間でのデータ共有による競合状態やデッドロックが発生する可 能性がある OS 32
7.4 スレッド安全性 スレッド安全性とは 複数のスレッドが同時にデータにアクセスするときに,データの整合性が保 たれること あるスレッドがデータの変更を始めたら,終了するまで他のスレッドが データを読み出したり変更したりすることができないようにする デッドロック(2つ以上のスレッドが相互にリソースを待ち合う状態)の回避 リソースをロックする仕組み 33
7.4 スレッド安全性 スレッド間でのデータの競合を避けるRustの機能 Arc (Atomic Reference Counting) : 複数のスレッド間で所有権を共有 Mutex (mutual exclusion): 共有データへの同時アクセスを制御 spawn() : 新しいスレッドを生成 join() : 全てのスレッドの完了を待つ 34
7.4 スレッド安全性 Rust におけるスレッド安全なカウンタ use std::sync::{Arc, Mutex}; use std::thread; const NUM_THREADS: i32 = 10000; 35
7.4
スレッド安全性
fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..NUM_THREADS {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Result: 10000
36
7.5 まとめ の特徴 型安全性 : 型の不適切な使用をコンパイル時に検出 メモリ安全性 : 所有権システムによりメモリ管理を行う スレッド安全性 : スレッド間でのデータの競合を避ける機能が提供されてい る Rust 37