Rustの基本文法を振り返る
Rust
RustはMozillaのグレイドン・ホアレさんが開発し、Mozillaやコミュニティによって開発が進められている汎用プログラミング言語です。
Rustの特徴として高いパフォーマンスや、型システムや所有権による安全性、高い生産性が挙げられます。
Rustのインストールとツールチェーン
Rustは公式でインストールの方法がアナウンスされています。
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になる整数としてisize
とusize
があります。
i32
やu64
のような型を明示したリテラルを記述する方法もあります。
何も指定しない場合はデフォルトで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は文字列を扱うことができる型が複数存在しています。
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;
このnumber
は1u64
つまり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
のあとの()
に書かれているa
とb
が引数となります。こちらは仮引数です。
呼び出す側は以下のように記述します。
add_number(1, 2);
上記の場合、1
と2
が実引数となります。
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() {}
他の用法については以下をご覧ください
use
Javaなどでいうimport
はRustではuse
といいます。
たとえば、std::collections::HashMap
をHashMap
という名前で使う場合は以下のように記載します。
use std::collections::HashMap;
HashMap::new(); // ← このように単にHashMapとして使える
std::collections::HashMap::new(); // ← useを書かないと完全修飾名で書く必要がある
prelude
Rustのcrateを見ているとprelude
というモジュールが登場することがあります。
これは前奏曲という意味で、標準ライブラリの同様のモジュール名を真似たものです。
標準ライブラリのstd::prelude
は自動でuse
されるモジュールで、いちいちuse
しなくても使うことができるようになっています。
std::prelude
には、Result
やOption
、String
などよく使われる機能が含まれています。
自動で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}");
}
impl
とtrait
構造体や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;
}
このtrait
をimpl
することで、他のToString
を実装した型と同様にto_string()
を提供できます。
struct MyStructData;
impl ToString for MyStructData {
fn to_string(&self) -> String {
"MyStructData".to_string()
}
}
Copy
トレイト
Rustは値が基本的にムーブされると説明しました。しかし、プリミティブ型のような軽い値までいちいちムーブするのは面倒です。
そこで、コピー処理が重くないデータ型にはCopy
トレイトが実装されています。
Copy
はコンパイラにコピー可能であることを伝えるトレイトで、これ自体に実装が必要なメソッドはありません。
このように中身はないが特定の機能を提供できることを表すトレイトをマーカートレイトと呼びます。
マーカートレイト
マーカートレイトは暗黙的に実装されるものが多いです。
たとえばSized
というトレイトは値のサイズがコンパイル時に判明する場合につけられます。
Send
やSync
トレイトも暗黙的に実装されるトレイトです。
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
マクロを使っている部分です。
Clone
はClone
トレイト、Debug
はDebug
トレイト、Default
はDefault
トレイトのように、それぞれ実装されるトレイトの名前が使われます。
Clone
トレイトはvalue.clone()
のように値の複製ができるトレイトです。
Debug
トレイトはデバッグ表示ができるようになるトレイトです。
Default
トレイトはデフォルトの値を作成できます。型名::default()
のようなインスタンス化ができます。
非同期
Rustは非同期処理をサポートしています。
Rustの非同期処理は非同期ランタイムという機能を別途必要とします。
これは、非同期処理の根幹になる部分ですが、非同期処理の実装方法をカスタマイズできるようにあえて浮かせてあるようです。
有名な非同期ランタイムとしてtokio
やasync-std
などが挙げられます。
async
とawait
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
が使われることがあります。
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