⚙️

Rustの基本文法を振り返る

2023/08/19に公開

Rust

RustはMozillaのグレイドン・ホアレさんが開発し、Mozillaやコミュニティによって開発が進められている汎用プログラミング言語です。

Rustの特徴として高いパフォーマンスや、型システムや所有権による安全性、高い生産性が挙げられます。

Rustのインストールとツールチェーン

Rustは公式でインストールの方法がアナウンスされています。
https://www.rust-lang.org/ja/learn/get-started

RustはrustupというコマンドによってRustツールチェーンのインストールができます。
Rustツールチェーンにはコンパイラであるrustc、パッケージマネージャー兼ビルドツールであるcargo、LSP実装であるrust-analyzer、標準ライブラリなどがついてきます。

Rust開発ではrustcを直接使うことはほぼなく、cargoを介して実行ファイルやライブラリを作成することがほとんどです。

# プロジェクトをビルド
cargo build

# プロジェクトのリリースモードでビルド
cargo build --release

エディターやIDE

Rustには優秀なLSP実装がついてくるので対応しているエディターであれば大体開発できます。
Visual Studio CodeやIntelliJ IDEAはRust用の拡張機能やプラグインが提供されているので簡単に導入することが可能です。

用語の整理

Rustでの名前 これは何か 他の言語で言うと
cargo ビルドツール、パッケージマネージャー JSのnpm、JavaのMavenやGradle
crate ライブラリ、パッケージ JSのnpmパッケージ、JavaのMavenパッケージ
crates.io パッケージサイト JSのnpmjs.com、JavaのMavenCentral

整数型

Rustの整数型はi{ビット数}u{ビット数}の形式で名前が付けられています。
たとえば32bit幅でsignedの整数型はi32となります。
また、32bit CPUでは32bit、64bit CPUでは64bitになる整数としてisizeusizeがあります。

i32u64のような型を明示したリテラルを記述する方法もあります。
何も指定しない場合はデフォルトでi32として扱われます。

42i32 // i32型で42
42u64 // u64型で42
名前 大きさ signed
i8 8bit YES
i16 16bit YES
i32 32bit YES
i64 64bit YES
i128 128bit YES
isize アーキテクチャ依存 YES
u8 8bit NO
u16 16bit NO
u32 32bit NO
u64 64bit NO
u128 128bit NO
usize アーキテクチャ依存 NO

浮動小数点数型

浮動小数点数型も整数型と同様にf{ビット数}の形式で名前が付けられています。
単精度浮動小数点数型であれば32bitなのでf32型、倍精度浮動小数点数型であれば64bitなのでf64になります。浮動小数点数はIEEE 754にしたがい表現されます。

浮動小数点数型も整数型と同様に型を明示したリテラルを持っています。
何も指定しない場合はデフォルトでf64になります。

3.14f32
3.14f64

文字列型

Rustは文字列を扱うことができる型が複数存在しています。

https://doc.rust-lang.org/std/string/struct.String.html

std::string::StringはUTF-8でエンコードされ長さを変更することができる文字列型です。
C++でいうとstd::stringに当たる型で、他の一般的な言語で用いられるString型と大体同じものです。

&strはUTF-8でエンコードされた文字列への参照です。
C++でいうとstd::string_viewに当たる型です。
本当はstrへの参照ですが、strがそのまま出てくることはまれなので、std::string::Stringが持つ文字列への参照と説明します。

std::string::String
↓ この領域への所有権を持つ
+----+----+----+----+----+----+----+----+
| R  | u  | s  | t  | l  | a  | n  | g  |
+----+----+----+----+----+----+----+----+
↑この領域への所有権を持たず見るだけ
&str

配列とスライス

配列はCやC++、Javaの配列と同様に要素型と長さが決まっている型です。
長さはコンパイル時に決定している必要があり、伸ばしたり短くしたりすることはできません。

[i32; 500] // 要素数500で要素型がi32な配列

スライスは配列などの連続した領域への参照です。
Stringに対する&strの関係性に近いです。
スライスは配列の先頭がどこにあるかと、長さがどれくらいあるかを保持しています。

&[i32] // i32の配列へのスライス

可変長配列

要素の追加や削除など長さを自由に変えることができる配列としてVec<T>が用意されています。

let mut vec = Vec::new();
vec.push(1);
vec.push(2);

vec[0];

ユニット

ユニットとは要素が1つもないタプルのことです。
型としては()と記述し、値も()になります。
CやC++、Javaのvoid、KotlinのUnitと同様に値がないことを表します。

カスタム型

カスタム型は複数の値や型をまとめた扱うなどができる型です。

struct (構造体)

構造体は0以上の値をまとめて扱う場合などに便利な機能です。
Cスタイルの構造体やタプルスタイル、ユニットスタイルがあります。

// Cスタイルで色を管理する型
struct CStyleColor {
    red: u8,
    green: u8,
    blue: u8,
}

// タプルスタイルで色を管理する型
struct TupleStyleColor(u8, u8, u8);

// ユニットスタイルの構造体
struct UnitStyle;

enum (列挙型)

enumはいくつかの要素から1つを選ぶことができる型です。

enumの要素には構造体と同様にCスタイルの要素と、タプルスタイルの要素、ユニットスタイルが選べます。

enum Color {
    CStyleRgb { red: u8, green: u8, blue: u8 },
    TupleStyleRgb(u8, u8, u8),
    UnitStyleWhite,
}

std::result::Result<T, E>

Result<T, E>は成功と失敗を表すことができるenum型です。
Rustには例外がなく、代わりにこの値を返すことで成功失敗を伝えます。

enum Result<T, E> {
    Ok(T),
    Err(E),
}

成功時はOk(成功時の値)を使い、失敗時にはErr(エラーを表す値)を使います。

RustではResultが高頻度で登場するためエラー時の処理を簡単に記述するための糖衣構文が用意されています。

do_something()?; // 失敗する可能性がある処理do_somethingを呼び出す

上記の例の末尾についている?がその糖衣構文です。
この構文は?がついている式がErr側の値を持っている場合にその値をそのまま返すという構文です。
意味としては以下のコードとだいたい同じ動きをします。

if let Err(e) = do_something() {
    return Err(e);
}

std::option::Option<T>

Option<T>は値を持つことと持たないことを表すことができるenum型です。
値が存在しない可能性がある場合に利用できます。

enum Option<T> {
    None,
    Some(T),
}

値が存在する場合はSome(値)を使い、存在しない場合はNoneを使います。

タプル

タプルは複数の値をまとめて使うことができる型です。

(i32, String, f64)

変数

変数は値を入れる領域です。
Rustで変数を作る場合はletを使います。

let number = 1; // numberという名前の変数に1を代入

let number: i32 = 1; // 型を明示することもできる

Rustの変数はデフォルトで不変 (Immutable) なので再代入や入っている値を変更することができません。

let number = 1;
number = 2; // エラー: numberはimmutableなので値の再代入はできない

let vec = Vec::new();
vec.push(1); // エラー: vecはimmutableなので、vecが持つ値を変更できない

値の変更が必要な場合はmutをつけます。

let mut number = 1;
number = 2; // OK

let mut vec = Vec::new();
vec.push(1); // OK

型推論

Rustには非常に強力な型推論機能があります。

簡単な型推論の例を以下に示します。

let number = 1u64;

このnumber1u64つまりu64の値を代入しているので、u64と推論されます。
このレベルの型推論であれば、C++やKotlinにも存在しています。

Rustの型推論はその後にどのように使われているかからも推論してくれます。

let mut vec = Vec::new();
vec.push(1u64);

上記の例では1行目の時点でvecの型はVec<?>になります。つまり要素の型が不定になってしまいます。
Rustは2行目のpush(1u64)から要素型がu64であることを推論し、vecの型がVec<u64>であると判断します。

Rustはこの強力な型推論機能が存在するためあまり型を書くことなく記述できます。

制御構文

条件分岐

if

ifは処理の分岐に使われる制御構文です。

if value == 42 {
    // valueが42のとき
} else {
    // valueが42でないとき
}

if value1 == 42 {
    // value1が42のとき
} else if value2 == 500 {
    // value1が42でなく、value2が500のとき
} else {
    // value1が42でなく、value2が500でないとき
}

Rustのifは式なので、値をもたせることができます。

let v = if value1 == 42 {
    "value1 is 42".to_string()
} else if value2 == 500 {
    "value2 is 500".to_string()
} else {
    "other".to_string()
};

ifの値を持たせる場合は一番最後の式にセミコロンをつけません。

ifはパターンマッチングを行う機能も持っています。

let optional_value: Option<i32> = do_something();

if let Some(value) = optional_value {
    // optional_valueがSomeのときに実行される
}

パターンマッチングは後述するmatchで行われることが多いですが、ifで行うほうがきれいにかける場合もあります。

match

matchも処理の分岐に使われる制御構文です。パターンマッチングを行うことができます。

let value: i32 = do_something();

match value {
    1 => {
        // value == 1のとき
    }
    2 | 3 | 4 => {
        // value == 2か3か4のとき
    }
    _ => {
        // それ以外
    }
}
let optional_value: Option<i32> = do_something();

match optional_value {
    Some(value) => {
        // optional_valueがSomeのとき
    }
    None => {
        // optional_valueがNoneのとき
    }
}

繰り返し

loop

loopは無限ループを作るための構文です。

loop {
    // ここは無限ループ

    if break_loop_condition {
        break; // ループを抜ける
    }

    if continue_loop_condition {
        continue; // ここ以後の処理をスキップ
    }
}

基本的にはwhile trueと同じですが、条件の確認を省くことができるため、こちらのほうが高速に動作します。

while

whileは条件を満たす間繰り返す構文です。

let mut counter = 0;

while counter < 100 {
    counter += 1;
    // 100回繰り返す
}

for

forはイテレータを回すことができる構文です。

for n in 0..100 {
    // 0から99までの100回 回る。
}

let vec = vec![1, 1, 2, 3, 5, 8, 13, 21];
for v in vec {
    println!("{v}");
}

関数

関数は一連の操作をひとまとめにすることができる機能です。

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

関数は一番最後のを返り値として認識します。
最後のではありません。
上記の例をreturn文を用いて書き換えると以下のようになります。

fn add_number(a: i32, b: i32) -> i32 {
    return a + b;
}

引数

関数に渡す値を引数といいます。
受け取る側つまり関数の定義側で使われる引数を仮引数、呼び出す側が実際に渡す引数を実引数といいます。
引数は関数名のあとの()の中に書いていきます。

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

上記の例ではadd_numberのあとの()に書かれているabが引数となります。こちらは仮引数です。
呼び出す側は以下のように記述します。

add_number(1, 2);

上記の場合、12が実引数となります。

main関数

プログラムの開始地点となる関数です。

fn main() {
    // ここからプログラムが開始される
}

モジュールと可視性

Rustのモジュールはファイル単位やディレクトリ単位で勝手に作られます。
それ以外に作りたい場合は自分で明示的に作ることもできます。

pub

Rustの各識別子はとくに明示がない限り他のモジュールから見えないようになります。
つまりprivateな状態になります。
pubはその公開範囲を指定する機能です。
この機能は細かく見える範囲を指定できますが、簡単に3つ紹介します。

// どこからでも見える
pub fn public_function() {}

// 属するcrateから見える
pub(crate) fn crate_wide_public_function() {}

// 親モジュールから見える
pub(super) fn parent_module_wide_public_function() {}

他の用法については以下をご覧ください

https://doc.rust-jp.rs/rust-by-example-ja/mod/visibility.html

use

JavaなどでいうimportはRustではuseといいます。

たとえば、std::collections::HashMapHashMapという名前で使う場合は以下のように記載します。

use std::collections::HashMap;

HashMap::new(); // ← このように単にHashMapとして使える
std::collections::HashMap::new(); // ← useを書かないと完全修飾名で書く必要がある

prelude

Rustのcrateを見ているとpreludeというモジュールが登場することがあります。
これは前奏曲という意味で、標準ライブラリの同様のモジュール名を真似たものです。

標準ライブラリのstd::preludeは自動でuseされるモジュールで、いちいちuseしなくても使うことができるようになっています。
std::preludeには、ResultOptionStringなどよく使われる機能が含まれています。

自動でuseされるのはstd::preludeのみで、他のpreludeはあくまでそれを真似ただけのものです。
なので、明示的にuseする必要があります。

use std::prelude::*; // これは自動的に行われたことになっている

use std::io::prelude::*; // std::io系でよく使われる機能を使う。これは明示的にuseする必要がある

所有権とムーブ

Rustの非常に重要な概念として所有権があります。
所有権はその名の通りリソースを所有する権利ですが、この所有権は唯一つの変数のみが持つことができます。
所有権を持つ変数は、自身が破棄されるときに所有権を持つリソースを破棄する義務を負います。

所有権を持つ変数は唯一つなので別の変数でもそのリソースを使いたい場合は「所有権を移す」か「所有権を渡さず参照させる」のどちらかを選択します。

ムーブ

「所有権を移す」はムーブと呼ばれ、移したあと元になった変数へのアクセスはエラーになります。

let vec = vec![1, 1, 2, 3, 5, 8, 13, 21];
let v2 = vec; // ← この時点でvecはv2にムーブされる

for v in vec { // ← vecはムーブ後の値なのでエラーになる。
    println!("{v}");
}

若干難しいのは、この動作がデフォルトなので予期せずムーブしてしまうことがあることです。

fn move_value(value: Vec<i32>) {}

// move_valueを使う
let vec = vec![1, 1, 2, 3, 5, 8, 13, 21];
move_value(vec); // ← この時点でムーブされる
for v in vec { // vecはムーブ後なのでエラーになる
    println!("{v}");
}

この動作が正しい、つまり値をムーブすることが正しいこともあります。
ですが、そうでない場合は仮引数がムーブを要求する形になっていないかを確認しましょう。

借用 (参照) (borrow)

「所有権を渡さず参照させる」場合は値の所有権を移動させず値を渡す方法です。

fn reference_value(value: &Vec<i32>) {}

// reference_valueを使う
let vec = vec![1, 1, 2, 3, 5, 8, 13, 21];
reference_value(&vec); // ← vecへの参照を渡す
for v in vec { // vecはムーブされていないので、エラーにならない
    println!("{v}");
}

データを読み取り専用で参照するだけなら、これで問題ありません。
しかし、Vecに要素を追加するなど借りてきたデータを書き換えたいこともあると思います。
その場合は可変借用 (mutable borrow) を行います。
可変借用をする場合は引数を&mut 型とします。
そして渡す場合は&mutをつけます。

fn reference_value(value: &mut Vec<i32>) {
    value.push(42);
}

// reference_valueを使う
let mut vec = vec![1, 1, 2, 3, 5, 8, 13, 21];
reference_value(&mut vec); // ← vecへの可変参照を渡す
for v in vec { // vecはムーブされていないので、エラーにならない
    println!("{v}");
}

impltrait

構造体やenumにメソッドを追加したいことがあると思います。
implを使うことで構造体やenumにメソッドを追加できます。

struct SomeData;

impl SomeData {
    // Selfは自分を表す型。この場合はSomeData
    fn new() -> Self {
        SomeData
    }

    // &selfはインスタンスメソッド。レシーバーを参照として受け取る
    fn print(&self) {
        println!("SomeData print");
    }

    // &mut selfは可変なインスタンスメソッド
    fn mut_func(&mut self) {}

    // selfはインスタンスメソッド。レシーバーをムーブして受け取る
    fn moved(self) {}
}

// 使う
{
    let sd = SomeData::new();
    sd.print();
}
{
    let mut sd = SomeData::new();
    sd.mut_func();
}
{
    let sd = SomeData::new();
    sd.moved();
    // sd.print(); // エラーsd.moved()ですでにムーブされている
}

trait

トレイト (trait) は実装した型に共通した機能を提供できる機能です。
他の言語のインターフェイスに近い機能です。

例としてto_string()というメソッドを提供するtraitを以下に示します。

trait ToString {
    fn to_string(&self) -> String;
}

このtraitimplすることで、他のToStringを実装した型と同様にto_string()を提供できます。

struct MyStructData;

impl ToString for MyStructData {
    fn to_string(&self) -> String {
        "MyStructData".to_string()
    }
}

Copyトレイト

Rustは値が基本的にムーブされると説明しました。しかし、プリミティブ型のような軽い値までいちいちムーブするのは面倒です。
そこで、コピー処理が重くないデータ型にはCopyトレイトが実装されています。
Copyはコンパイラにコピー可能であることを伝えるトレイトで、これ自体に実装が必要なメソッドはありません。
このように中身はないが特定の機能を提供できることを表すトレイトをマーカートレイトと呼びます。

マーカートレイト

マーカートレイトは暗黙的に実装されるものが多いです。

たとえばSizedというトレイトは値のサイズがコンパイル時に判明する場合につけられます。
SendSyncトレイトも暗黙的に実装されるトレイトです。
Sendはスレッドをまたいで値を転送できる場合に実装されます。
Syncは複数スレッドから安全に参照を共有できる場合に実装されます。
これらのトレイトは要素型がそのトレイトを実装していない場合は実装されません。

Rustのコンパイラはこれらのマーカートレイトを見てスレッドをまたいでも大丈夫かを判断しています。

ジェネリクス

RustにはJavaやKotlinでいうジェネリクスや、C++のテンプレートに似た機能が提供されています。

ここまで出てきたジェネリクスを利用した型として、Result<T, E>Option<T>Vec<T>などがあります。
これらのように特定の型に特化するのではなく、汎化することでいろいろな方法で使うようにできる機能がジェネリクスです。

トレイト境界 (trait bound)

Rustのトレイトとジェネリクスには、「あるtraitを実装した型」のような記述ができます。

たとえば、ToStringを実装した型を引数に取る関数を作る場合は以下のように記述します。

fn print_to_string<S: ToString>(value: &S) {
    println!("{}", value.to_string());
}

複数の条件を書く場合はwhereを使って記述する場合もあります。

// ToStringとDisplayが実装されている型Sの値を引数に取る
fn print_to_string<S>(value: &S) where S: ToString + std::fmt::Display {
    // ...
}

// 上と同じ
fn print_to_string<S>(value: &S)
where
    S: ToString,
    S: std::fmt::Display {
    // ...
}

マクロ

Rustのマクロは文字列の置き換えではなく、抽象構文木を直接書き換えることで実現されます。
Rustのマクロは使うときに末尾に!が付きます。
!が一番うしろについていたらマクロによって作られた機能ということです。

よく使われるマクロ

Rustには標準で提供されるマクロがいくつか存在しています。

format!

フォーマットを指定してデータをStringに変換します。

let value = 1;
format!("{}", value); // valueを表示
format!("{value}"); // 変数のみであればフォーマット文字列の中に直接書くこともできます。

独自の型をformat!で使用するにはstd::fmt::Displayトレイトを実装する必要があります。

println!

format!と同様のフォーマットで標準出力に出力します。

vec!

Vec<T>を配列のリテラルのような形式で初期化できます。

let v = vec![1, 2, 3];

deriveマクロ

他の便利なマクロとしてderiveマクロがあります。
deriveマクロは独自型にトレイトを簡単に実装できる方法です。

#[derive(Clone, Debug, Default)]
struct MyStruct;

この#[derive(Clone, Debug, Default)]の部分がderiveマクロを使っている部分です。
CloneCloneトレイト、DebugDebugトレイト、DefaultDefaultトレイトのように、それぞれ実装されるトレイトの名前が使われます。

Cloneトレイトはvalue.clone()のように値の複製ができるトレイトです。
Debugトレイトはデバッグ表示ができるようになるトレイトです。
Defaultトレイトはデフォルトの値を作成できます。型名::default()のようなインスタンス化ができます。

非同期

Rustは非同期処理をサポートしています。
Rustの非同期処理は非同期ランタイムという機能を別途必要とします。
これは、非同期処理の根幹になる部分ですが、非同期処理の実装方法をカスタマイズできるようにあえて浮かせてあるようです。
有名な非同期ランタイムとしてtokioasync-stdなどが挙げられます。

asyncawait

asyncは非同期処理をawaitするため非同期処理を行う関数につけたり、ブロックにつけたりする予約語です。

// 非同期実行する関数
async fn asynchronous_function() {
    other_async().await;
}

// 非同期実行するブロック
async {
    other_async().await;
}

Futureトレイト

Futureトレイトはawaitできる値を表すトレイトです。
async関数やasyncブロックはFutureトレイトを実装した値を返します。
一般的にawaitしない限り実行は開始されません。

async-trait

2023/08/19現在、Rustにはasync関数を含むトレイトを作ることができません。
そのためそれを解決するcrateとしてasync-traitが使われることがあります。

https://crates.io/crates/async-trait

cargoとCargo.toml

前述したようにcargoはパッケージマネージャー兼ビルドツールです。
その設定はCargo.tomlに記述します。

[package]
name = "my_command"
version = "0.1.0"
edition = "2021"

[dependencies]
# シリアライズとデシリアライズを提供するcrate
serde = "1.0.183"

# 非同期ランタイム
# Cargo.tomlのdependenciesはfeaturesに指定した機能を有効にできます
tokio = { version = "1.32.0", features = ["full"] }

Discussion