Rust
Rust とは?
- Mozilla が支援
- Android OS や Linux カーネルでも使われている
- C/C++/Java/C#のいいとこ取り
- ネイティブコンパイラ言語 (ダイレクトに機械語に変換できる)
- メモリ安全
- スレッドセーフ
- ガベージコレクタが不要
- 自動テスト機能がある
- モジュールシステムが整っている
- プロジェクトを階層化、分割して管理する仕組み
インストールと更新
- インストール
- 公式サイトのスクリプトを実行する
- 更新
rustup update
- フォルダ構成
$HOME/.rustup
- Rust のアップデート用フォルダで、メタデータやツールチェーン(開発に必要なプログラム群)が置かれる
$HOME/.cargo
- パッケージマネジャーのフォルダ
- Rust のコマンド類は bin フォルダに配置される
- Rust のプログラム
- rustup
- ツールチェーンのインストールやアップデートを管理
- rustc
- コンパイラ
- Rust の中核
- 直接使うことは少ない
- cargo
- パッケージマネジャーである
- 外部ライブラリ(crates)のダウンロード、アップデート、バージョン管理
- crates.io での自作の crate を共有
- ビルドツールでもある
- コードのビルド
- コードのテスト
- ドキュメントの作成
- パッケージマネジャーである
- rustup
Hello World
# 新規プロジェクトの作成
cargo new --bin hello_world
cd hello_world
# ビルドと実行(デバッグ用)
cargo run
# ビルドと実行(リリース用)
cargo run --release
- ビルド成果は
target/(debug|release)
フォルダに出力される- ここでは
hello_world
というバイナリが生成されている
- ここでは
コメント
//
と/* */
が使える。///
を使うとドキュメンテーションコメントになる- マークダウンで書いたコメントが HTML ドキュメントになる
Cargo
cargo は以下の全てをこなす、何でもできるひと。
- Package manager (like npm)
- Build system (like
make
) - Test runner
- Docs generator
変数と可変性
Rust では変数はデフォルトで不変である。これは、安全、並列性、スピードのためである。
- 変数
- 不変
let x=5
- 可変
let mut x=5
- 命名規則は小文字のスネークケース
- rust では「値 5 を x に拘束する」という表現をする
- 不変
- 定数
const MAX_POINTS: u32 = 100_000
- 命名規則は大文字のスネークケース
- 型注釈は必須
- コンパイル時に値が定まる式(定数式)のみをセットできる
- 一方で immutable な変数はあらゆる式(e.g. 関数の戻り値など)をセットできる
シャドーイング
- 前に定義した変数と同じ名前の変数を新しく宣言して上書きすること
- ブロック内で使用した場合はブロック内でのみ有効
- シャドーイングは型を変更できるという点で、
mut
とは異なる。 - うまく使うとコードを可読性を向上させたり、凡ミスの可能性を減らしたりできる
let v = 1;
{
let v = 2;
// v === 2
}
// v === 1
let v = 3;
// v === 3
データ型
大きくわけてスカラー型と複合型(Tuple, Array)がある。
スカラー型
スカラー型には基準型がある。基準型というのは、型注釈のない変数宣言などで、優先して選択されるデータ型のこと。
- 整数
i8
,u8
,i16
,u16
,i32
,u32
,i64
,u64
,isize
,usize
- 基準型は
i32
usize
はプラットフォームに依存する。プロセスで使用するメモリアドレスをどこでも表せる。isize
もプラットフォームに依存する。Array の上限数と等しい。メモリアドレス間の差分を表すのに使ったりする。- 一部しかサポートしないプラットフォームもあるので注意
- 浮動小数点数
f32
(単精度浮動小数点数),f64
(倍精度浮動小数点数)- 基準型は
f64
- 一部のプラットフォームでは f64 はマジで遅くなるので注意
- 論理値型
- 文字型
char
型- シングルクオートで表す
- Unicode Scalar Value である
U+****
の****
の部分- 最大 21-bit なので、キャストするときはそれ以上の型でないと表現しきれない点に注意
- https://lets-emoji.com/emojilist/emojilist-1/
- よって世間一般的な「文字」とは離れた性質のものも含まれる(e.g. ゼロ幅スペース)
- ほとんどの場所では UTF-8(≒ 文字列)を使うので、あまり使うことはない
Tuple
アリティ(Tuple の要素数)は最大で 12 までで、それを超えると機能が制限される。
let tup: (i32, f64, u8) = (500, 6.4, 1);
// or
let tup = (500, 6.4, 1);
// 分配と呼ばれる代入方法
let (x, y, z) = tup;
// 0番目の要素にアクセス
let five_hundred = x.0;
Array
要素数は最大で 32 までで、それを超えると機能が制限される。
let a = [1, 2, 3, 4, 5];
let a = [123, 3]; // 123を3つ
// 添字を使ったアクセス
let first = a[0];
let second = a[1];
// 型を指定する方法
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b: &[i32] = &a;
- 一度宣言した Array の要素数を変更することはできない
- 可変長が必要な場合はコレクションライブラリ(e.g. Vector 型)を使う必要がある
- Array の終端を超えたアクセスはパニックになる
型推論
複数の型が推論され る可能性がある場合、型注釈が必須。
let guess: u32 = "42".parse().expect("Not a number!");
リテラル
リテラルとは「見たままのもの」、つまり数値や文字などの値そのもののこと。
- 数値リテラル
123
12345u32
末尾に型をつけることもできる12_345_u32
アンダースコアで区切ることもできる0xffff
1.23
など
- 文字列リテラル
"Hello, world!"
- 型は
str
、実体はバイト列[u8]
- バイナリに埋め込まれ、実行時には Static Memory に格納されたうえ、そこへの参照が変数にセットされる。
- 文字列型ではない。なお Rust には文字列型はないかわりに String というライブラリで表現される。
- 文字列に関するごちゃごちゃの分かりやすい説明 -> https://qiita.com/k-yaina60/items/4c8e3562fe6d22f845a9
- char 型リテラル
'a'
,'あ'
など
- bool 型リテラル
true
又はfalse
文と式
- 文 / Statement
- 値を返さない
;
で終わる
- 式 / Expression
- 値を返す
;
は不要- 例
- スカラ値
- マクロ呼び出し
- 末尾に
;
のない関数呼び出し - 末尾に
;
のないスコープ
関数
関数は小文字のスネークケースで命名する。可変長の引数はサポートされない(マクロでは利用可能)。
Tips として、expected hogehoge, found "()"
というエラーは、return しわすれたときによく出るので覚えておくと良い。これは、if 式や while 式自体が空の Tuple()
を返すことに起因している。
// シンプルな関数
fn say_hello() {
println!("hello!")
}
// 引数あり
fn say_hello(num: i32) {
println!("number is {}", num)
}
// 引き数あり(値を書き換えてから関数内で利用したい場合)
// - 変更した値はスコープ内でのみ有効で、呼び出し元には反映されない
// - mutの位置に注意
fn mutate_number(mut y: i32) {
y - 1;
println!("{}", y)
}
// 引き数あり(呼び出し元の値を書き換えたい場合)
// - mutの位置に注意
// - `&`に注意
fn mutate_number(y: &mut i32) {
*y = 64;
}
// 戻り値あり(returnを使う場合)
fn say_hello() -> i32 {
return 32;
}
// 戻り値あり(式を使う場合)
fn say_hello2() -> i32 {
32
}
クロージャー
関数の中などに入れ子になっている匿名関数のこと。クロージャが定義されたスコープに存在する変数や値をキャプチャ(参照または所有権を取得)できる。引数や返り値の型は全て推論される。以下は全てクロージャーの例。
|x, y| { x + y };
|| { x + y };
|| {};
|| 123;
以下のようにスコープにある値を使える。
let s = "🍓".to_string();
let f = || {
println!("{}", s)
};
f();
ただしこのままだとクロージャーを返り値として返したり、別スレッドに渡したりすると問題が起きる。 なぜならクロージャーで使った値(上記でいうとs
)がドロップされるかもしれないからだ。解消法としてはmove
を使って所有権を移すことが挙げられる。こうしておけば使った値はクロージャーと一蓮托生となり、クロージャーをどこで使っても問題は起きなくなる。
let f = move || { /* ... */ };
クロージャーの型は以下のように記述する。
fn make_closure() -> impl Fn() -> i32 {
let v = 1234;
let f = move || v;
return f;
}
フロー制御
Rust では条件式を()
で囲む必要はない。 {
までの間にある記述が条件式として扱われる。
if
if age >= 35 {
println!("大人");
} else if age >= 18 {
println!("若者");
} else {
println!("子供");
}
if は式なので代入もできる。その際はセミコロンを省略すること。return は使えない、全てのブロックで同じ型を返さなければならない点に注意。
let num = if true {
5
} else {
6
};
match
パターンマッチングが行える。
let number = 1;
let result = match number {
1 => "1です",
2 => "2です",
_ => "その他です"
};
アームの左辺に変数を置いたうえで、if 文を使った分岐をすることもできる。この if 文をガードという。
let number = 1;
let result = match number {
x if x < 10 => "10より小さい",
x if x < 20 => "20より小さい",
_ => "20以上"
};
Enum と match を組み合わせたときの使い方はEnumの項を参照。
for
for 文には従来の言語のような初期化、条件、繰り返し前処理のような構文はない。break
が使える。
iter()
はイテレータを返す。配列のように順番があるものは順番通りに、Map のようなものは順番がないものはランダムに取り出される。
for thisNumber in [10, 20, 30, 40, 50].iter() {
println!("the value is: {}", thisNumber);
}
for thisNumber in 0..50 {} // 0から49まで
for thisNumber in 0..=50 {} // 0から50まで
loop
loop {
// do something
}
loop ではbreak
やcontinue
が使える。入れ子のループからそれらを行うときは、識別のためにtick identifierが使える。また、break
時に値を返すこともできる。
let number = loop {
break 100
}
while
基本的な動きは loop と同じ。
while number != 0 {
// do something
}
range
for number in (1..4) {
// do something
}
Slice
Slice には 2 種類ある。
まずは String Slice(&str
)である。これは文字列リテラルへの参照や、String
の一部分に対する参照である。
つぎに Array Slice(&[T]
)である。これは配列[T]
の一部分に対する参照か、ベクタVec<T>
の一部分に対する参照である。
// String Slice
let my_string: String = "hello world".to_string();
let my_string_slice: &str = &my_string[1..5]; // -> 'ello'
// Array Slice
let numbers = [1, 2, 3, 4, 5];
let numbers_slice: &[i32] = &numbers[1..3]; // -> [2, 3]
Struct
struct User {
username: String,
sign_in_count: u32,
}
const shota = User {
username: String::from("shota"),
sign_in_count: 23,
}
TS でおなじみの省略記法も使える。ドットは2個なので注意。
let default_user = User { /* 省略 */ };
let user = User {
username, // プロパティ名と設定したい変数名が同じ場合は省略できる
..default_user, // デフォルト値を使いたい場合はこうする
}
構造体をプリントするには Debug トレイトを実装する必要がある
#[derive(Debug)]
struct User { /* */ }
let user = Rectangle { /* */ };
println!("rect is {:?}", rect);
メソッドと関連関数
メソッド / Methods は self を引数にとる。インスタンスメソッドのようなもの。
関連関数 / Associated functions は self を引数にとらない。クラスメソッドのようなもの。
構造体にはクラスのような「継承」の概念はない。なぜなら、トレイトを使うほうが優れていると判断したからだ。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 関連関数
fn new() -> Self {
Self {
width: 10,
height: 10,
}
}
// メソッド
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
特殊な構造体
- Tuple 構造体
- 構造体のフィールド名自体にはさほど意味がないようなときに使う
struct Ipv4(u8, u8, u8, u8);
let address = Ipv4(192, 168, 1, 100);
- Unit-like 構造体
- 構造体に値がまったくないときやトレイトの実装で役立つ?詳細不明
Trait
Trait とは特性や特質のこと。複数の型にまたがって共通の振る舞いを定義するための仕組みである。Composition over Inheritance という考え方に基づいている。なお、Trait は構造体のみならず組み込み型に対しても実装できる。
struct Position {
x: f64,
y: f64,
}
pub trait Summary {
fn summarize(&self) -> String;
}
impl Summary for Position {
fn summarize(&self) -> String {
format!("x: {}, y: {}", self.x, self.y)
}
}
直接実装するのと Trait を介して実装することの違いは、Trait を介せばジェネリック関数を活用できるようになるという点である。Trait を持つ構造体をなんでも受け取るジェネリック関数は以下のように定義することができる。
fn notify<T: Summary>(item: T) {
println!("Breaking news! {}", item.summarize());
}
// 以下のようにも書ける?
fn notify(item: impl Summary) {
println!("Breaking news! {}", item.summarize());
}
Trait にはデフォルト実装を持たせることもできる。
pub trait Summary {
fn summarize(&self) -> {
println!("This is default summary...");
}
}
impl Summary for Position {} // デフォルト実装を生かしたいときは単に実装を書かなければ良い
構造体は継承関係を持つことができる。Trait A が祖先に Trait B, Trait C を持つ場合には、Trait A を実装した型は、Trait A/B/C のすべてのメソッドを実装する必要がある。
代表的な Trait には以下のようなものがある。
- Copy trait
- 代入時などに値のコピーが行われるようにする
- Display trait
- ある型の値をユーザーフレンドリーな文字列形式で出力するためのもの。
println!
- ある型の値をユーザーフレンドリーな文字列形式で出力するためのもの。