はじめに
Rust の入門をしようと思っていてできていなかったので、この機に軽く触っていきたいと思います。
久しぶりに新しい言語を学ぶので、せっかくなので初見の感想を残しておきます。
基本的に記録なので書いてあることが間違ってるかもしれないので、ご了承ください。あくまで私の学習のためのアウトプットです。
Rust について知っていることといえばこんな感じです。
- 「最も愛されているプログラミング言語」であること
- 安全/高速
- C/C++に置き換わり得る言語
- コンパイルがなかなか通らない
コードは何回が目にした気がしますが、全く記憶にないので初見です。
使うサイト
The Rust Programming Language 日本語版
本を買おうかとも考えましたが、軽く触るだけなのでこちらで。
その他、気になったら他の記事などを読んで補完していきます。
感想
ここ以降は自分がサイトを読みながら、思考をまとめるためにアウトプットしてるだけなので、ここで伝えたいことを書いておきます。
まず Rust 自体に対しての感想です。
まだ全て読み切ってはないですが、一言で言えば「良い感じ」です。おそらく今後も勉強していくと思います。
全体的な印象としては、**「使いやすい C++」**みたいな感じ。C++は大学で軽く触った程度ですが。
C/C++の後継と言いつつも全くの別物なのでは? と想像していましたが、所々 C/C++ の面影を感じさせる部分がありました。
しかし、メモリの確保・開放などを明示的に書く必要がないことや、型を書かないでも推論してくれたりと、色々スマートになった印象です。
Cargo, Rustfmt, Rust Language Server などの開発ツールの力強さは十分に感じとることができました。こういった言語以外の環境もかなり重要ですよね。
-
Cargo (パッケージマネージャ)
これについてはまだほとんど使っていませんが、小さなプロジェクトでもストレスなく使えたというのは強みだと思います。
-
Rustfmt (フォーマッタ)
自動フォーマットの感動を味わったことのない人は、ぜひ触ってみて欲しいです。普段 TS を書くときに Prettier を使っていますが、初めて Prettier を使った時はかなり感動したのを覚えています。
-
Rust Language Server (エディタの補完)
VSCode に拡張を入れて書いていましたが、かなり快適でした。
細かい点ですが、変数・関数の命名規則がスネークケースなのは良いですね。結局スネークケースが一番見やすいです。
ここ 2 年間くらい新しい言語に手をつけていなかったので、久しぶりの新しい言語への挑戦でした。
やっぱり新しい言語を勉強するのは疲れますね。本当はもう少し学習を進める予定でしたが、想定より早めに切り上げてしまいました。
とはいえ 1 年に 1 つくらいは新しい言語に触れていきたいと思うので、継続的にいろんな言語に触れていきたいと思います。
今後 Rust を学習していって、多少身についてきたら、この記事を自分で読み返してみます。
入門
まえがき
まずはまえがきから。
新しい言語・FW を勉強するときは、それらの思想を理解することが重要です。
言語を学ぶっていうと 具体 的な機能などを学ぶ印象が強く、実際そこにかける時間は長いです。
ただ 具体(機能) を学ぶのではなく、抽象(思想) と紐付けながら学ぶことで、効率よく言語の特性を学ぶことができます。
他に読んだ記事
道入
開発者チーム向けの説明に開発ツールの説明がありますね。
-
Cargo
依存マネージャ兼ビルドツール
-
Rustfmt
開発者の間で矛盾のないコーディングスタイルを保証します
JS・TS でいうところの ESLint 的なものですかね?
-
Rust Language Server
Microsoft Language Server Protocol に則った補完ツールで、いろんなエディタで補完が効きます、的なやつです。
そのほか、学生・企業向けの説明と、本の使い型が書いてあるので、しっかり目を通しておきます。
どこまでやるかは未定ですが、基本的に上から順に進めていきます。
事始め
インストール
rustup
というコマンドラインツールを利用するらしいので、指示にしたがって進めます。
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
1) Proceed with installation (default)
2) Customize installation
3) Cancel installation
インストールの方法を問われたので、デフォルトの 1 を選択。
先に紹介したツール類がインストールされてるのも確認できました。
$ rustc --version
rustc 1.48.0 (7eac88abb 2020-11-16)
Hello, World!
$ touch main.rs
癖で 1 回 main.ts
って打ちました。
fn main() {
// 世界よ、こんにちは
println!("Hello, world!");
}
$ rustc main.rs
$ ./main
Hello, world!
このコンパイルしてから実行する感じ、久しぶりですね。
Rust は C や Java のように main
関数が自動的に呼びだされるようです。
また特殊な点として良く見ると、println
の後ろに !
が付いています。
これは関数ではなくマクロを呼び出しているそうです。
マクロを読むと
マクロは全て、展開され、 手で書いたよりも多くのコードを生成します
マクロは、コンパイラがコードの意味を解釈する前に展開される
などの記述があるので、C のマクロ (#define
) と同等の機能だと予想できます。細かい説明はまた後で読みます。
Hello, Cargo!
次に Cargo に触れるようです。思ったより早い登場だと感じました。
プロジェクトの作成
$ cargo new hello_cargo --bin
$ cd hello_cargo
$ tree
.
├── Cargo.toml
└── src
└── main.rs
--bin
は通常のアプリを作るときのオプション(省略可能)で、代わりに --lib
とするとライブラリ用のプロジェクトが作られます。
Cargo.toml というファイルがあります。中身はこんな感じ。
[package]
name = "hello_cargo"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
TOML (Tom's Obvious, Minimal Language) って名前は初めて見ました。
どこかで見たフォーマットだなーって思いましたが、systemd の*.service ファイルにかなり近いですね。あと Pipfile。
役割としてはプロジェクトの情報や、依存関係を管理するためのものです。
main.rs には Hello, world! 用のコードが書かれています。
ビルド & 実行
$ cargo build
$ ./target/debug/hello_cargo
Hello, world!
ビルドすると、target/debug/ に実行可能ファイルが生成されます。
また、ビルドすると同時に新たに Cargo.lock が生成されています。
現状は空っぽですが、依存関係のバージョンを正確に示すためのファイルです。package-lock.json とかと同じ。
cargo build
の代わりに cargo run
を使うことでビルドから実行までを 1 コマンドで実行できます。
$ cargo run
Hello, world!
cargo build
と同様に target/ や Cargo.lock が生成されることを確認できました。
そのほかに cargo check
というコマンドがあります。こちらは基本的には cargo build
と同じですが、実行可能ファイルを生成しないため、より高速に動きます。
リリースビルド
$ cargo build --release
オプションをつけることで、 最適化を行なってコンパイルができます。
この場合、実行可能ファイルは target/release/ に生成されます。
数当てゲームをプログラムする
適宜別の章で知識を補完しながら、この章を進めていきます。
use std::io;
fn main() {
println!("Guess the number!");
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
こんなコードを書かされます。
新しく出てきたものとしては、次の 3 つです。
use
- 変数宣言
- 標準入力
use
は C++ の using
と同じ間ですかね。
変数
let
は変数の宣言であることを表します。また let
と変数名(guess)の間にある mut
は変数が mutable であることを表します。デフォルトは immutable になります。
String::new()
は String 型の関連関数である new を呼び出している、とのことです。コンストラクタとかはないっぽい?
関連関数は初めて聞く単語でしたが 静的(スタティック)メソッド のことだそうです。
let mut guess = String::new();
つまりこのコードは、
String 型の束縛関数 new を呼び出し、戻り値を可変変数 guess に束縛している。
ということになります。
標準入力
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
目を引くのは、引数の &mut guess
の部分です。
まず &
ですが、これは引数が参照であることを表します。PHP の参照渡しも同じ書き方ですよね(PHP はほとんど書いたことありませんが)。
これをさらに &mut
とすることで、参照が可変であることを表しています。
次に見るべきなのは .expect(...)
の部分です。
試しにこの部分を消して実行すると次のような警告が出ます。
warning: unused `std::result::Result` that must be used
--> src/main.rs:7:5
|
7 | io::stdin().read_line(&mut guess);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_must_use)]` on by default
= note: this `Result` may be an `Err` variant, which should be handled
warning: 1 warning emitted
read_line
は Result 型を返します。Result 型は Enum であり、Ok
か Err
をとります。
-
Ok
は成功を表し、生成された値を保持します。 -
Err
は失敗を表し、処理が失敗した過程や、 理由などの情報を保有します。
expect
は Result が Ok
の場合は保持している値を返して、Err
の場合はプログラムを終了させます。
乱数生成
乱数生成に必要な rand クレートを追加します。
[dependencies]
+ rand = "0.3.14"
クレートっていう単語はちょっとスルーしていましたが、7 章に説明があります。
ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
そのほか、バージョン指定の方法、アップデートのやり方(cargo update
)についての説明がありました。
コードを次のように書き換えます。
extern crate rand;
use rand::Rng;
use std::io;
fn main() {
println!("Guess the number!");
let secret_number = rand::thread_rng().gen_range(1, 101);
println!("The secret number is: {}", secret_number);
println!("Please input your guess.");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
println!("You guessed: {}", guess);
}
extern crate rand;
は、外部のクレートを使用するための行で、use rand;
と書いても同じ効果が得られます。
じゃあなんで、こんな書き方するの? って思ったので、軽く調べましたが、まだわからないので後でまた調べます。
調べてみると、あまり使う必要がないというコメントもあったので、その辺りも頭の片隅に留めておきながら読み進めて行きます。
また、rand の使い方がわからずとも cargo doc --open
を実行することでドキュメントを読むことができます。
読んだ記事
予想と秘密の数字を比較する
use std::io;
use std::cmp::Ordering; // 追加
use rand::Rng;
println!("You guessed: {}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
cmp
は enum である Ordering を返します。
ここで新たに match
式が登場します。cmp
の結果が Ordering::Less
の場合は、1 つ目のパターンにマッチして println!("Too small!")
が実行されます。
それはそうと、さっきから VSCode がエラーを表示してるので修正します。
match guess.cmp(&secret_number) {
^^^^^^^^^^^^^^ expected struct `String`, found integer
要は guess と secret_number の型が違うためにエラーが出ています。
これを解消するために次のコードを追加します。
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Failed to read line");
let guess: u32 = guess.trim().parse().expect("Please type a number!"); // 追加
println!("You guessed: {}", guess);
guess.trim().parse().expect(...)
の部分は文字列を数値に変換しているだけですが、
特筆するべき点として、その値を新たな変数 guess に束縛しています。
guess という変数が 2 度宣言されていますが、Rust はこれを許容しています。
こうすることで 1 つめの guess は覆い被され、2 つめの宣言以降は使えなくなります。これをシャドーイングと呼びます。
ループ処理
loop {
println!("Please input your guess.");
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => println!("You win!"),
}
}
loop
で囲われた部分は無限ループされます。
当然正解を当てても、実行は終了しません。
これを終わらせるために、次のようにコードを書き換えます。
// --snip--
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"),
Ordering::Greater => println!("Too big!"),
Ordering::Equal => {
println!("You win!");
break;
}
}
}
}
break
文でループから抜けることができます。
これでほとんど動くようになりましたが、入力時に数字以外を入れた時にプログラムが終了してしまいます。これは、あまり親切ではありません。
- let guess: u32 = guess.trim().parse().expect("Please type a number!");
+ let guess: u32 = match guess.trim().parse() {
+ Ok(num) => num,
+ Err(_) => continue,
+ };
このように Err
の時に continue
することで、プログラムを続行させることができます。
Result はエラーが起こりうることを示してくれますが、対処の方法は実装者依存です。
なのでエラーとすべきか・別の値に変換するか、などはしっかり考える必要がありますね。
最後にsecret_number
を出力する文を消して完成です。
一般的なプログラミングの概念
普通に読みました。
数当てゲームでは使わなかった、Rust の基本的な文法を学びます。
-
変数と可変性
-
定数は
const
で宣言するconst MAX_POINTS: u32 = 100_000;
-
-
データ型
-
タプル型
let tup: (i32, f64, u8) = (500, 6.4, 1); let x = tup.0; let y = tup.1; let z = tup.2; println!("The value of y is: {}", y);
let tup: (i32, f64, u8) = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {}", y);
タプルが存在して、上のように分割代入ができます。Rust では分配と呼ぶそうです。
-
-
関数
return
の省略。関数内の最後の式が暗黙的に return される。fn plus_one(x: i32) -> i32 { return x + 1; }
fn plus_one(x: i32) -> i32 { x + 1 // セミコロンがあると、文として扱われてしまって return されないので注意 }
セミコロンをつけたときのエラーが、
i32を予期したのに、()型が見つかりました
なので空のタプルが返されてるようです。 -
フロー制御
-
if 式
let number = 3; if number < 5 { println!("condition was true"); } else { println!("condition was false"); }
条件式は bool 値でないといけない
if number { // expected `bool`, found integer ... }
if 式 なので、値を返す
let condition = true; let number = if condition { 5 } else { 6 }; println!("The value of number is: {}", number); // > The value of number is: 5
-
while
while は条件付きループ
let mut number = 3; while number != 0 { println!("{}!", number); number = number - 1; }
-
for
let a = [10, 20, 30, 40, 50]; for element in a.iter() { println!("the value is: {}", element); } // > the value is: 10 // > the value is: 20 // > the value is: 30 // > the value is: 40 // > the value is: 50
Range を使ったループ
for number in (1..4).rev() { println!("{}!", number); }
-
所有権を理解する
メモリの管理についての話のようです。
ガベコレが動くわけでも、明示的にメモリの確保をするでもない、別の方法を Rust はとっているとのことです。
つまり今までの知識が活かせないところなので心して学んでいきます。
所有者規則
- Rust の各値は、所有者と呼ばれる変数と対応している。
- いかなる時も所有者は一つである。
- 所有者がスコープから外れたら、値は破棄される。
所有者がスコープから外れたら、値は破棄される。
try-with 文が自動的に close してくれるのと似ていると思いました。
変数がスコープから外れたら、自動的にメモリが解放されます。Rust では drop
関数が呼び出されます。
所有者は一つ
let x = 5;
let y = x;
let s1 = String::from("hello");
let s2 = s1;
このような 1 と 2 のコードがあった場合、2 つのコードには大きな違いがあります。
1 の場合は、y
に x
の値がコピーされ、両方の値は 5
になります。
2 の場合も、s1
と s2
の両方が "hello"
という値をとりますが、値のコピーは起こりません。そのため両方が同じメモリを参照することとなります。(s1
-> "hello"
<- s2
の関係)
しかし、Rust はそのようなことを許していません。なぜなら 2 つの変数がスコープから外れた場合に、同じメモリを解放しようとするからです。
そのため、所有権の譲渡が行われます。これをムーブと呼びます。2 の場合 let s2 = s1;
でムーブが発生します。
例えば次のようなコードはコンパイルが通りません。
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
// ^^ value borrowed here after move
では値がコピーされるものとされないものの違いはどこにあるのかですが、Copy
トレイトと呼ばれる特別な注釈に適合するかどうかが関わっているそうです。
トレイトについては深く説明はありませんが、Java のインターフェースに近いものでしょうか?
代入以外のムーブ
関数呼び出しの際、引数に渡す変数についてもムーブが起きます。
また、例は書きませんが、戻り値についても同様です。
fn main() {
let s1 = String::from("hello");
test(s1); // ここでs1の値が関数にムーブする
println!("{}, world!", s1);
// ^^ value borrowed here after move
}
fn test(s: String) {
println!("{}, world!", s);
}
参照と借用
先ほどのムーブの有用性についてはわかりましたが、流石に関数に渡した変数が使えなくなってしまうのは不便です。
これを回避するための方法として、参照があります。
fn main() {
let s1 = String::from("hello");
test(&s1);
println!("{}, world!", s1);
}
fn test(s: &String) {
println!("{}, world!", s);
}
参照については 2 章で触れた通り、&
を使うことで参照を渡すことができます。
この場合、s
は s1
自体のメモリを参照するため、同じメモリを参照することにはなりません。(s
-> s1
-> "hello"
の関係)
このように、関数に参照を渡すことを借用と呼びます。
さて、ここで借りてきた値を変更しようとするとどうなるでしょう。
fn main() {
let s1 = String::from("hello");
test(&s1);
println!("{}", s1);
}
fn test(s: &String) {
s.push_str(", world!");
// ^ `s` is a `&` reference, so the data it refers to cannot be borrowed as mutable
}
当然怒られます。
ただし、許可すれば変更が可能になります。ここで出てくるのが標準入力で使用した、&mut
です。
fn main() {
let mut s1 = String::from("hello");
test(&mut s1);
println!("{}", s1);
}
fn test(s: &mut String) {
s.push_str(", world!");
}
つまり、この形です。ここまでしてようやく呼び出し先で値を書き換えることができます。呼び出しを見て、書き換えの可能性がわかるのは良いですね。
ただ、可変の借用にも制約があります。それは一度に複数の可変の借用はできないということです。
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
// ^^^^^^ second mutable borrow occurs here
println!("{}", r1);
println!("{}", r2);
ダングリングポインタ
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
// ^ expected lifetime parameter
let s = String::from("hello");
&s
}
こちらも Rust ではエラーになります。
当然 s
は dangle
の呼び出し後に開放されるので、&s
は無効な参照となります。
スライス型
所有権のない別のデータ型は、スライスです。スライスにより、コレクション全体というより、 その内の一連の要素を参照することができます。
また聞き慣れない単語ですが、どうやらコレクションに関わる用語のようです。
説明を読むとここでいうスライスとは他の言語のリストのメソッドなどに見られる、slice
関数と同じもののようです。
[1,2,3,4,5].slice(2,4);
// > [ 3, 4 ]
ここで気になるのが、スライス「型」という部分です。
スライス型は先の例の[ 3, 4 ]
のようにリストの一部を参照する型のようです。
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];
for n in slice.iter() {
println!("{}", n);
// 2
// 3
}
let a: String = String::from("Hello, world");
let slice: &str = &a[0..5];
println!("{}", slice);
// Hello
この &[i32]
や &str
がスライス型を呼ばれるものです。
この後、構造体と続きますが、長くなってきたので一旦ここまでで。