Rust入門日記 - 基本編
とりあえず、Rustを入門するにあたって、いろいろ学んだことや、注意ポイントなどをまとめていく。
まとめ終わったら、Ruby, PHP使いが始めるRust的なBookにまとめていこうかななどと思っている。
読んでいる資料など
環境
- macOS Catalina
- IntelliJ Idea Ultimate 2020.3 + Rust Plugin
自分が業務上経験している言語など
- Ruby
- JavaScript
- TypeScript
- PHP
- Java
- Groovy
最近、メインで使っているのがRubyであるため、Ruby関連のツールとの比較やらが
よく出てくるかもしれない。
目標
とりあえずの到達ゴールを3つとしたい。
- 競技プログラミング (AtCoder) の問題をRustで解けるようにする: 緑コーダーで最近はお休みしてますが、そろそろ復帰しようかなと。
- Web API が実装できるようになる (GraphQLでも、RESTでもどっちでもおk): これは業務レベルで最終的に使うことを想定したいので、Clean Architecture の適用なども考える。
- ラズパイのGPIO制御: 完全に趣味。
目次
基本編はこのスクラップです
競技プログラミング編
Webアプリケーション編
ラズパイGPIO編
まだやってない
インストール
の「おすすめ」の手段に従って、インストールをするだけ。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
自分の home ディレクトリ下に展開されるので、特に権限はいらないようだ。
インストール後、画面の指示に従い、以下を実行する。
source $HOME/.cargo/env
また、次回 shell を起動したときに、自動的に環境をセットアップするために、
以下を行う。自分は zsh を使っているので、~/.zshrc に追加する。
echo "source $HOME/.cargo/env" >> ~/.zshrc
これで、以下のようにバージョン情報が出ればインストール成功。
$ rustc --version
rustc 1.49.0 (e1884a8e3 2020-12-29)
今後、rust のバージョンアップを行いたい場合は、以下を行えばよい。
rustup update
rustup について
rustup は、rust の関連ツールを管理してくれる便利なアプリケーション。
以下を管理してくれる。Rubyでいうと、rbenv のような機能を持っている。
- コンパイラである、rustc
- パッケージマネージャーである、cargo (Ruby でいうと Bundler + ruby gems みたいなもの)
- その他諸々
古いバージョンを入れたい
Rust は Edition という概念により互換性を保っているそうだが、チーム開発する場合などは
コンパイラのバージョンを揃えたほうが良さそうだ。
また、AtCoder のような競技プログラミングでは、コンパイラのバージョンは大抵指定されている。
今回の目標の1つとして、競技プログラミング向けの環境を作ることが掲げられているため、
2021/02/02現在、AtCoderで採用されているコンパイラである、rust 1.42.0 をインストールする。
rustup install 1.42.0
これで、古いバージョンは入ったが、デフォルトをこのバージョンに切り替えたい場合
(rbenv global x.x.x
みたいなやつ。)
以下のコマンドを実行すれば問題なさそうだ。
rustup default 1.42.0
もとに戻す場合は、
rustup default stable
ただし、実際にはディレクトリごとに管理したほうが良いと思われるので、
対象のプロジェクトフォルダに、rust-toolchain
というファイルを置き、その中にバージョンを書くことで
バージョンの統一が図れそうである。(rbenv における、.ruby-version
のような仕組み)
$ cd プロジェクトのフォルダ
$ echo "1.42.0" > rust-toolchain
$ rustc -V
rustc 1.42.0 (b8cedc004 2020-03-09)
プロジェクトを作る
Rust には単体で動作する、rustc というコンパイラが用意されているが、依存ライブラリの管理等を
考えると、直接使うのは得策ではなさそうである。
ということで、rustup により一緒にインストールされている、cargo というツールを利用する。
以下で新しいプロジェクトを開始できる。
cargo new hello_cargo
hello_cargo というフォルダが作られて、中には1つのファイルと、1つのディレクトリが作られる。
$ cd hello_cargo
$ tree
.
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
デフォルトでは不可視ファイルとして、.git
と、.gitignore
も作られる。
これは、 cargo new
で、 --vcs none
とすることで、作らない設定も可能なようだ。
Cargo のドキュメントによると、設定ファイル を、$HOME/.cargo/config.toml
に置き、以下のような設定をすることで、デフォルトでは作らない設定とすることもできる。
[cargo-new]
vcs = "none"
この辺はお好み。
作ったプロジェクトをとりあえず動かしてみる。
src/main.rs
には最初から動作するコードが書かれているので、以下によりビルド+実行することができる。
こんにちは世界!
$ cargo run
Compiling hello_cargo v0.1.0 (/path/to/hello_cargo)
Finished dev [unoptimized + debuginfo] target(s) in 2.90s
Running `target/debug/hello_cargo`
Hello, world!
このタイミングで、targetディレクトリに実行用のバイナリが作られ、
Cargo.lock
という、依存管理のためのファイルもこのタイミングで作られる。
Ruby gems でいうと、Gemfile.lock 的な存在だろう。
開発用のバイナリをビルドだけしたいときは
$ cargo build
で、行える。
$ ./target/debug/hello_cargo
Hello, world!
リリース用のビルドは以下のコマンドで作ることができる。
コンパイル時間は長くなるが、実行時間は最適化されて早くなるそうだ。
$ cargo build --release
これにより、target/release/hello_cargo
にバイナリが作成される。
Linux用にバイナリを作るか、Windows用に作るかなどの設定もできるようだが、
しばらくはとりあえず実行することに着目するために、まとめるのをこの辺くらいまでにしておく。
IntelliJ IDEA で Rust のプロジェクトを開く
愛用している IntelliJ IDEA で Rust のコードを書けるようにする。
まずは、JetBrains 公式で出している、Rust Plugin を導入する。
また、デバックも行っていくことも考え、Native Debugger Plugin も導入する。
インストール後、再起動する。
次に先程作成した、hello_cargo プロジェクトを IntelliJ で開く。
src/main.rs
を開くと、以下のようにハイライトされた状態で展開される。
mainの左にある、再生ボタンを押すと、以下のように Cargo で実行するというオプションが
出てくるので、実行する。(Run 'Run hello_cargo')
実行結果が画面下に表示される。
デバッグが行えるかも試す。
試しに、ブレークポイントを println!("Hello, world!");
の行に入れてみる。
Debug "Run hello_cargo" を実行する。Shift + F9!
最初のデバッグ実行時はツールのインストールについての確認が出る。
特に断る理由はないので「Install」を実行する。
ブレークポイントでプログラムが止まったことを確認。
ここまでできれば最初の一歩は大丈夫そうだ。
なんとなく main.rs を読んでみる
とりあえず何も考えずに、Cargo でプロジェクトを作ることにできるプログラムを読んでみる。
fn main() {
println!("Hello, World!");
}
どうやら、C言語などと同じ用に、main という関数を最初に読む仕組みのようだ。fn がまだ何者かわからないけど、多分関数だろう。
また、println! というのは、文字列を出力して改行するという処理を行ってくれる、関数ではなく、マクロというものだそうだ。
関数とマクロの違いは、たぶんCと同じでマクロはコンパイラが実行環境などに合わせてうまいところやって別のコードに変えてくれるもので(コードを生成するコード)、関数は実行時に呼び出すもののようだ。
マクロには !
がつくっぽい。おそらく、しばらくマクロを使うことはあっても、自ら書くということは当分先なようである。
ということで、しばらく main.rs をいじって実行しながら動作を検証してみる。
値を出力したいとき、C言語やRubyでいう printf
に当たる概念を知ってくと便利そうだが、
これは、 {}
により実現できるようだ。
// 12 が出てくる。
println!("{}{}", 1, 2);
不変変数
let 変数名: 型 = 代入する値;
で、値を再代入できない変数を定義することができる。
基本的な型
基本的な型は以下のものがある。
- i32: 符号付きの整数型。32の部分が、ビット数で、8, 16, 32, 64, 128を指定することができる。数値は可読性の向上のため、
_
を入れても良い。 - isize: 符号付きの整数型。ビット数はアーキテクチャに依存。
- u32: 符号なしの整数型。32の部分が、ビット数で、8, 16, 32, 64, 128を指定することができる。
- usize: 符号なしの整数型。ビット数はアーキテクチャに依存。
- f32: 浮動小数点型。32の部分がビット数で、32, 64のみを利用できる。
- bool: 論理型。true もしくは false
- char: 文字型。文字1文字を格納できる。ここで言う文字は、ユニコードのスカラー値を表すので、基本的には漢字などが格納できる。char値には、濁点などを表現する結合文字が入る場合もあるので、厳密には「文字」ではないかもしれない。) 文字はシングルクォートで囲むことで表現できる。
タプル型
複数の値をまとめて管理したいときに使うもの。
// 2, 3をfooにセット
let foo: (i32, i64) = (2, 3);
// 添字で取り出す
println!("{}", foo.0);
println!("{}", foo.1);
// タプルで受け取って再定義する
let (foo_1, foo_2) = foo;
println!("{}", foo_1);
println!("{}", foo_2);
配列型
他の言語でもよくある配列。
// i32の数値3つの配列を定義する
let foo:[i32; 3] = [1, 2, 3];
// 添字は foo[0], foo[1] のように取り出す
println!("{}", foo[0]);
// 配列の長さは len() で取れる
println!("{}", foo.len());
let で型を省略するとどうなるか?
セットした値に基づいて、自動的に最適な型が選択されるので、明示的であれば
省略する方針にしてもよさそう。
IntelliJ + Rust Pluginでは省略した場合、どの型になるのかを見ることができるので便利である。
// i32になる
let a = 1_234;
// boolean になる
let b = true;
// char になる
let c = '😍';
// f32 になる
let d = 1.02;
渡す値を 1.
や 1.0
のようにしても、浮動小数点型として取り扱われるようだ。
IntelliJ + Rust Plugin で、自動的に型が選択されているのがわかる様子。
ただ、定義後の使い方によっては、しっかりと型定義をしないといけなそうではある。
2回 同じ変数の名前でlet をするとどうなるか?
let a = 1_234;
println!("{}", a);
let a = 4_567;
println!("{}", a);
このプログラムは普通に動く。JavaScript の const の場合だと、
「もうそれある!」などというエラーが出てくるが、こちらは再定義ができる。
後述する、可変変数から不変変数に同じ名前で定義することもできそうである。
可変変数
後々値を変えたい場合は、mut
というキーワードを let
のあとに入れる必要がある。
let mut foo = 123;
foo = 456;
println!("{}", foo);
定数
constを利用することで、定数を定義できる。
const MY_NUMBER:i32 = 2;
let も値を変更できないじゃないか、とは思ったものの、const については、同じ名前のものを定義できない。
また、型についての記述の省略もできないようだ。
// これはエラー
const MY_NUMBER = 2;
const MY_NUMBER:i32 = 2;
// これはエラー
const MY_NUMBER:i32 = 3;
const はコンパイルを行うときにインライン化されるとのこと。
競技プログラミングでいうと、1000000007 などをセットしておくと良さそう。
数値の記載方法
数値は、10進数だけでなく、16進数(0x
から始まる)、8進数 (0o
から始まる)、2進数 (0b
から始まる)
でも表現することができる。
プログラムの中で明示的に取り扱いたい場合は便利そうだ。
// 16進数表記, 10進数表現の291と表示される
println!("{}", 0x123);
// 8進数表記, 10進数表現の83と表示される
println!("{}", 0o123);
// 2進数表記, 10進数表現の10と表示される
println!("{}", 0b1010);
演算
四則演算は、他のプログラミング言語と大きな差はない。
四則演算は以下のようになる。
// 加算 3
println!("{}", 1 + 2);
// 減算 -1
println!("{}", 1 - 2);
// 掛け算 2
println!("{}", 1 * 2);
// 割り算 2
println!("{}", 4 / 2);
// あまりを出す 1
println!("{}", 10 % 3);
// 掛け算・割り算から先にやる 7
println!("{}", 1 + 2 * 3);
// カッコを使うことで優先順位を変えられる 9
println!("{}", (1 + 2) * 3);
// 割り算 整数型どうしの割り算で、表示も整数型となるため3となる
println!("{}", 10 / 3);
// 割り算 浮動小数点型どうしの割り算のため、表示は 3.3333333 となる
println!("{}", 10.0 / 3.0);
べき乗については、型が持つ機能である、pow を利用する。
// 2の2乗 = 4
// 2.pow(2) とはかけない
// 2_i32.pow(2) とすることはできる。
let i:i32 = 32;
println!("{}", i.pow(2));
C言語やら、PHPなどにある、インクリメント・デクリメントに当たるものはないので、
変数にある値の四則演算を行う場合は、演算子に=
をつけた代入演算子を使う必要がある。
let mut i:i32 = 32;
// i = i + 1
i += 1;
// 33
println!("{}", i);
// i = i - 1
i -= 1;
// 32
println!("{}", i);
ほかの言語にあるような、シフト演算、論理演算もさほど変わりなし。
// 2の左シフト, 0b0010 が 0b1000 となるため、8
println!("{}", 2 << 2);
// 8の右シフト, 0b1000 が、0b0010 となるため、2
println!("{}", 8 >> 2);
// AND演算 0b10 と、0b11 のビットが1で重なる桁が1となるため、 0b10 となり 2
println!("{}", 2 & 3);
// OR演算 0b10 と、0b11 のどちらかが1であれば、1となるため、0b11 となり 3
println!("{}", 2 | 3);
// XOR演算 0b10 と、0b11 のビット同じ桁は0、そうでない場合は1となり、0b01 となり 1
println!("{}", 2 ^ 3);
なお、型が異なるものを演算しようとすると、コンパイラに怒られる。
println!("{}", 1.5 + 2);
error[E0277]: cannot add an integer to a float
--> src/main.rs:2:24
|
2 | println!("{}", 1.5 + 2);
| ^ no implementation for `{float} + {integer}`
|
= help: the trait `Add<{integer}>` is not implemented for `{float}`
as
キーワードを使うことでキャストができる場合がある。
println!("{}", 1.5 + (2 as f32));
文字列についてちょっと触れる
文字列については、String
と、&str
が存在する。
一旦、スライスや参照、ライフサイクル、所有権についてやらないと&str
と、String
の違いについて理解できなそうなので、まずは触れるだけのレベルでやってみる。
let moji = "Hello";
とすると、文字の型は &str
型となる。これは スライス と呼ばれるもので、可変変数にしたところで、編集することはできない。文字列の位置しか示していない状態だそうだ。
let moji = "Hello";
println!("{}", moji + ", World");
などとやると、コンパイラに、
3 | println!("{}", moji + ", World");
| ---- ^ --------- &str
| | |
| | `+` cannot be used to concatenate two `&str` strings
| &str
などと怒られる。&str
と &str
は結合できません! とのこと。操作が必要な場合は String
を使う必要がある。
let moji = String::from("Hello");
println!("{}", moji + ", World");
この状態だと、String
側に、&str
と結合する機能が用意されているので、無事に出力することができる。
もうちょっと、変数の性質について理解しないと、なぜこの2つが存在しているのかがわからないかもしれない。
配列とベクター
String
と、&str
の関係を探るためには、まずはこのへんから知る必要がありそうだ。
そもそも、配列は固定長で、一度定義したら大きさを変えることはできない。
let mut arr:[i32; 3] = [1, 2, 3];
// mut なので、OK
arr[0] = 30;
println!("{}", arr[0]);
// ダメだよ (PHPとか、Rubyとかでやっていいやつ。)
arr[3] = 30;
println!("{}", arr[3]);
では、可変な配列はどのように作るべきか?
ということで、ベクター Vec
を使う。
let mut arr:Vec<i32> = Vec::new();
arr.push(1);
arr.push(2);
arr.push(3);
// 1
println!("{}", arr[0]);
// 3
println!("{}", arr.len());
ベクターは可変な配列で、コンパイル時に大きさがよくわからない物となっている。
そのため、メモリ上ではデータをとりあえず置いていくヒープ領域を確保することになる。
初期値がある場合は、以下のようにvec!
マクロを使って通常の配列を渡す。
let mut arr:Vec<i32> = vec!(1, 2, 3);
// 1
println!("{}", arr[0]);
// 3
println!("{}", arr.len());
見様見真似のループ処理
ループ処理には以下のような方法がある。
while
条件に一致している場合は、ループする。ほかの言語でもよくある構文。
while true {
println!("無限ループ!!");
}
loop
break
されない限り、ループから抜け出すことができない。
loop {
println!("無限ループ!!");
}
loop {
println!("無限ループ、しない!!");
break;
}
loop は値を返すことができるらしいが、ブロックなどを取り扱うときに考える。
for
ここまで、やってきたら Ruby の each
や PHP の foreach
にあたる文がほしい。
ということで、見様見真似でやるとこうなる。
let arr = [1, 2, 3];
for i in &arr {
println!("{}", i);
}
これを実行すると以下のように出力された。
1
2
3
感じとしては、arr の中身を1つずつとってきて、i に入れて、iを出力するという流れに見えるが、ここで &arr としているのに、秘密がありそうだ。
参照
ということで、 &
については、「参照」であるとのことだ。
let i = 300;
let re = &i;
// 300
println!("{}", re);
300という値を re
に書き込むのではなく、 re
には i
のメモリのアドレスを渡すという仕組み。
上記の場合、re
のメモリのアドレスにある、300が出力されるが、実際のデータは i の場所にある。
この、参照をする行為を「借用」という。
上記のコードについて型を省略せずに書くと、re
は i32 の参照型ということになる。
let i:i32 = 300;
let re:&i32 = &i;
println!("{}", re);
先述の for では、値そのものというよりは、配列のメモリの番地を取り出してループしていくという形になるので、for i in &arr
という書き方になり、i には、配列の中の値の参照型になるという形になるようだ。
一瞬面倒かな? と思ったが、そう考えれば、ごくごく自然な構文だと感じた。
ライフタイム
Rust は、ガーベージコレクションがないため、高速に処理できると言われている。
一方で、C言語にあるような malloc のように動的にメモリを確保したり、解放するような面倒なことは標準ではやっていない。
Rust は言語構造として、使う場所と使わなくなる場所を管理する仕組みが備わっている。
そんな感じなので、変数がどこから生まれて、どこで使い終わるのかを理解する必要がある。
変数が生き残る期間をライフタイムという。
fn main() {
let a = 123; // aはここから使えるよ
println!("{}", a); // aはここで最後に使うので、これ以降はaはないよ
}
ブロックという構造によって、変数のライフタイムをブロックの中に閉じ込めることができる。
fn main() {
let a = 123; // aはここから使えるよ
{
let b = 123; // bはここからつかえるよ
println!("{}", a); // ブロックの外にある a は使えるよ
println!("{}", b); // ブロックが終わる & bが使い終わったのでbはここまで
}
// b はブロックの中なので、ここではbは使えないよ
println!("{}", a); // aはここで最後に使うので、これ以降aはないよ
}
ブロックの最後の式を、変数に代入することもできる。
ちょっとした計算を行って、変数に入れたい場合には便利そうだ。
このとき、返す値の行にはセミコロンを付けてはいけない。
fn main() {
let a = {
300 + 400
};
println!("{}", a);
}
以下のような可変変数を用意して、「借用している変数のライフタイムが残っている」状態で、変数の値を変更しようとするとコンパイルエラーとなる。
fn main() {
let mut a = 123; // aはここから使えるよ
let b = &a; // aを借りるよ, bはここから使えるよ
println!("{}", b);
a = 456; // エラー!! a は b が借りているから、変更できないよ!!
println!("{}", b); // bはここまで使えるよ
}
3 | let b = &a; // aを借りるよ, bはここから使えるよ
| -- borrow of `a` occurs here
4 | println!("{}", b);
5 | a = 456;
| ^^^^^^^ assignment to borrowed `a` occurs here
6 | println!("{}", b);
| - borrow later used here
ライフタイムを意識してコードを書き換えるとコンパイルが通る。
fn main() {
let mut a = 123; // aはここから使えるよ
let b = &a; // aを借りるよ, bはここから使えるよ
println!("{}", b); // bはここまで使えるよ
a = 456; // a を借りている変数はないため、変更できるよ
}
スライス
ということで、ようやく &str
, String
の謎に近づけそうだ。
配列と、Vec
は固定長か、可変長かの違いだが、操作するときはなるべく共通の操作をしたい。ということで、配列もしくは Vec
を参照してまとめて取り扱えるようにした仕組みが「スライス」と呼ばれるもの。
以下のように、b
と、 d
は共通の型である、 スライス &[i32]
を持ち、共通の操作を行うことができる。
fn main() {
let a: [i32; 3] = [1, 2, 3];
let b: &[i32] = &a;
// [1, 2, 3]
// "{:?}" は、デバッグプリント的なことができる。 Ruby でいう p やら、PHPの var_dump みたいなノリ
println!("{:?}", b);
let c: Vec<i32> = vec!(4, 5, 6);
let d: &[i32] = &c;
// [4, 5, 6]
println!("{:?}", d);
}
スライスを作るときは、範囲指定などをおこなって切り取ることもできる。
この辺の指定の仕方は Ruby に似ているかもしれない。
fn main() {
let a: [i32; 4] = [1, 2, 3, 4];
let b: &[i32] = &a[2..3]; // 2番地から3番地の間にある値のみの参照
// [3]
println!("{:?}", b);
let c: Vec<i32> = vec!(4, 5, 6, 7);
let d: &[i32] = &c[2..3]; // Vec でも同じことができる
// [6]
println!("{:?}", d);
}
fn main() {
let a: [i32; 4] = [1, 2, 3, 4];
let b: &[i32] = &a[2..]; //2番地から最後まで
// [3, 4]
println!("{:?}", b);
}
fn main() {
let a: [i32; 4] = [1, 2, 3, 4];
let b: &[i32] = &a[..3]; //3番地まで
// [1, 2, 3]
println!("{:?}", b);
}
fn main() {
let a: [i32; 4] = [1, 2, 3, 4];
let b: &[i32] = &a[1..=2]; //1番地から、2番地まで
// [2, 3]
println!("{:?}", b);
}
ということがわかると、&str
と、String
の関係は、&[i32]
と、Vec<i32>
のような関係だということがなんとなくわかってくる。
"Hello, World"
が作られると、それはメモリのヒープ領域に置かれ、それを参照するものが &str
ということになる。
これを理解すると、以下のようなこともおのずとできることがわかる。
fn main() {
// 文字用のベクタ
let a: String = String::from("hello");
// ベクタのスライスを作る。0文字目から2文字目の間を取り出し
let b: &str = &a[0..2];
// he
println!("{}", b);
}
長い道のりだったが、&str
と String
の2つが存在する意義を見出すことができた。
スライスの型である [i32]
で新しい配列が定義できないように、str
で新しい値を作ることができない。ドキュメントを読むと、String
の中には、Vec[u8]
が入っていて、String
は、これをうまいところ処理をしてユニコードな文字であることを保障してくれるというものらしい。&str
も同様に、取り扱うものが文字であることを保障してくれる。
分岐構文
if
そういえば、まだやってなかったと思い立つ。
この構文はさほど他の言語と大きな差はない。
fn main() {
let a = 30;
if a > 20 {
println!("aは20よりおおきいあるよ");
} else if a > 10 {
println!("aは10よりおおきいけど、20以下あるよ");
} else {
println!("aは10以下あるよ");
}
}
ということで、ここはあまり詳しい説明はナシ。
match
Ruby でいう case...when 構文のような感じ。
ここは、後々 enum が出てくるときによく使いそうではある。
fn main() {
let a = 3;
let b = match a {
// a が1なら、1
1 => 1,
// a が3か4なら、2
3 | 4 => 2,
// それ以外の場合は3
_ => 3
};
// 2が出力される
println!("{}", b);
}
ちなみに、範囲指定的なこともできるという記事やらドキュメントも見かけるが、現状コンパイラに怒られる。
fn main() {
let a = 3;
let b = match a {
1..3 => 2,
_ => 3
};
println!("{}", b);
}
error[E0658]: exclusive range pattern syntax is experimental
--> src/main.rs:4:9
|
4 | 1..3 => 2,
| ^^^^
|
= note: see issue #37854 <https://github.com/rust-lang/rust/issues/37854> for more information
if let
a が 3だったときに、なにかをしたい場合は、シンプルに以下のように書くこともできる。
fn main() {
let a = 3;
if let 3 = a {
println!("hello");
}
}
これは、以下と同じである。
fn main() {
let a = 3;
match a {
3 => println!("hoge"),
_ => ()
}
}
関数
関数を定義したいときは、以下のように書くことができる。
# 関数名 (引数名: 型) -> 返り値の型
fn double(num: i32) -> i32 {
return num * 2;
}
fn main() {
println!("{}", double(2));
}
もちろん引数は複数にすることができる。
fn multiply(num: i32, num2: i32) -> i32 {
return num * num2;
}
fn main() {
println!("{}", multiply(30, 20));
}
返り値を複数にしたければ、タプル型を返すなりすればいい。
もうちょっと意味をもたせるのであれば、後々やる構造体 (struct
) を使うのも手である。
fn hop_step_jump(num: i32) -> (i32, i32, i32) {
return (num * 2, num * 3, num * 10);
}
fn main() {
let (v1, v2, v3) = hop_step_jump(2);
println!("{}", v1);
println!("{}", v2);
println!("{}", v3);
}
返り値がない場合は、特にかかなくても問題ない。
省略せず記載する場合は、-> ()
を記載すれば良い。
fn say_hello() {
println!("Hello!");
}
fn main() {
say_hello();
}
ムーブと所有権
今まで、参照型について取り扱ってきたが、なぜ参照型が便利なのかがいまいちわかってない。
メモリとかを省略できそう感はあるものの、実際は Rust が持つ所有権システムがそれを必要としているようだ。
以下は num2 に、num1 の値を渡している例。
これは特に問題なく、num2 という変数には、num1 の値が「コピー」される。
fn main() {
let num1 = 1;
let num2 = num1;
// 1
println!("{}", num1);
// 1
println!("{}", num2);
}
このように num1 を可変変数にしておくと、num1 と num2 の値は独立して存在しているのがわかる。
fn main() {
let mut num1 = 1;
let num2 = num1;
num1 += 1;
// 2
println!("{}", num1);
// 1
println!("{}", num2);
}
i32
, bool
, char
のような、基本的な型や、その配列, タプルであればこのような挙動になる。
一方で、String や Vec について考えてみる。
以下のコードはコンパイラに怒られる。
fn main() {
let s1 = String::from("Hello!");
let s2 = s1;
println!("{}", s1);
println!("{}", s2);
}
2 | let s1 = String::from("Hello!");
| -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 | let s2 = s1;
| -- value moved here
4 | println!("{}", s1);
| ^^ value borrowed here after move
これは、s2
に s1
を渡したときに、値が「コピー」されるのではなく、「ムーブ (移動)」するという Rust の挙動のため起きています。s1
は移動されてしまった抜け殻のようなものなので、もう使うことができなくなる。
ライフタイムと同様、メモリを正しく管理するための仕組みとなっている。
このムーブは、関数に値を渡したときにも発生する。
以下のようなコードもコンパイルエラーとなる。
fn print_str(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("Hello");
print_str(s); // s は、print_str へ ムーブする
print_str(s); // ↑で ムーブしてしまった抜け殻を使うことはできない!
}
6 | let s = String::from("Hello");
| - move occurs because `s` has type `String`, which does not implement the `Copy` trait
7 | print_str(s);
| - value moved here
8 | print_str(s);
| ^ value used here after move
これは、場合によっては面倒なことになる。
単純に値を使うだけの場合は 参照 だけすれば良い。ということで、参照型が役に立つ。
以下のコードは s
の値をムーブせずに、借用しているだけなので、エラーは起きない。
fn print_str(s: &String) {
println!("{}", s);
}
fn main() {
let s = String::from("Hello");
print_str(&s);
print_str(&s);
}
また、借用している最中は、値をムーブすることはできないという制約もある。
以下のコードは問題ない。
fn print_str(s: &String) {
println!("{}", s);
}
fn main() {
let s = String::from("Hello");
print_str(&s); //sを借ります
print_str(&s); //sをまた借ります
let j = s; //sをjにムーブします, sはここで終了
print_str(&j); //jを借ります, jはこれ以上つかわないので終了
}
以下のコードはダメ。
fn print_str(s: &String) {
println!("{}", s);
}
fn main() {
let s = String::from("Hello");
let ss: &String = &s; //sを借用して、ssへ
let j = s; //s は借りている最中なので、ムーブはできない
print_str(ss); //借用しているssを使う, sはここまで借りている
}
意外と先があるぞ。ということで、残り学ぶことに見通しをつける。
- 構造体
- 列挙型
- メソッド
- 標準ライブラリの列挙体 Result, Option について
- トレイト
一旦ここで区切って「基本編」は終わらせて、「競技プログラミング編」に進める。
Zenn は、スクラップをいい感じに畳んだり章を作ったりできなそうなので、別のスクラップを作ってノートまとめをすることにする。
- 標準入出力
- ファイル入出力
- テストと便利マクロ
- proconioクレート
- 問題を10問くらいとく
その次に、「Webアプリ開発編」を進める。ここはまだ未知数だが以下はやっておきたい。
(基本的にFrontendについては別に作る前提な、Webアプリケーションを作る)
- Cargoのパッケージ管理について
- actix-web
- diesel
- クリーンアーキテクチャの適用を考える
構造体
タプルのようにデータをまとめて表してもいいが、意味をもたせたい場合 struct
(構造体) を使うのが良い。
構造体の中の値は参照型であってはならないので、文字列は &str
ではなく、String
で取り扱う必要がある。
mut な変数で構造体を使うことによって、編集可能になる。
struct User {
name: String,
email: String,
age: u8,
}
fn main() {
let user1 = User {
name: String::from("Yamada Taro"),
email: String::from("yamada@example.com"),
age: 38
};
println!("Welcome, {}!", user1.name);
let mut user2 = User {
name: String::from("Tanaka Taro"),
email: String::from("takana@example.com"),
age: 32
};
user2.email = String::from("takana_taro@example.com");
println!("Welcome, {}!", user2.name);
}
以下のようなコードはエラーになる。Rust には、null
やら nil
のような怪しげなリテラルはない。
じゃあ、JSONの null
とかをRustの世界にもたらすときはどうするねん。という点については、列挙型 Option<T>
というのがあるらしい。また今度取り扱う。
struct User {
name: String,
email: String,
age: u8,
}
fn main() {
let user1 = User {
name: String::from("Yamada Taro"),
email: String::from("yamada@example.com"),
};
println!("Welcome, {}!", user1.name);
}
列挙体
取りうる値を列挙して管理したい場合は、列挙体を使う。match
や、 let if
などと合わせて使うと大変便利。
enum Direction {
Up,
Down,
Right,
Left,
}
fn move_to(step: i32, direction: Direction) {
match direction {
Direction::Up => println!("{}歩上へ行く", step),
Direction::Down => println!("{}歩上へ下へ行く", step),
Direction::Right => println!("{}歩右へ行く", step),
Direction::Left => println!("{}歩左へ行く", step),
};
}
fn main() {
let direction = Direction::Up;
// 30歩へ行くが出力される
move_to(30, direction);
}
列挙する各要素には、構造体と同様値を付加することができる。取り出すときは match
や if let
(パターンマッチ) を利用する必要がある。
enum Action {
Stop,
Move { x: i32, y: i32 },
Say(String, String)
}
fn main() {
let action1 = Action::Move {
x: 300,
y: 10
};
if let Action::Move {x : v1, y: v2} = action1 {
println!("{}", v1);
println!("{}", v2);
}
let action2 = Action::Say(String::from("Hello"), String::from("World"));
if let Action::Say(w1, w2) = action2 {
println!("{}", w1);
println!("{}", w2);
}
}
構造体でも、タプルをもたせるという形式は可能らしい。
struct Something(String, String);
fn main() {
let s = Something(String::from("hello"), String::from("world"));
println!("{}", s.0);
println!("{}", s.1);
}
それどころか、構造体は何も値を持たないというのもできる。
(以下は、if let やる必要あるの? というエラーが出るものの、コンパイルは通り、動くコード)
struct Something;
fn main() {
let s = Something;
if let Something = s {
println!("s is Something");
}
}
このように見ると、enum
は、struct
を同じような意味でまとめたものだなぁというのを感じることができた。
メソッド記法
構造体には、データだけでなく、関連する機能の情報も与えることができる。各メソッドの第1引数は、&self
となり、構造体自身の参照型となる。ちょっと python っぽい。
struct Cat {
name: String,
age: u8,
}
impl Cat {
fn mew(&self) {
println!("{}({}):「にゃーん」", self.name, self.age)
}
fn eat(&self, food: &str) {
println!("{}({}):{}を食べている", self.name, self.age, food)
}
}
fn main() {
let cat = Cat { name: String::from("うどん"), age: 2 };
// うどん(2):「にゃーん」
cat.mew();
// うどん(2):キャットフードを食べている
cat.eat("キャットフード");
}
もし、当該メソッドが、構造体の値を変更する場合は、&mut self
のような、可変参照型となる。これを使う場合は、構造体を作成するときにも同様に let mut
で定義する必要がある。
struct Cat {
name: String,
age: u8,
}
impl Cat {
fn age_increment(&mut self) {
self.age += 1
}
}
fn main() {
let mut cat = Cat { name: String::from("Udon"), age: 2 };
cat.age_increment();
// 3
println!("{}", cat.age);
}
仮に、let mut
にしない場合、以下のようなコンパイラエラーとなる。
|
13 | let cat = Cat { name: String::from("Udon"), age: 2 };
| --- help: consider changing this to be mutable: `mut cat`
14 | cat.age_increment();
| ^^^ cannot borrow as mutable
impl
は、enum
に使っても良いようだ。アレなポリモーフィズムの例っぽいコードになってしまった。
なお、ポリモーフィズム的なことを実現するなら、後々やるトレイトを使っても良さげである。
enum Animal {
Dog,
Cat,
}
impl Animal {
fn say(&self) {
match self {
Animal::Cat => println!("にゃーん"),
Animal::Dog => println!("わんわん")
}
}
}
fn main() {
let dog = Animal::Dog;
dog.say();
let cat = Animal::Cat;
cat.say();
}
なお、メソッドの引数に self
を入れない場合は、「関連関数」として定義できる。
Java でいう static method みたいなものかな。
struct Cat {
name: String,
age: u8,
}
impl Cat {
fn generate_any_cat() -> Cat {
return Cat { name: String::from("なまえはない"), age: 6 };
}
fn mew(&self) {
println!("{}({}):「にゃーん」", self.name, self.age)
}
}
fn main() {
let cat = Cat::generate_any_cat();
// なまえはない(6):「にゃーん」
cat.mew();
}
Option
Rust には、null
というリテラルはない。今どきっすね。変数を定義して、値を与えないということはできるが、それをやろうとすると、コード的に借用するときに、コンパイルエラーとなる。
fn main() {
let a: u8;
println!("{}", a);
}
|
3 | println!("{}", a);
| ^ use of possibly-uninitialized `a`
とはいえ、どうしても 「値がない」状況というのを作りたい場合は生まれてくる。そんなときに使うのが、Option
列挙体。
特に何も宣言しなくても利用することができる標準の機能である。
fn main() {
let mut a: Option<u8> = Option::None;
println!("{:?}", a);
a = Option::Some(30);
if let Option::Some(n) = a {
println!("{}", n);
}
}
以下のような定義がなされている。
enum Option<T> {
Some(T),
None,
}
なお、Some
, None
はそのまま利用することもできる。(標準 Option の中身が展開されているようだ。)
fn main() {
let mut a: Option<u8> = None;
println!("{:?}", a);
a = Some(30);
if let Some(n) = a {
println!("{}", n);
}
}
Result
関数の返り値が、諸般の事情で返せない場合はどうなるだろうか。
Ruby, Java, PHPとかだと、例外をスローするなどして、呼び出した側に責任をとらせるなども可能だが、Rust の場合は例外の機構はない。
Result
列挙体を使う。これも標準で使えるので、特に何か追加で記載する必要はない。
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
return Err(String::from("0で割ることはできません!!"));
}
Ok(a / b)
}
fn main() {
let result = divide(100, 0);
match result {
Ok(n) => println!("{}", n),
Err(m) => println!("{}", m)
}
}
panic! とエラーコード
なんだかの理由でしかたなくプログラムを止める場合は、panic!
マクロを利用する。
panic!
によりプログラムを強制停止させると、エラーコードは101
となる。
fn main() {
panic!("もうだめだ!!");
}
$ ./target/debug/hello_cargo
thread 'main' panicked at 'もうだめだ!!', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
$ echo $?
101
別のエラーコードを発したい場合は、std::process::exit(エラーコード)
を使うことでエラーを発し、プログラムを終了させることができる。
fn main() {
std::process::exit(1);
}
$ ./target/debug/hello_cargo
$ echo $?
1
トレイト
Rust には、JavaやRubyで言うところの「継承」に当たる概念はない。インターフェイスに当たる機能はあり、これを使う場合、実装を強制することができる。
仮に、以下のようなコードで、Dog
のために実装しようとしている、メソッドをなにか1つでも省略しようとすると、エラーとなる。
trait Animal {
fn name(&self) -> &String;
fn age(&self) -> u8;
fn say(&self);
fn eat(&self, food: String);
fn human_age(&self) -> u8;
}
struct Dog {
name: String,
age: u8,
}
impl Animal for Dog {
fn name(&self) -> &String {
&self.name
}
fn age(&self) -> u8 {
self.age
}
fn say(&self) {
println!("{}({}):「わんわん」", self.name(), self.age())
}
fn eat(&self, food: String) {
println!("{}({}):{}を食べている", self.name(), self.age(), food)
}
fn human_age(&self) -> u8 {
self.age() * 7
}
}
fn main() {
let d = Dog { name: String::from("そば"), age: 3 };
d.say();
}
なお、Javaで言うところの interface
とは違い、 trait にはメソッドを実装することができる。trait でメソッドを実装している場合、trait を利用するときにメソッドの定義を行わなくてもコンパイルが通る。
また、dyn
を利用することで、Animal
トレイトを使っている構造体について共通の処理を行うことができる。上記のコードを少し書き換える。
fn process(a: &dyn Animal) {
a.say();
a.eat(String::from("おにぎり"));
println!("{}は人間の年齢に換算すると{}歳です", a.name(), a.human_age())
}
fn main() {
let d = Dog { name: String::from("そば"), age: 3 };
process(&d);
}
基本編はこれにて終了
続けて、競技プログラミング編に続く