📖

Rust入門の勉強会で使った資料を公開しました

2022/12/17に公開

Rust超入門勉強会

TechCommit Advent Calender 2022の24日目の記事です。

2022年10月,とあるエンジニアコミュニティでRust入門の勉強会をしました。
2021年の末にRust学習を本格的に始めて,学習したことを振り返る良い機会となりました。
極力分かりやすく説明するように努めていますが,読んでも不明点がある場合公式ドキュメントや技術書の参照をお願い致します。


目次

  1. 勉強会の概要説明
    1. 目的
    2. 学ぶメリット(この勉強会のメリット)
    3. 説明すること/説明しないこと
    4. 勉強会資料
  2. 私がRust学習を始めた理由
  3. Rustとは?
    1. 特徴
    2. 言語設計思想
    3. Rust採用事例
  4. Rustを使う上で知っておくべき知識
    1. 可変(ミュータブル)と不変(イミュータブル)
    2. メモリ(スタックとヒープ)
  5. 開発環境構築
  6. 動かしてみよう!(「ハローワールド!」を出してみよう)
  7. 基本文法
    1. 変数
    2. 制御(if, loop, for, match)
    3. Enum(列挙体)
    4. 関数
    5. 所有権
  8. 演習
  9. CLIツールを作ってみよう
  10. 最後に
  11. 備考

1. 勉強会の概要

  1. この勉強会の目的(ねらい・勉強会で得られること)
  2. 説明すること
  3. 説明しないこと

1-1. 勉強会の目的

  • Rust学習の一歩目を踏み出す
  • Rustの特徴や採用事例を知る
  • Rustの基本的な文法を学ぶ

1-2. この勉強会のメリット

  • Rustの実行環境が手に入る
  • 簡単な文法だけで、CLIツールを作れる

1-3. 説明すること/説明しないこと

  1. 説明すること
    1. Rustの基本文法(変数・条件制御・関数)
    2. 列挙体
    3. 所有権
  2. 説明しないこと
    1. 構造体
    2. トレイト
    3. ジェネリクス

1-4. 勉強会資料

サンプルプログラムです。

https://github.com/Yuki2Kisaragi/rust_hands_on


2. 私がRust学習を始めた理由

  • 2021年初頭にとある技術書を本屋で見つけて、Rust言語の存在を知る。
  • 軽く調べてRustの魅力を知る
    • GUI・CLI・Webに使える汎用性(採用事例の多さ)
    • 組み込みで使えることに興味を持つ
    • C/C++でよく起こるメモリの失敗をRustでは解決すること
  • 当時、受託IoTシステム開発プロジェクトで疲弊していたこともあり、組込みRust開発で楽になりたいと思い始める


3. Rustとは?

3-1. 特徴

  • 静的型付け言語
  • 2015年に安定板のリリース(Ver1.00)
  • 開発元はFireFoxでお馴染みのMozilla
  • 「最も愛されている言語」 6年連続一位

Rust公式サイトより抜粋


3-2. 言語設計思想

  • パフォーマンス速度
    • 静的型付け言語
    • ガベージコレクタがない、軽量ランタイム
  • 安全性
    • C/C++で頻発するメモリ問題をコンパイラで未然に防ぐ
    • メモリ安全性とスレッド安全性を保証する

3-3. Rustの採用事例

参考文献:Rustの本番での採用事例リンク集


4. Rustを使う上で知っておくべき知識

  1. 可変(ミュータブル)と不変(イミュータブル)
  2. メモリ(スタックとヒープ)

4-1. 可変(ミュータブル)と不変(イミュータブル)

Rustは、変数の定義時に値を変更をできる/できないかを決めます。

  • 可変 ・・・ mutable(ミュータブル)
  • 不変 ・・・ immutable(イミュータブル)

Rustは、原則として、イミュータブルで変数が定義されます。

let var1 = 1;     // immutable = 定義したら値変更できない!
let mut var2 = 1; // mutable = 定義した後でも変更できる!

var2 = 100; // OK
var1 = 2;   //  NG! コンパイルエラー

4-2. メモリ(スタックとヒープ)

Rustでは、そのデータがどのメモリで使われているか把握しながら、プログラミングする必要がある。
値が同じのように見えて、メモリ確保方法や可変/不変が異なる!

例1. 同じ文章だけど…!

データ(文字)のメモリ確保方法が異なります。

let s1 = "Hello World!!";                  
let s2 = String::from("Hello World!!");     

例2. 同じ可変型の配列のように見えて・・・!

変更できるものが異なります。

let mut array_a = [1,2,3,4];     // 要素の追加・削除ができない!
let mut array_b = vec![1,2,3,4]; // 要素の追加・削除ができる!

Rustは、コンパイル時に変数のメモリ確保範囲を指定しなければなりません。

  • データサイズがコンパイル時に決定されて、実行後もデータサイズが保証するものか?
  • データサイズが実行中に変動するか

上記の違いは、メモリ確保先がスタックメモリとヒープメモリの違いによるものです。

スタックとヒープの違い

スタック

スタックメモリは、順番にデータを格納する

  • アクセスが高速
  • サイズが小さい

ヒープ

ポインタを利用し、データの確保/解放を行う

  • サイズが大きい
  • アクセスがスタックより低速

参考


5. 動かしてみよう!("Hello World!"を出してみよう)

Rustのインストールが完了したら、Rustのプログラムを動かしてみよう!
(備考1 参照)

rustup --version # rustup: Rustのツールチェインマネージャー 
rustc --version  # rustc : Rustのコンパイラ 
cargo --version  # cargo : Rustのパッケージマネージャー
$ rustup --version
rustup 1.24.3 (ce5817a94 2021-05-31)
info: This is the version for the rustup toolchain manager, not the rustc compiler.
info: The currently active `rustc` version is `rustc 1.58.1 (db9d1b20b 2022-01-20)`

$ rustc --version
rustc 1.58.1 (db9d1b20b 2022-01-20)

$ cargo --version
cargo 1.58.0 (f01b232bc 2022-01-19)

Rustを動かしてみよう‼

# CargoでRustプロジェクト(ファイル群)を作成する
$ cargo new hello
     Created binary (application) `hello` package

# `hello/`が作られる
$ ll
hello/

# パス移動
$ cd hello/

# `hello`フォルダの中身
# `src/`にソースファイルが入っている 
$ ll
Cargo.toml
src/

# プロジェクト生成時に自動でメイン関数が作られる
$ cat src/main.rs 
fn main() {
    println!("Hello, world!");
}

# `cargo run`でビルド&実行!!
$ cargo run
   Compiling hello v0.1.0 (C:\Users\MasaH\Desktop\Engineering\RustLearning_TechCommit\LT_Slide_Sheet\hello)
    Finished dev [unoptimized + debuginfo] target(s) in 1.61s
     Running `target\debug\hello.exe`
Hello, world!

6. 基本文法

  1. 変数
    1. 変数の定義
      1. 不変型変数に変数を代入する
      2. 型の一覧(整数・浮動小数点・真理値型)
    2. 文字列型
    3. String型
  2. 関数
  3. 条件制御(if, loop, for, match)
  4. Enum(列挙体)
  5. 所有権
    1. 所有権とは
    2. ムーブセマンティクス
    3. コピーセマンティクス
    4. 関数と所有権
    5. 参照

1. 変数

Rustにおける変数宣言を学んでみましょう!

変数の定義

let x = 10;
let y = 20;
println!("x = {}", x);
println!("y = {}", y);
println!("x + y = {}", x + y );

変数の定義はlet x = ...を使って行います。


不変型変数に変数を代入する

次に、Rust始めたての方が陥りやすい、コンパイル失敗する書き方を説明します。

一見何も問題ないように見えますが…コンパイル失敗します。

let x = 10;
println!("x = {}", x);
x = 100;
println!("x = {}", x);
$ cargo run
error[E0384]: cannot assign twice to immutable variable `x`
 --> src\main.rs:3:5
  |
2 |     let x = 10;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     x = 100;
  |     ^^^^^^^ cannot assign twice to immutable variable

For more information about this error, try `rustc --explain E0384`.

コンパイラ(rustc)が、どこでどのようなエラーが発生したか教えてくれて、更にエラー解決方法まで提案してくれます。

error[E0384]: cannot assign twice to immutable variable `x`

E0384というエラーで、「イミュータブルな変数に二回書き込もうとしています」と教えてくれます。

2行目でxが最初に定義(初期化)されて、3行目で再び定義(代入)していますね。

help: consider making this binding mutable: `mut x`

help:でエラー解決方法をコンパイラが提案してくれます。
(なんて親切なんでしょうか‼ 私はこのコンパイラの親切さに惚れました‼)

mutをつけてミュータブルとして変数宣言してくださいと教えてくれます。

Rustでは、ミュータブル(可変)の変数として定義するために必ずmutをつける必要があります。

mutをつけなければ、原則としてイミュータブル(不変)な変数として定義されます。

それでは、コンパイラのアドバイスに従って、コードを修正しましょう。

let mut x = 10;
println!("x = {}", x);
x = 100;
println!("x = {}", x);

無事コンパイルされると思います。


ミュータブル(可変)の変数

mutをつけて可変変数を定義できます。

sample_var0()

let mut x = 1;
println!("x = {}", x); // x = 1
x = 10;
println!("x = {}", x); // x = 11
x += 9;                // x = x + 9 と同義
println!("x = {}", x); // x = 20

データ型一覧

Rustは明示的に型を指定しなくても、型推論をしてくれます。

整数型

型名 詳細
u8 8bit 符号なし整数
i8 8bit 符号あり整数
u16 16bit 符号なし整数
i16 16bit 符号あり整数
u32 32bit 符号なし整数
i32 32bit 符号あり整数
u64 64bit 符号なし整数
i64 64bit 符号あり整数
u128 128bit 符号なし整数
i128 128bit 符号あり整数
usize アドレス幅 符号なし整数
isize アドレス幅 符号あり整数

isizeusizeはアーキテクチャによって幅が決まります。

例)32bitだったら32bit幅になる。

sample_var1()

let n1 = 10_000;     // n1 = 10000, Defaultだとi32で定義される
let n2 = 0u8;        // u8
let n3 = -100_isize; // isize型
let x4:i128 = 10;    // i128型
let x5 = 250;        // i32

実数型

型名 詳細
f32 32bit 浮動小数点数
f64 64bit 浮動小数点数
let f1 = 10.1;    // Defaultだとf64で定義される
let f2 = 30.4f32; // f32型で定義

真理値型

型名 詳細
bool true/false
let b1 = true;
let b2 = false;

文章を扱う型

Rustで文章を表現する型は2種類あります。
似たような値でも扱いが異なるので注意してください。

文字列型 str

strは、固定サイズで変更不可能な型です。

let s = "hello world!";
println!("{}",s);

String型

String型はstrと違い、可変サイズで、変更・追加・削除が可能です。

sample_var2()

let s1 = "Hello world!".to_string();
let s2 = String::from("Good evening!");
let str_1 = "ハローワールド";           // str型で定義
let s3 = str_1.to_string();            // str型からString型を作る

println!("s1: {}",s1);
println!("s2: {}",s2);
println!("str_1: {}",str_1);
println!("s3: {}",s3);

文章の合体

sample_var3()

let name = "MasaHero";                      // str型
let mut greeting = String::from("My name is "); //String型
greeting.push_str(name);
println!("{}",greeting);

2. 関数

関数の定義

引数・戻り値がない関数

fn hello_print(){
   println!("Hello world!");
}

fn main(){
   hello_print();
}

引数・戻り値がある関数

Rustでは、関数定義時に引数と戻り値の型を必ず指定する必要があります。

fn add_i32(a:i32,b:i32) -> i32 {
   a + b
}

let a:i32 = 100;
let b:i32 = 500;

let c = add_i32(a,b);
println!("a + b = {}",c);

また、戻り値として値を返したい場合、return;を使わないで戻り値のみを記載してください。

add_i32()でいうと a+b


3. 条件制御(if, loop, for, match)

条件・制御の構文はいくつかありますが、今回は限定して紹介します。

if

テンプレート

if <condition> {

} else if <condition2>{

} else {

}

サンプル

let a = 100;

if a > 10 {
      println!("greater than 10");
} else if a == 10 {
      println!("equal");
} else {
      println!("rather than 10");
}

サンプル2

if文を使って、戻り値を返すこともできます。

サンプル1と同様な処理をサンプル2として作ります。

このときに、if文の後の;をつけること、各条件時の戻り値の型を同じにすることに注意してください。

sample_if_2()

let a = 10;

let result = if a > 10 {
      "greater than 10"
} else if a == 10 {
      "equal"
} else {
      "rather than 10"
};

println!("result : {}",result);


loop

while true文のように常にループする処理です。

テンプレート

loop {
   // hoge
}

サンプル1

sample_loop1()

let mut i = 0;
loop {
   println!("hoge: {}", i);
   i += 1;
   if i > 10 {
      break;
   }
}

サンプル2

sample_loop2()

let mut i = 0;
loop {
   println!("hoge: {}", i);
   i += 1;
   if i > 10 {
      break;
   }else{
      continue;
   }
}

サンプル3

loopを抜けるときに値を返すこともできます。

sample_loop3()

let mut counter = 0;

let ten = loop {
   if counter == 10 {
      break counter;
   }
   counter += 1;
};

println!("counter = {}", ten);

for

Pythonのfor文に近いです。

sample_for_1()

let array = [1, 2, 3, 4, 5];

for e in array {
   println!("{}",e);
}

let str_vec = vec!["a", "i", "u", "e", "o"];
for e in str_vec {
   println!("{}",e);
}

match

C/C++のswitch/case文に似たようなものですが、結構奥深い構文です。
(今回は基本的な機能の一部のみを紹介します)

テンプレート

match <target> {
   pattern1 => expression1,
   pattern2 => expression2,
   pattern3 => expression3,
}

サンプル1

sample_match_1()

let value = 100;

match value {
   1 => println!("one"),
   10 => println!("ten"),
   100 => println!("one hundred"),
   _ => println!("something"),     // 1,10,100以外の数字
}

サンプル2

match文は値を返すこともできます。

sample_match_2()

let value = 100;
let result = match value {
      1 => "one",
      10 => "ten",
      100 => "one hundred",
      _ => "something",
};
println!("result: {}", result);

サンプル3

乱数生成とmatchを使って0~10までの数の判別をしてみましょう。

3~4,5~8のように、複数パターンの表現もできます。

sample_match_3()

use rand::Rng;
 #[allow(dead_code)]
fn sample_match_3() {
    
    // 0~10までの乱数を作る
    let num: i32 = rand::thread_rng().gen_range(0..11);
    
    let str_number = match num {
        0 => "zero",
        1 => "one",
        2 => "two",
        3 | 4 => "three or four",
        5..=8 => "something",
        9 => "nine",
        _ => "greater than 9",
    };
    
    println!("str_number = {str_number}");
}


4. Enum(列挙体)

C/C++の列挙体に近いものですが、これも自由度が高い構文です。
(今回は基本的な機能の一部のみを紹介します)

テンプレート

enum Weekdays { // 列挙体名は大文字にする
   Monday, // 大文字にする 
   Tuesday, 
   Wednesday,
   Thursday,
   Friday,
   Saturday,
   Sunday,
}
enum Role {
   Developper = 1, // 定義時に整数値を与えられなかった場合、原則0が入る
   ProductMangager,
   ProjectManager,
}

サンプル1

Enumは基本的に、関数の外で定義します。

enum Country {
   Japan,
   USA,
   UK,
}

fn main(){
   // Country型Enumを代入する
   let country = Country::USA;
}

サンプル2

match文と合わせることもできます。

sample_enum_2()

enum Country {
   Japan,
   USA,
   UK,
}

let country = Country::Japan;
let cont = match country {
   Country::Japan => {
      println!("日本です");
      "日本"
   }
   Country::USA => {
      println!("米国です");
      "米国"
   }
   Country::UK => {
      println!("英国です");
      "英国"
   }
};
println!("国 : {}", cont);

5. 所有権

ここからRustの根幹の技術とも言える、所有権を学んでいきます。

所有権とは

  • 所持しているメモリを開放する権利/責任

次に所有権のねらい、他メモリ管理の手法の説明を経てRustの概要を学習しましょう!

所有権のねらい

所有権が解決したかったもの、それは「メモリの確保と解放」を安全・高速に達成するためです。

どのプログラミング言語も総じて、変数や関数などを定義する際に必ずメモリに適当なサイズの空間を確保し、使わなくなったら確保空間を開放するようになっています。

次に、メモリの確保/解放の手法2つについて軽くおさらいします。


「メモリの確保/解放」の手法おさらい

動的メモリ確保

C/C++のような言語では、プログラマがメモリの確保と解放を管理します。

プログラマが自由に確保サイズを指定し確保して、任意のタイミングで解放する手法です。

メリット
  • 必要最低限のメモリ量でプログラムを実装できる
    • (余計なメモリ確保が抑えられる)
デメリット
  • メモリ管理を人の手で行う必要があり大変(バグが混入してしまう恐れ)
    • 解放忘れ、二重開放、メモリ未初期化、無効なポインタを参照、メモリオーバーフローなど,etc...

ガベージコレクション

Python,Ruby,Goなどのポインタを採用していない、言語ではガベージコレクションを使っています。
こちらは使われなくなったメモリを自動で検出し解放するものです。

メリット
  • メモリ管理を自動で行ってくれる
    • 動的メモリ管理で陥りがちなバグを回避できる
  • プログラム実装が楽になる
デメリット
  • メモリ監視をする必要があるので、応答性・メモリ使用量などの性能に影響を及ぼす可能性

所有権のメリット

  • ガベージコレクションが不要になり、動作が軽量化される
  • コンパイル時にメモリ安全性が保証される
    • メモリ二重開放・ダングリングポインタを防ぐ
  • メモリ以外にも、ファイルディスクリプタやMutexロックも自動開放される

所有権のルール

  • Rustの各値は、所有者と呼ばれる変数と対応している (所有者=変数のこと)
  • いかなる時も所有者は一つ
  • 所有者がスコープから外れたら、値は破棄される

(参考:Rust公式ドキュメント)


所有権とスコープ

ガベージコレクションがある言語と同様に、スコープを抜けると値は破棄されます。

lesson1_scope_and_drop()

   {
      let a = 10;
      println!("a = {}", a); // a = 100
                              // ここでaは破棄される
   }
   // aは破棄されているのでコンパイルエラーとなる
   println!("a = {}", a);

ここまでは、なんとなく理解できると思います。


コピーセマンティクスとムーブセマンティクス

Rustは、データを格納しているメモリ(スタック/ヒープ)によってプログラムの意味合い(セマンティクス)が変化します。

例えば、下記のようなコードでもxに入るデータの型によって、let y = x ;の処理が違います。
(スタックメモリ・ヒープメモリについては、4-2章を参照)

let x = <データ> ;
let y = x;

ムーブセマンティクス

xに入るデータの型がString型などのヒープメモリ領域だった場合,所有権がxからyへ移動します。(値が移動する)

lesson2_move_semantics()

let x = String::from("hello");
println!("x = {}", x);
let y = x;
// xからyへ所有権(値)が移動したためコンパイルエラーとなる
println!("x:{}", x);
コンパイルエラー内容
error[E0382]: borrow of moved value: `x`
   --> src\main.rs:146:22
    |
142 |     let x = String::from("hello");
    |         - move occurs because `x` has type `String`, which does not implement the `Copy` trait
143 |     println!("x = {}", x);
144 |     let y = x;
    |             - value moved here
145 |     // xからyへ所有権()が移動したためコンパイルエラーとなる
146 |     println!("x:{}", x);
    |                      ^ value borrowed here after move

図1


図4

xの中身がyにコピーされるのではなく、helloが格納されているメモリへのアクセスがyだけになります。xのアクセスは無効となります。


なぜコピーされないのか?

図3

もしxhelloよりもとても大きい長さのデータだった場合、ヒープメモリを圧迫し実行性能が悪化してしまうため、原則ではヒープメモリ上のコピーは暗黙的には行わないようになっています。


なぜxyで同じメモリを参照できないのか?

図2

もしxyの2つでヒープメモリ上のhelloを参照した場合、xyがスコープから抜けるときメモリの二重開放となり、未定義動作を引き起こします。

そこで、ムーブセマンティクスを使うことでhelloを参照するのはyだけとなるので、スコープを抜ける時も安全にメモリ開放ができます。

let x = String::from("hello");
let y = x; // moveが発生する

コピーセマンティクス

スタックのみのデータの場合は、ムーブは発生せず値がコピーされます。
コンパイル時にサイズが予め分かっているので、スタックメモリ上にコピーされます。

let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

スタックメモリを使う型は、下記の通りです。

  • 整数型(i8,u32)
  • 真理値型(true/false)
  • 浮動小数点型(f32)
  • 文字型(char)
  • 配列やタプル

など

(※正確には、コピーセマンティクスが発生するのはCopyトレイトを実装している型なのですが、今回は省略します。)


所有権と関数

関数の受け渡しも所有権の移動が発生します。
(変数の代入と似たような動きです。)

関数を抜けるときもスコープを抜けることと同じなので、値が破棄されます。

lesson3_ownership_and_func()

fn move_ownership(str1: String) { // String型なのでムーブセマンティクス。
    println!("{}", str1);
}

fn copy_data(some_integer: i32) { // i32型なのでコピーセマンティクス。
    println!("{}", some_integer);
} 

fn lesson3_ownership_and_func() {
    let s = String::from("hello");  

    move_ownership(s);   // sが関数に入り、所有権の移動が発生する。sは破棄される。

    let x = 5;                      

    copy_data(x);        // xも関数に入るが、コピーなので`x`は再度使用可能!

}

参照

関数に所有権を渡さず、値のみを渡したい場合に参照(借用)が役立ちます。

先ほどの関数move_ownership()を基にして、所有権が移動しない関数を参照を使って書いてみます。

関数の引数の型宣言と関数の実引数の変数に&ついていることに注目してください。

lesson4_reference()

fn print_value(str1: &String) {
    println!("{}", str1);
}

fn lesson4_reference() {
    let s = String::from("hello");

    print_value(&s); // 所有権は移動されない。

    println!("{}", s); // sはそのまま使える
}

&を付けることにより、所有権を渡すことなく値を参照することができます。

サンプル1

fn calculate_length(s: &String) -> usize {
   // Stringの長さを返す関数
   s.len()
}

fn main() {
   let s1 = String::from("hello");
   let len = calculate_length(&s1);
}

可変参照

イミュータブルの変数の参照は、当然変更できません。

let s1 = String::from("hello");
let s2 = &s1;
s2.push_str(" world"); // コンパイルエラー

lesson5_mutable_reference()

&mutをつけると可変参照となり、参照先から変更できるようになります。

let mut s1 = String::from("hello");
let s2 = &mut s1;
s2.push_str(" world"); // コンパイルできる

可変参照は、1つのデータに対して1つまでの制約があります。
下記のようなコードはコンパイルエラーとなります。

let mut s1 = String::from("hello");
let s2 = &mut s1;
let s3 = &mut s1;

7. 演習(宿題)

CLIでじゃんけんゲームを作ってみよう!

https://github.com/Yuki2Kisaragi/rusty-janken


8. 最後に

俺たちはようやく登り始めたばかりだからな…!
このはてしなく遠いRust坂をよ・・・!


備考

1. 開発環境構築方法

Linux,Mac

WSL(Ubuntu 20.04)でも動作確認してます

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Windows

ワンライナーでインストールできないので、公式サイトをご覧ください…!

Rust公式インストールガイド URL


2. Rust書籍


3. Web上のRust学習資料


参考文献

  • 実践Rust入門
  • Rust公式サイト
    • The Book
    • Rust By Example

Discussion