Rust 勉強メモ
当初の目的通り、簡易的なCLIツールを作るところまではいけたので本スクラップはクローズ。
このスクラップについて
- 筆者が Rust を勉強する際のメモ用途で、まとまりはないし、浅い理解とか嘘とか混じると思う
- 普通に途中で投げ出す可能性も高い
- めちゃくちゃ AI (ChatGPT o1) との対話ベースで学んでるので、基本的には AI の生成内容を貼り付けて、それに対する自身の考え、理解を返信の形式で書く
- 筆者は2年ぐらい前に、ちょっとだけチュートリアルで Rust に触れた程度で、所有権周りがやたら小難しいやつって記憶しかない
- フロントエンドツールチェインが本格的に Rust で書かれる時代になってきたので、フロントエンジニアの教養として、TypeScript に次ぐ第二言語として抑えたい
ゴール
- それなりに理解して、簡単なCLIツールを自分で書けるようにすること
Rustの概説
Rustは、高いパフォーマンスと安全性を両立したシステムプログラミング言語です。主な特徴は以下のとおりです。
- メモリ安全性:Rustの所有権システムにより、メモリ管理のエラー(例:ダングリングポインタ、二重解放など)をコンパイル時に防止します。
- 高いパフォーマンス:CやC++に匹敵する速度で動作し、低レベルの制御が可能です。
- 並行性の容易さ:データ競合をコンパイル時に検出し、安全な並行処理を実現します。
- 豊富なエコシステム:公式のパッケージマネージャー兼ビルドツールであるCargoや、クレートと呼ばれるパッケージシステムが充実しています。
あなたのRubyやTypeScriptでの経験は、Rustの習得に大いに役立つでしょう。特に、型システムや関数型プログラミングの概念は共通点があります。
Q. ダングリングポインタとはなんですか?
ダングリングポインタとは、無効なメモリ領域を指すポインタであり、プログラムの安全性と信頼性を損ないます。
Rustは所有権システムによって、ダングリングポインタの発生をコンパイル時に防ぎます。
メモリ安全性はRustの大きな強みであり、安心してソフトウェア開発を行うことができます。
Cargo とクレートの違いがわからなかったけど、npm と npm パッケージの違いっぽい。
Cargo は Rust のプロジェクトに必要なものが一通り詰まってるツールで、クレートは Rust のライブラリの単位。
環境構築
インストール (デフォルトバージョンでOK)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
パスを通す
source $HOME/.cargo/env
バージョン確認。日付的に最新版かそれに近いバージョンと考えて良さそう。
$ rustc --version
rustc 1.82.0 (f6e511eec 2024-10-15)
$ cargo --version
cargo 1.82.0 (8f40fc59f 2024-08-21)
vscode の公式拡張をとりあえず入れておく
作業用ディレクトリを切って、その中でプロジェクトを作成
$ mkdir rust-study
$ cd rust-study
$ cargo new hello_world
$ cd hello_world
HelloWorld コードが実行できることを確認
fn main() {
println!("Hello, world!");
}
$ cargo run
Compiling hello_world v0.1.0 (/Users/shingo.sasaki/study/2024/rust-study/hello_world)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.79s
Running `target/debug/hello_world`
Hello, world!
しばらくはコードを編集しては挙動を見てを無限に繰り返すと思うから、ファイルを保存したら自動で実行するぐらいしてほしいな。
これでいけた。
$ cargo watch -x run
変数・定数・データ型
Rustでは、変数と定数の宣言と使用方法に独自の特徴があります。ここでは、イミュータブル(不変)な変数、ミュータブル(可変)な変数、定数、そしてシャドーイングについて説明します。
イミュータブルな変数
デフォルトでは、Rustの変数はイミュータブル、つまり値を変更できない不変の変数として宣言されます。
fn main() {
let x = 5;
println!("x の値は {}", x);
}
ミュータブルな変数
fn main() {
let mut y = 5;
println!("y の値は {}", y);
y = 6;
println!("y の新しい値は {}", y);
}
シャドーイング
Rustでは、同じ名前の変数を再度宣言してシャドーイングすることができます。これにより、新しい変数が前の変数を覆い隠す形で再宣言されます。
fn main() {
let z = 5;
let z = z + 1; // シャドーイング
println!("シャドーイング後の z の値は {}", z); // 6
{
let z = z * 2;
println!("ブロック内の z の値は {}", z); // 12
}
println!("ブロック外の z の値は {}", z); // 6
}
最初のlet z = 5;で変数zを宣言します。
let z = z + 1;で新たにzを宣言し、以前のzをシャドーイングしています。
ブロック内({})でさらにzを宣言し、シャドーイングしています。
シャドーイングの利点
- 変数名を再利用しつつ、値や型を変更できます。
- ミュータブルな変数を使わずに値を更新できます。
定数
定数は、プログラムの実行中に決して変更されない値を持ちます。定数を宣言するにはconstキーワードを使用し、必ず型を指定します。
const MAX_POINTS: u32 = 100_000;
定数名は、慣例的にすべて大文字で記述し、単語の区切りにアンダースコア(_)を使用します。
定数はグローバルスコープで宣言できます。
mutを使って可変にすることはできません。
変数と定数の違い
特性 | 変数(Variables) | 定数(Constants) |
---|---|---|
可変性 | デフォルトで不変 | 不変(常に不変) |
mutの使用 | 使用可能 | 使用不可 |
型注釈 | 省略可能(推論される) | 必須 |
スコープ | ブロックスコープ | グローバルスコープも可 |
シャドーイング | 可能 | 不可能 |
型注釈
Rustは強い型付けの言語であり、コンパイラは多くの場合、変数の型を推論しますが、必要に応じて型注釈を追加できます。
fn main() {
let guess: u32 = "42".parse().expect("数値を入力してください");
println!("guess の値は {}", guess);
}
let guess: u32
のように、変数名の後にコロンと型名を記述します。
型注釈が必要な場合
- 型が曖昧でコンパイラが推論できないとき。
- コードの可読性を高めたいとき。
数値型
数値型には、以下のような種類があります。
整数型
符号付き(負の値も扱える):i8, i16, i32, i64, i128, isize
符号なし(正の値のみ):u8, u16, u32, u64, u128, usize
浮動小数点型:
f32(32ビット)
f64(64ビット、デフォルト)
fn main() {
let x = 42; // デフォルトでi32型
let y: u8 = 255;
let z: f64 = 3.1415;
println!("x = {}, y = {}, z = {}", x, y, z);
}
文字型とブーリアン型
文字型(char):
シングルクオートで囲みます。
Unicodeスカラー値を扱えるため、ASCII以外の文字も使用可能。
fn main() {
let c = '🍎';
println!("文字: {}", c);
}
ブーリアン型(bool):
trueまたはfalseの値を持ちます。
fn main() {
let is_active = true;
println!("is_active = {}", is_active);
}
タプルと配列
タプル: 異なる型の値をまとめて扱えます。
fn main() {
let tup: (i32, f64, char) = (500, 6.4, '🚀');
let (x, y, z) = tup; // 分解して変数に代入
println!("x = {}, y = {}, z = {}", x, y, z);
}
配列:同じ型の値を固定長で格納します。
fn main() {
let a = [1, 2, 3, 4, 5];
println!("a[0] = {}", a[0]);
}
変数の使い方の例
fn main() {
// イミュータブルな変数
let name = "Alice";
println!("こんにちは、{}さん!", name);
// ミュータブルな変数
let mut age = 30;
println!("年齢は {} 歳です。", age);
age += 1;
println!("誕生日おめでとう!あなたは {} 歳になりました。", age);
// 定数
const SECONDS_IN_MINUTE: u32 = 60;
println!("1分は {} 秒です。", SECONDS_IN_MINUTE);
// シャドーイングを使った型変換
let spaces = " "; // 文字列型
let spaces = spaces.len(); // 整数型にシャドーイング
println!("スペースの数は {}", spaces);
}
シャドーイングだいぶキショいな。
これそんな活用することあるんかな。
疑問点を元に深ぼったポイント
- シャドーイングはブロックスコープでイミュータブル変数の名前を再利用するのに有用
-
const
はコンパイル時に値が決定するため、ランタイムで値が決まるものには使用できない - 配列はそのサイズも型の一部として扱われる
- タプルは異なる型をまとめられるので、各要素で意味合いが異なる用途(関数の返り値) などに適している
関数の定義と使用
構文
fn 関数名(引数: 型, ...) -> 戻り値の型 {
// 関数の本体
}
引数のない関数
fn main() {
greet("Alice");
}
引数のある関数
fn greet(name: &str) {
println!("こんにちは、{}さん!", name);
}
引数の指定と戻り値の受取
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
fn main() {
let result = multiply(4, 5);
println!("結果は {}", result);
}
関数内で最後に評価された式が戻り値になるタイプね。
今更だけど、Rust の標準スタイルガイドではインデントは4スペースみたい。しゃーないので vscodeの設定を追加する。
// 言語 Rust
"[rust]": {
"editor.tabSize": 4
},
制御フロー(条件分岐とループ)
if 式
fn main() {
let number = 7;
if number < 5 {
println!("number は5より小さいです");
} else if number == 5 {
println!("number は5です");
} else {
println!("number は5より大きいです");
}
}
- 条件式はbool型でなければなりません。
- 他の言語のように、0や空文字をfalseとみなすことはありません。
ifは式として扱えるため、値の代入に利用できます。
fn main() {
let condition = true;
let number = if condition { 5 } else { 10 };
println!("number の値は {}", number);
}
- ifとelseのブロックは、同じ型の値を返す必要があります。
loop
fn main() {
let mut counter = 0;
loop {
counter += 1;
println!("カウンター: {}", counter);
if counter == 5 {
break;
}
}
}
- breakでループを抜けます。
- continueで次のループへスキップできます。
while
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("発射!");
}
for
コレクションに対するループ
fn main() {
let array = [10, 20, 30, 40, 50];
for element in array.iter() {
println!("要素の値は {}", element);
}
}
Range を使ったループ
fn main() {
for number in 1..5 {
println!("{}回目のループ", number);
}
}
Match
パターンパッチによる分岐
fn main() {
let number = 3;
match number {
1 => println!("1です"),
2 => println!("2です"),
3 => println!("3です"),
_ => println!("それ以外の数字です"),
}
}
- _はワイルドカードパターンで、どのパターンにもマッチしなかった場合に使用します。
- matchはすべての可能性を網羅する必要があります。
- パターンの書き方はめちゃくちゃ色々あるけどここでは割愛
パターンマッチがめちゃくちゃ強力そうな雰囲気を感じる。パターンガードやレンジと組み合わせて何でもできそう。
所有権
Rustの一番の特徴と言えば、やっぱり所有権システムなの。これは他の言語にはない独自の仕組みで、メモリの安全性を高めてくれるのよ。一緒にこの大事な概念を理解していきましょう!
1. 所有権の基本ルール
所有権には3つの大切なルールがあるの
- 各値は変数とともに所有される:すべての値には所有者がいて、その所有者は変数よ。
- 所有者は一度に一つだけ:値の所有権は一つの変数しか持てないの。
- 所有者がスコープを抜けると値は破棄される:変数が使えなくなると、その持っていた値もメモリから解放されるわ。
やや記憶残ってる。
値は一つ変数が所有していて、通常はその変数以外から利用できないこと。変数はスコープを持つから、それを抜けると値は変数ごと破棄されること。
で、確か所有権を貸し出すことで他の変数に値を受け渡せる仕組みがあるんだよね。
2. 所有権のムーブ(移動)
値を別の変数に代入したり、関数に渡したりすると、所有権が移動するの。これをムーブって呼ぶわ。
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1の所有権がs2に移動する
// println!("{}", s1); // エラーになるよ!s1はもう使えないの
println!("{}", s2); // "hello" と表示される
}
明示的に貸し出さずとも、他の変数に代入したらその時点で所有権は移っちゃって、元の方からは参照不可能になるんだ。きびし〜。
3. クローンでデータをコピー
もし、所有権を移動させずに値をコピーしたいときは、cloneメソッドを使うの。
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone(); // データを深くコピーする
println!("s1 = {}, s2 = {}", s1, s2); // 両方とも使えるよ
}
cloneを使うと、ヒープ上のデータもちゃんとコピーされるから、s1も s2もそれぞれ独立して使えるの。
まぁ複製して新しい値にしてるんだから、それぞれに所有者ができるわけね。
4. スタック上のデータとCopyトレイト
整数とか浮動小数点数みたいなシンプルな型(スカラー型)は、所有権が移動しないで、コピーされるの。
fn main() {
let x = 5;
let y = x; // xの値がコピーされる
println!("x = {}, y = {}", x, y); // xもyも使えるよ
}
i32型とかはサイズが決まってて、スタックに置かれるから、コピーもサクッとできちゃうの。
あー、最初の例でわざわざ String クラスを使ったのはそういうことね。
参照渡しと値渡しみたいなものか。値渡しなら所有権は新しくなると。
これこんがらがる要因になるから気をつけないとな。
5. 所有権と関数
関数に値を渡すときも、所有権が移動するかどうかを意識しないといけないわ。
fn main() {
let s = String::from("hello");
takes_ownership(s); // sの所有権が関数に移動
// println!("{}", s); // エラー!sはもう使えない
let x = 5;
makes_copy(x); // xはCopyなので所有権は移動しない
println!("{}", x); // xはまだ使えるよ
}
fn takes_ownership(some_string: String) {
println!("{}", some_string);
}
fn makes_copy(some_integer: i32) {
println!("{}", some_integer);
}
String型は所有権が移動するけど、i32みたいなCopyな型は所有権が移らないの。
関数に渡すってことは、その関数の引数である変数に所有権が移動するからか。
所有権のシステムがなかったら、呼び出した関数の先でデータを改変されるおそれがあるから、改変されてても問題ないように以降で参照できないようになってる感じかな。
6. 所有権を戻す
所有権を渡したくないときは、参照を使うといいわ。
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // s1を参照渡し
println!("'{}' の長さは {} です。", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
おーん?この場合、 calculate_length の中でデータの改変させられちゃわないのかな。
参照で受け取った場合は変更ができないとかある…?
7. 可変な参照
値を変更したいときは、可変な参照を使うの。
fn main() {
let mut s = String::from("hello");
change(&mut s);
println!("{}", s); // "hello, world" と表示される
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
改変できるんかい。じゃあ何も守れてなくない?
あとあとまとめて確認しよ。
8. 参照のルール
参照を使うときにはいくつかルールがあるの
- 同時に一つの可変参照しか持てない。
- 不変の参照はいくつでも持てる。
- 可変参照と不変参照を同時に持てない。
fn main() {
let mut s = String::from("hello");
let r1 = &s; // 不変な参照
let r2 = &s; // 不変な参照
let r3 = &mut s; // エラー!不変な参照がある間は可変な参照を持てない
println!("{}, {}, {}", r1, r2, r3);
}
あー、このルールのおかげでちょっとだけイメージ湧いてきたかも。
一箇所からしか同時に変更されないことと、参照される可能性がある場合は編集できないみたいなのが保証されそうな気配がする。
良いのか。複数個所が書き込み権限を持っている状態が発生しないから、マルチスレッドにおいても競合が発生しないから安全で高速な実行を実現できるのね。
可変参照があるから、関数に渡したデータが書き換わっている可能性は十分にあるけど、可変参照自体はそれを理解した上で慎重に使う感じか。
構造体
構造体は、自分だけのカスタムデータ型を作るためのものよ。
構造体の定義
ユーザー型
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
インスタンスの作成
fn main() {
let user1 = User {
username: String::from("Alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
active: true,
};
println!("ユーザー名: {}", user1.username);
}
構造体の更新
構造体インスタンスをミュータブルにすれば変更可能
fn main() {
let mut user1 = User {
username: String::from("Alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
active: true,
};
user1.email = String::from("alice_new@example.com");
println!("新しいメールアドレス: {}", user1.email);
}
構造体インスタンスの一部から値をコピーして新しいインスタンスを作成可能。
(この場合、プリミティブでないフィールドは所有権の移動が発生するので、コピー元は利用不可になる)
fn main() {
let user1 = User {
username: String::from("Alice"),
email: String::from("alice@example.com"),
sign_in_count: 1,
active: true,
};
let user2 = User {
email: String::from("bob@example.com"),
..user1
};
println!("ユーザー2の名前: {}", user2.username);
}
タプル構造体
型をカスタムしたタプルの定義も可能
struct Color(i32, i32, i32);
fn main() {
let black = Color(0, 0, 0);
println!("黒のRGB値: ({}, {}, {})", black.0, black.1, black.2);
}
構造体にメソッドを定義する
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!("面積は {} 平方ピクセルです。", rect.area());
}
関連関数
構造体に生やす関数ではあるが、self を受け取らずに、構造体の名前経由で参照する関数
impl Rectangle {
fn new(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}
fn main() {
let rect = Rectangle::new(30, 50);
println!("幅: {}, 高さ: {}", rect.width, rect.height);
}
構造体でデータ構造を作ってからそこにメソッドを生やすタイプの言語か。
関連関数はクラスメソッドみたいなものね。
関連関数は汎用関数のネームスペースを切る用途でも使えたりするのかな?それはまた別にモジュールシステムがある気もするけど。
普通にある。
mod my_module {
pub fn my_function() {
println!("Hello from my_function!");
}
}
のでネームスペースを切るんじゃなくて、明確にその構造体と関連するロジック置き場になる感じだな。
列挙型
列挙型の定義
他の言語の列挙型と比べると、値を羅列するのではなく、複数の異なる方を一つの方としてまとめる役割がある。 Message 型は、 Quit,Move,Write,ChangeColor のいずれかの型に属すことになる。
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
列挙型の使用
fn main() {
let msg = Message::Write(String::from("こんにちは"));
match msg {
Message::Quit => println!("プログラムを終了します"),
Message::Move { x, y } => println!("座標移動: x={}, y={}", x, y),
Message::Write(text) => println!("メッセージ: {}", text),
Message::ChangeColor(r, g, b) => println!("色変更: r={}, g={}, b={}", r, g, b),
}
}
Option型
言語組み込みの特別な列挙型 Option<T>
値がある(T) かない(None)のどちらかを取る。
fn main() {
let some_number = Some(5);
let absent_number: Option<i32> = None;
println!("some_number は {:?}", some_number);
println!("absent_number は {:?}", absent_number);
}
Option<T> の実態
enum Option<T> {
Some(T),
None,
}
パターンマッチング
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
Some(i) => Some(i + 1),
None => None,
}
}
fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
println!("six は {:?}", six);
println!("none は {:?}", none);
}
Result 型もこれで表現されてるんだ。
enum Result<T, E> {
Ok(T),
Err(E),
}
エラー処理
Result 型によるエラー処理
enum Result<T, E> {
Ok(T),
Err(E),
}
- Ok(T):成功した場合に結果の値 T を含む。
- Err(E):失敗した場合にエラー情報 E を含む。
Result 型の使い方
ファイル操作や数値のパースなど、エラーが発生する可能性のある操作は、Result 型を返すの。
use std::fs::File;
fn main() {
let result = File::open("hello.txt");
let file = match result {
Ok(file) => file,
Err(error) => {
println!("ファイルを開く際にエラーが発生しました: {:?}", error);
return;
}
};
// ファイルの操作を続ける
}
ファイル読み込み失敗時、関数を早期リターンしているため、 file
の型は File
に安全に絞られる。
エラーの種類に応じた処理
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let result = File::open("hello.txt");
let file = match result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => {
println!("ファイルが見つかりませんでした。新しく作成します。");
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
println!("ファイルの作成に失敗しました: {:?}", e);
return;
}
}
}
other_error => {
println!("ファイルを開く際に問題がありました: {:?}", other_error);
return;
}
},
};
// ファイルの操作を続ける
}
ファイル読み込み失敗時、エラーの種別に応じて再びパターンマッチを行い、エラーごとのハンドリングを行う。
?
演算子
エラーの伝搬と ?
演算子を付与することで、エラー発生時に即座に Error<T> を関数で返して終了する。
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> io::Result<String> {
let mut file = File::open("username.txt")?; // エラーがあれば返す
let mut username = String::new();
file.read_to_string(&mut username)?; // エラーがあれば返す
Ok(username)
}
fn main() {
match read_username_from_file() {
Ok(name) => println!("ユーザー名は {} です。", name),
Err(e) => println!("エラーが発生しました: {:?}", e),
}
}
unwrap と expect
unwrap()
は、エラーが発生しないと信じて簡易的に中身を取り出せる。エラーだった場合パニックになる。
let file = File::open("hello.txt").unwrap();
expect
を使うと、パニックのエラーメッセージまで定義できる。
let file = File::open("hello.txt").expect("ファイルを開けませんでした");
Result 型とパターンマッチによってエラーハンドリングを柔軟に行えるのが強力な一方、unwrap()
みたいなラフに使える機能もあって書き心地良さそうに感じる。
モジュールと名前空間
モジュールの定義
mod greetings {
pub fn say_hello() {
println!("こんにちは!");
}
fn secret_greeting() {
println!("これは秘密の挨拶です。");
}
}
fn main() {
greetings::say_hello();
}
モジュール内の関数はデフォルトでは非公開なので、 公開する場合は pub
キーワードが必要。
モジュールのファイル分割
ファイルを分けることで、ファイルの中身をモジュールとしてインクルードできる。
greeting.rs
pub fn say_hello() {
println!("こんにちは!");
}
fn secret_greeting() {
println!("これは秘密の挨拶です。");
}
main.rs
mod greetings;
fn main() {
greetings::say_hello();
}
サブモジュールの作成
ネスト可能
mod outer {
pub mod inner {
pub fn inner_function() {
println!("こんにちは、内部モジュールから!");
}
}
}
fn main() {
outer::inner::inner_function();
}
use
によって名前空間のスコープを持ち込める。
use outer::inner::inner_function;
fn main() {
inner_function();
}
コレクション型
Vec<T>
)
ベクタ (ベクタは同じ型の値を動的に格納できる可変長の配列
空のベクタを作成
fn main() {
// 型を明示的に指定
let mut numbers: Vec<i32> = Vec::new();
println!("{:?}", numbers); // 出力: []
}
初期値つき
vec!
マクロによって初期値を簡単に作成可能
fn main() {
// 初期値を持つベクタを作成
let fruits = vec!["Apple", "Banana", "Cherry"];
println!("{:?}", fruits); // 出力: ["Apple", "Banana", "Cherry"]
}
要素の追加
fn main() {
let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
numbers.push(3);
println!("{:?}", numbers); // 出力: [1, 2, 3]
}
要素へのアクセス
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
// インデックスでアクセス(不変参照)
let first = numbers[0];
println!("最初の要素: {}", first); // 出力: 最初の要素: 10
}
get
メソッドを使うと安全にアクセス可能
fn main() {
let numbers = vec![10, 20, 30, 40, 50];
// `get` メソッドを使う(Option<&T> を返す)
match numbers.get(2) {
Some(number) => println!("3番目の要素: {}", number), // 出力: 3番目の要素: 30
None => println!("存在しない要素です。"),
}
}
要素の変更
ベクタ自体がミュータブルの場合のみ変更可能
fn main() {
let mut numbers = vec![1, 2, 3];
numbers[1] = 20;
println!("{:?}", numbers); // 出力: [1, 20, 3]
}
for ループ
fn main() {
let fruits = vec!["Apple", "Banana", "Cherry"];
for fruit in &fruits {
println!("{}", fruit);
}
}
イテレーター
fn main() {
let numbers = vec![1, 2, 3, 4, 5];
// イテレータを使って各要素を2倍にする
let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
println!("{:?}", doubled); // 出力: [2, 4, 6, 8, 10]
}
削除
fn main() {
let mut numbers = vec![10, 20, 30, 40, 50];
numbers.remove(2); // インデックス2の要素(30)を削除
println!("{:?}", numbers); // 出力: [10, 20, 40, 50]
}
あるいは retain メソッドで条件付きで削除
fn main() {
let mut numbers = vec![1, 2, 3, 4, 5];
// 偶数のみを残す
numbers.retain(|&x| x % 2 != 0);
println!("{:?}", numbers); // 出力: [1, 3, 5]
}
fn main() {
let scores: Vec<i32> = vec![85, 92, 78, 90, 88];
let passing_scores: Vec<i32> = scores.iter().filter(|x| **x >= 80).map(|&x| x).collect();
let sum: i32 = passing_scores.iter().sum();
println!("sum is {}", sum);
}
のうち、map
や filter
を使ったときのコールバック関数の引数の型がわけわからなったので整理する。
let scores = vec![85, 92, 78, 90, 88];
let iter = scores.iter(); // イテレータの型は Iterator<Item = &i32>
iter
はコレクションからイテレータを作るので、この時点でイテレーターオブジェクト的なのができる。まだ知らないけどトレイトって仕組みで、まぁインタフェースとか抽象クラスとかそういうものだとしておこう。
で、Vec<i32>
の各要素は i32 への参照だから &i32 になる。だからこのイテレーターは &i32
を扱う。続けて filter を使う。
let passing_scores: Vec<i32> = scores.iter().filter(|x| **x >= 80);
filter は各要素への参照を受けとる。元々 &i32 という参照だったのが更に参照になるので、 **x
で値を見に行く必要がある。
filter の返り値は、各要素の参照を使って、クロージャが true を返したもののイテレーターになるので、やはり[参照の参照]のイテレータになる。
ので、map で各[参照の参照]から[参照]を取り出す。
let passing_scores = scores.iter().filter(|x| **x >= 80).map(|&x| x);
まだイテレータなので、最後にコレクションに戻す。これで [参照] のコレクションが手に入る。つまり Vec<i32> になる。
なんで filter のクロージャーでいちいち参照を渡すんだって思ったけど、普通に渡したら所有権の移動になっちゃうからか。むずいー。
HashMap<K, V>
)
ハッシュマップ (キーバリュー型のコレクション。
要素の追加
use std::collections::HashMap;
fn main() {
// 空のハッシュマップを作成
let mut scores = HashMap::new();
// キーと値のペアを追加
scores.insert("Blue", 10);
scores.insert("Yellow", 50);
println!("{:?}", scores); // 出力例: {"Blue": 10, "Yellow": 50}
}
要素の参照
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Blue", 10);
scores.insert("Yellow", 50);
// キーでアクセス(Option<&V> を返す)
match scores.get("Blue") {
Some(&score) => println!("Blue team's score: {}", score),
None => println!("Blue team not found."),
}
// イテレータを使って全てのキーと値を表示
for (key, value) in &scores {
println!("{}: {}", key, value);
}
}
要素の更新
同じキーに対して再度insertすると上書きになる
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Blue", 10);
scores.insert("Yellow", 50);
// 既存のキーに新しい値を挿入(上書き)
scores.insert("Blue", 25);
println!("{:?}", scores); // 出力例: {"Blue": 25, "Yellow": 50}
}
要素の削除
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Blue", 10);
scores.insert("Yellow", 50);
// キーを指定して要素を削除
scores.remove("Blue");
println!("{:?}", scores); // 出力例: {"Yellow": 50}
}
ハッシュマップの使用例
文字列の出現回数をカウントする
use std::collections::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut word_count = HashMap::new();
for word in text.split_whitespace() {
let count = word_count.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", word_count);
}
entry / or_insert 便利だな。多言語でもこういう構文が欲しくなることよくある。
質問
HashMap にデータを追加する際に、insert メソッドを使用しましたが、そのためには変数をミュータブルにする必要がありました。
変数宣言時に初期値として最初からリテラルでマップを与えることで、イミュータブルにすことはできないんですか?
結果、普通にできるっぽい。それを最初に教えてくれ。
use std::collections::HashMap;
fn main() {
let capitals: HashMap<&str, &str> = HashMap::from([
("Japan", "Tokyo"),
("France", "Paris"),
("Russia", "Moscow"),
]);
println!("The capital of Japan is {}", capitals["Japan"]);
}
HashSet
)
ハッシュセット (重複しない要素の集合を管理するためのコレクション
要素の追加
use std::collections::HashSet;
fn main() {
// 空のHashSetを作成
let mut books: HashSet<&str> = HashSet::new();
// 要素を追加
books.insert("Rust Programming");
books.insert("The Pragmatic Programmer");
books.insert("The Pragmatic Programmer");
books.insert("The Pragmatic Programmer");
books.insert("The Pragmatic Programmer");
books.insert("Clean Code");
println!("{:?}", books); // 出力例: {"Rust Programming", "The Pragmatic Programmer", "Clean Code"}
}
存在確認と反復処理
use std::collections::HashSet;
fn main() {
let mut books: HashSet<&str> = HashSet::new();
books.insert("Rust Programming");
books.insert("The Pragmatic Programmer");
books.insert("Clean Code");
// 要素の存在確認
if books.contains("Rust Programming") {
println!("'Rust Programming' is in the collection.");
} else {
println!("'Rust Programming' is not in the collection.");
}
// 要素の反復処理
for book in &books {
println!("Book: {}", book);
}
}
要素の削除
use std::collections::HashSet;
fn main() {
let mut books: HashSet<&str> = HashSet::new();
books.insert("Rust Programming");
books.insert("The Pragmatic Programmer");
books.insert("Clean Code");
// 要素の削除
books.remove("Clean Code");
println!("{:?}", books); // 出力例: {"Rust Programming", "The Pragmatic Programmer"}
}
BTreeSet
HashSet 同様、重複のないコレクションだが、順序が自動でソートされているという違いがある。
基本的には HashSet と同じように使える。
use std::collections::BTreeSet;
fn main() {
let mut numbers: BTreeSet<i32> =
BTreeSet::from([1, 2, 2, 2, 3, 3, 4, 5, 6, 6, 7, 7, 7, 8, 9, 0, 0, 0]);
numbers.insert(100);
numbers.insert(50);
numbers.insert(70);
println!("{:?}", numbers); // {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 50, 70, 100}
}
B木構造で管理してるから、データを追加するだけでもうソートされた状態になってるし、検索も高速に行えるってことね。
ライフタイム
基本概念
ライフタイムは、参照(&Tや&mut T)が有効である期間を示すものです。Rustでは、メモリ安全性を保証するために、コンパイル時に参照のライフタイムをチェックします。これにより、ダングリングポインタ(無効なメモリ参照)やデータ競合を防ぎます。
なぜ必要なのか
Rustは所有権システムを使用してメモリ管理を行いますが、所有権だけでは参照が有効な期間を正確に管理できません。そこで、ライフタイムを導入することで、参照が有効な期間を明示的に指定し、コンパイラに安全性を保証させます。
ライフタイム注釈
関数や構造体でライフタイムを明示的に指定する必要がある場合があります。ライフタイム注釈は、参照が有効な期間をコンパイラに伝えるために使用されます。
fn function<'a>(x: &'a i32) -> &'a i32 {
x
}
-
'a
がライフタイム注釈 - 引数の参照と、返り値の参照はともに同じライフタイムを持つことを示している
- よって、引数と返り値が同じ期間有効であることが担保される
ただし、このコードの場合、ライフタイム注釈を省略することができる。なぜならコンパイラはそれぞれのライフタイムを推論できるため。
fn function(x: i32) -> i32 {
x
}
省略できないパターン
ライフタイム注釈が必要なケースは、主に以下のような状況です:
- 複数の参照を持つ関数:関数が複数の参照を受け取り、それらの関係を明確にする必要がある場合。
- 返り値が参照であり、参照元の寿命に依存する場合:返り値の参照が、入力参照の寿命に依存している場合。
以下のコードが該当するパターン
fn main() {
let string1 = String::from("abcd");
let string2 = "xyz";
let result = longest(string1.as_str(), string2);
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
2つの文字列スライスを受け取って、長い方の文字列スライスを返しています。
関数longestは、2つの参照xとyを受け取り、どちらか一方の参照を返します。しかし、返される参照がxなのかyなのか、あるいは両方に関連しているのかがコンパイラにとって不明確です。これにより、返り値の参照がどの参照のライフタイムに依存しているのかが判断できず、エラーが発生します。
よってライフタイムパラメータを利用する。
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
正直全然わからんな!!
弱音を吐いたら良い感じの回答が来たので一旦スキップしよう。
まずは、ライフタイムをあまり意識せずにRustのプログラミングを楽しんでみましょう。多くの場合、所有権と借用の基本を理解していれば、ライフタイムの複雑な部分に直面することなく、コードを書くことができます。
ライフタイムは所有権と借用の拡張です。まずは、所有権と借用の基本的なルールをしっかり理解しましょう。これらが理解できれば、ライフタイムの必要性や使い方も徐々に見えてきます。
ライフタイム注釈を避けるために、所有権を活用してデータの所有権を明確に管理する設計を選ぶことも一つの方法です。これにより、ライフタイムの複雑さを最小限に抑えることができます。
トレイト (Traits)
トレイトは、型に対して共通の機能や振る舞いを定義するためのものです。トレイトは、他の言語で言うところのインターフェースや抽象クラスに相当します。トレイトを使うことで、異なる型に対して共通のメソッドを実装させることができます。
トレイトの定義
trait Summary {
fn summarize(&self) -> String;
}
summarize
メソッドを持つっていう抽象クラスみたいなもの。
トレイトの実装
Tweet 型に Summary トレイトを実装する例
trait Summary {
fn summarize(&self) -> String;
}
struct Tweet {
username: String,
content: String,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn main() {
let tweet = Tweet {
username: String::from("sasaki"),
content: String::from("Hello, world!"),
};
println!("1 new tweet: {}", tweet.summarize());
}
トレイトのデフォルト実装
トレイト内でデフォルトの実装をすることで、それぞれの型で実装する必要がなくなる。
trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
この場合は実装を省略できる
impl Summary for Tweet {}
トレイト境界
関数の引数の型などで、特定のトレイトを実装している型に制限することができる。
fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
(後述の) ジェネリック構文でも書ける
fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
ライフタイムパラメータで心が折れかけたけど、また簡単に戻った。抽象クラスと具象クラスの関係ね。
ジェネリクス (Generics)
ジェネリクスは、関数や構造体、列挙型などで異なる型に対して同じコードを再利用できるようにするための仕組みです。ジェネリック型パラメータを使うことで、コードの汎用性が向上し、DRY(Don't Repeat Yourself)の原則を実現できます。
ジェネリック関数の定義
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
-
PartialOrd
は深くは言及しないけど、>
などの比較関数が定義されたトレイト - よって、比較可能な任意の型のコレクションを受け取って最大のアイテムを返す関数になる
ジェネリック構造体の定義
struct Point<T> {
x: T,
y: T,
}
fn main() {
let number_point: Point<i32> = Point { x: 1, y: 2 };
let float_point: Point<f64> = Point { x: 1.0, y: 2.0 };
println!("Number: x = {}, y = {}", number_point.x, number_point.y);
println!("Float: x = {}, y = {}", float_point.x, float_point.y);
}
属性 (Attributes)
#[derive(Debug)]
#[allow(dead_code)]
#[test]
のような、コードへのメタデータや指示を提供するためのもの。
#[attribute_name(parameters)]
の形式で記述する
derive 属性
型に対してトレイトを自動実装するための属性。
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 1, y: 2 };
println!("{:?}", p);
}
Debug トレイトは、構造体や列挙型の内容を簡単に出力するためのトレイトだが、derive 属性によって Point 型にこれを自動で実装されせることができる。
derive は複数のトレイトを指定可能。
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
allow 属性
コンパイラの警告を制御するために使用。以下は 属性x,y が未使用の場合の警告を抑制する。
#[allow(dead_code)]
struct Point {
x: i32,
y: i32,
}
test 属性
cargo test
によるテスト対象の関数をマークする。
// 通常の関数
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
// テストモジュール
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
}
$ cargo test
Compiling hello_world v0.1.0 (hello_world)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.14s
Running unittests src/main.rs (target/debug/deps/hello_world-983fe88bd01a5b9f)
running 1 test
test tests::test_add ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
めちゃくちゃ簡単にテストコード書けて実行できるんだ。
この感じだと Rust はテスト対象コードとテストコードを同じファイル内に並べて書いたりするのかな。
type (型エイリアス)
既存の型に別名を与えるための仕組み
type UserId = u64
TypeScript みたいに型そのものを作り出すわけでなく、単にエイリアスとして、可読性の向上や冗長性の削減に使用する。
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);