はじめに
The Rust Programming Language 日本語版の勉強のために、自分が大切だと思った箇所に装飾を付けて記録を残しました。
この記事の内容は、以下記事の引用です。
引用元 :
https://doc.rust-jp.rs/book-ja/ch01-00-getting-started.html
一般的なプログラミングの概念
この章では、ほとんど全てのプログラミング言語で見られる概念を講義し、それらがRustにおいてどう動作するのかを見ていきます。
多くのプログラミング言語は、その核心において、いろいろなものを共有しています。
この章で提示する概念は、全てRustに固有のものではありませんが、Rustの文脈で議論し、これらの概念を使用することにまつわる仕様を説明します。
変数と可変性
第二章で触れたとおり、変数は標準で不変になります。
これは、Rustが提供する安全性や簡便な並行性の利点を享受する形でコードを書くための選択の1つです。
ところが、まだ変数を可変にするという選択肢も残されています。
どのように、そしてなぜRustは不変性を推奨するのか、さらには、なぜそれとは違う道を選びたくなることがあるのかを見ていきます。
変数が不変であると、値が一旦名前に束縛されたら、その値を変えることができません。
これを具体的に説明するために、projectディレクトリに**「cargo new --bin variables」**コマンドを使って、valiablesという名前のプロジェクトを生成しましょう。
それから、新規作成したvariablesディレクトリで、src/main.rsファイルを開き、その中身を以下のコードに書き換えましょう。
▼ファイル名: src/main.rs
fn main() {
let x = 5;
println!("The value of x is: {}", x); // xの値は{}です
x = 6;
println!("The value of x is: {}", x);
}
これを保存し、cargo runコマンドでプログラムを走らせてください。
次の出力に示されているようなエラーメッセージを受け取るはずです。
error[E0384]: cannot assgin twice immutable variable `x`
(不変変数`x`に2回代入できません)
--> src/main.rs:4:5
|
2 | let x = 5;
| - first assignment to `x`
| (`x`への最初の代入)
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
この例では、コンパイラがプログラムに潜むエラーをも見つけ出す手助けをしてくれることが示されています。
コンパイルエラーはイライラする事もあるものですが、まだプログラムにしてほしいことを安全に行えてないだけということなのです。
このエラーは、エラーの原因が「不変変数xに2回代入できない」であると示しています。
不変なxという変数に別の値を代入しようとしたからです。
Rustでは、値が不変であると宣言したら、本当に変わらないことをコンパイラが担保してくれます。
つまり、コードを読み書きする際に、どこでどうやって値が変化しているかを追いかける必要がなくなります。
しかし、可変性は時として非常に有益なこともあります。
変数は標準でのみ不変です。
つまり、第二章のように変数名の前に「mut」キーワードをつけることで、可変にできるわけです。
この値が変化出来るようにするとともに、mutにより未来の読者に対してコードの別の部分がこの変数の値を変える可能性を示すことで、その意図を汲ませることができるのです。
例として、src/main.rsファイルを以下のように書き換えてください。
▼ファイル名: src/main.rs
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
今、このプログラムを走らせると、以下のような出力が得られます。
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
Running `target/debug/variables`
The value of x is: 5 (xの値は5です)
The value of x is: 6
mutキーワードが使われると、xが束縛している値を5から6に変更できます。
変数を可変にするほうが、不変変数だけがあるよりも書きやすくなるので、変数を可変にしたくなることもあるでしょう。
考えるべきトレードオフは、バグの予防以外にもいくつかあります。
例えば、大きなデータ構造を使う場合などです。
インスタンスを可変にして変更出来るようにするの方は、いちいちインスタンスをコピーして新しくメモリ割り当てされたインスタンスを返すよりも速くなります。
小規模なデータ構造なら、新規インスタンスを生成して、もっと関数型っぽいコードを書くほうが通して考えやすくなるため、低パフォーマンスは、その簡潔性を得るのに足りうるペナルティになるかもしれません。
変数と定数(constants)の違い
変数の値を変更できないようにするといえば、他の多くの言語も持っている別のプログラミング概念を思い浮かべるかもしれません。
定数です。
不変変数のように、定数は名前に束縛され、変更することが叶わない値のことですが、定数と変数の間にはいくつかの違いがあります。
まず、定数にはmutキーワードは使えません。
定数は標準で不変であるだけでなく、常に不変なのです。
定数はletキーワードの代わりにconstキーワードで宣言し、値の方は必ず注釈しなければなりません。
方と型注釈については、次のセクション「データ型」で講義しますので、その詳細について気にする必要はありません。
ただ単に型は常に注釈しなければならないのだと思っていてください。
定数はどんなスコープでも定義できます。
グローバルスコープも含めてです。
なので、いろんなところで使用される可能性のある値を定義するのに役立ちます。
最後の違いは、定数は定数式にしかセットできないということです。
関数呼び出し結果や、実行時に評価される値にはセットできません。
定数の名前が「MAX_POINTS」で、値が100,000にセットされた定数定義の例を御覧ください。
(Rustの定数の命名規則は、全て大文字でアンダースコアで単語区切りにすることです。)
const MAX_POINTS: u32 = 100_000;
定数は、プログラムが走る期間、定義されたスコープ内でずっと有効です。
したがって、プログラムのいろんなところで使用される可能性のあるアプリケーション空間の値を定義するのに有益な選択肢になります。
例えば、ゲームでプレイヤーが取得可能なポイントの最高値や光速度などですね。
プログラム中で使用される**ハードコード(外に出すべき処理や値をソースコード内に直接埋め込むこと)**された値に対して、定数として名前つけすることは、コードの将来的な管理者にとって値の意味を汲むのに役立ちます。
将来、ハードコードされた値を変える必要が出た時に、たった一箇所を変更するだけで済むようにしてくれます。
シャドーイング
第二章の数当てゲームのチュートリアルで見たように、前に定義した変数と同じ名前の変数を新しく宣言でき、新しい変数は、前の変数を覆い隠します。
これを最初の変数は2番目の変数に覆い隠されたと良い、この変数を使用した際に、2番目の変数の値が現れるということです。
以下のようにして、同じ変数名を用いて変数を覆い隠し、letキーワードの使用を繰り返します。
▼ファイル名: src/main.rs
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is: {}", x);
}
このプログラムはまず、xを5という値に束縛します。
それから「let x =」を繰り返すことでxを覆い隠し、元の値に1を加えることになるので、xは6になります。
3番目のlet文もxを覆い隠し、以前の値に2を掛けることになるので、xの最終的な値は12になりまう。
このプログラムを走らせたら、以下のように出力するでしょう。
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/variables`
The value of x is: 12
シャドーイングは、変数をmutにするのとは違います。
なぜなら、letキーワードを使わずに誤ってこの変数に再代入を試みようものなら、コンパイルエラーが出るからです。
letを使うことで値にちょっとした加工は行なえますが、その加工が終わったら、変数は不変になるわけです。
mutと上書きのもう一つの違いは、再度letキーワードを使用したら、実効的には新しい変数を生成していることになるので、値の型を変えつつ、同じ変数名を使い回せることです。
例えば、プログラムがユーザーに何らかのテキストに対して空白文字を入力することで何個分のスペースを表示したいかを訪ねますが、ただ実際にはこの入力を数値として保持したいとしましょう。
let spaces = " ";
let spaces = spaces.len();
この文法は、容認されます。
というのも、最初のspaces変数は文字列型であり、2番目のspaces変数はたまたま最初の変数と同じ名前になったまっさらな変数なのですが、数値型になるからです。
ゆえに、シャドーイングのおかげで、異なる名前を思いつく必要がなくなるわけです。
spaces_strとspaces_numなどですね。
代わりに、よりシンプルなspacesという名前を再利用出来るわけです。
一方で、この場合に「mut」を使おうとすると、以下に示したとおりですが、コンパイルエラーになります。
let mut spaces = " ";
spaces = spaces.len();
変数の型を可変にすることは許されていないと言われているからです。
error[E0308]: mismatched types (型が合いません)
--> src/main.rs:3:14
|
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected &str, found usize
| (&str型を予期しましたが、usizeが見つかりました)
|
= note: expected type `&str`
found type `usize`
さあ、変数が動作する方法を見てきたので、今度は変数が取りうるデータ型について見ていきましょう。
データ型
Rustにおける値は全て、何らかのデータ型であり、コンパイラがどんなデータが指定されているかを知れるので、そのデータの取り扱い方も把握できるというわけです。
2種類のデータ型のサブセット(全体の中の一部のこと)を見ましょう。
スカラー型と複合型です。
Rustは静的型付き言語であることを弁(わきま)えておいてください。
つまり、コンパイル時に全ての変数の型が判明している必要があるということです。
コンパイラは通常、値と使用方法に基づいて、使用したい型を推論してくれます。
複数の型が推論される可能性がある場合、例えば第二章の「予想と秘密の数字を比較する」節で、parseメソッドを使ってString型を数値型に変換したときのように、複数の型が可能な場合には、型注釈をつけなくてはいけません。
let guess: u32 = "42".parse().expect("Not a number!"); // 数字ではありません!
ここで型注釈をつけなければ、コンパイラは以下のエラーを表示し、これは可能性のある型のうち、どの型を使用したいのかを知るのにコンパイラがプログラマからもっと情報を得る必要があることを意味します。
error[E0282]: type annotations needed
(型注釈が必要です)
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ cannot infer type for `_`
| (`_`の型が推論できません)
|
= note: type annotations or generic parameter binding required
(注釈: 型注釈、またはジェネリクス引数束縛が必要です)
他のデータ型についても、様々な型注釈を目にすることになるでしょう。
スカラー型
スカラー型は、単独の値を表します。
Rustには主に4つのスカラー型があります。
整数、浮動小数点数、論理値、最後に文字です。
他のプログラミング言語でも、これらの型を見かけたことはあるでしょう。
Rustでの動作方法に飛び込みましょう。
整数型
整数とは、小数部分のない数値のことです。
第二章で1つ整数型を使用しましたね。
u32型です。
この型定義は、紐付けられる値が符号なし整数(符号付き整数はuではなくiで始まる)になり、これは32ビット分のサイズを取ります。
表3-1は、Rustの組み込み整数型を表示しています。
符号付きと符号なし欄の各バリアント(変型)を使用して、整数値の型を宣言することができます。
各バリアントは、符号付きか符号なしかを選べ、明示的なサイズを持ちます。
符号付きと符号なしは、数値が正負を持つかどうかを示します。
つまり、数値が符号を持つ必要があるかどうか、または絶対に正数しかならず符号無しで表現できるかどうかです。
正数リテラル(リテラル:見たままの値)は、表3-2に示すどの形式でも記述することができます。
バイトリテラルを除く数値リテラルは全て、 型接尾辞(例えば、57u8)と_を見た目の区切り記号(例えば、1_000)に付加することができます。
(例)
1_000 は1,000、
0.000_001 は0.000001とそれぞれ同一。
では、どの整数型を使うべきかはどう把握すればいいのでしょうか?
もし確信が持てないのならば、Rustの標準型は一般的にいい選択肢になります。
整数型の標準は**「i32型(iは符号付き)」**です。
64ビットシステム上でも、この型が普通最速になります。
isizeとusizeを使う主な状況は、何らかのコレクションにアクセスすることです。
浮動小数点型
Rustにはさらに浮動小数点数に対しても、2種類の基本型があり、浮動小数点数とは数値に小数点がついたもののことです。
Rustの浮動小数点型は、「f32」と「f64」で、それぞれ32ビットと64ビットサイズです。
**基準型は「f64」**です。
なぜなら、現在のCPUでは、「f32」とほぼ同スピードにも関わらず、より精度が高くなるからです。
実際に動作している浮動小数点数の例を御覧ください。
▼ファイル名: src/main.rs
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮動小数点数は、IEEE-754規格に従って表現されています。
「f32」が単精度浮動小数点数、「f64」が倍精度浮動小数点数です。
数値演算
Rustにも全数値型に期待されうる標準的な数学演算が用意されています。
足し算、引き算、掛け算、割り算、余りです。
▼ファイル名: src/main.rs
fn main() {
// 足し算
let sum = 5 + 10;
// 引き算
let difference = 95.5 - 4.3;
// 掛け算
let product = 4 * 30;
// 割り算
let quotient = 56.7 / 32.2;
// 余り
let remainder = 43 % 5;
}
これらの文の各式は、数学演算子を使用しており、一つの値に評価され、そして
変数に束縛されます。
論理値型
他の多くの言語同様、Rustの論理値型も取りうる値は2つしかありません。
**「true」と「false」**です。
Rustの論理値型は、「bool」と指定されます。
▼ファイル名: src/main.rs
fn main() {
let t = true;
let f: bool = false; // 明示的型注釈付きで
}
論理値を使う手段は、条件式です。
if式などですね。
文字型
ここまで数値型のみ扱ってきましたが、Rustには文字も用意されています。
Rustの「char」型は、言語の最も基本的なアルファベット型であり、以下のコードでその使用方法の一例を見ることができます。
「char」はダブルクォーテーションマークを使用する文字列に対して、シングルクォートで指定される」ことに注意してください。
▼ファイル名: src/main.rs
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻'; //ハート目の猫
}
Rustのchar型は、**ユニコードのスカラー型(スカラー型:単独の値)**を表します。
これはつまり、アスキーよりもずっとたくさんのものを表せるということです。
アクセント文字(中国語、日本語、韓国語文字、絵文字、ゼロ幅スペース)は全てRustでは、有効なchar型」になります。
ところが、「文字」は実はユニコードの概念にはないので、文字とは何かという人間としての直感は、Rustにおけるchar値が何かとは合致しない可能性があります。(第8章で詳しく議論しましょう)
複合型
複合型により、複数の値を1つの型にまとめることができます。
Rustには、2種類の基本的な複合型があります。
タプルと配列です。
タプル型(JSで言うオブジェクトみたい)
タプルは、複数の型の何らかの値を1つの複合型にまとめ上げる一般的な手段です。
タプルは、丸括弧の中にカンマ区切りの値リストを書くことで生成します。
タプルの位置ごとに型があり、タプル内の値はそれぞれ全てが同じ型である必要はありません。
今回の例では、型注釈をあえて追加しました。
▼ファイル名: src/main.rs
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
変数tupは、タプル全体に束縛されています。
なぜなら、タプルは1つの複合要素と考えられるからです。
タプルからここの値を取り出すには、パターンマッチングを使用して分解することができます。
▼ファイル名: src/main.rs
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {}", y);
}
このプログラムは、まずタプルを生成し、それを変数tupに束縛しています。
それからletとパターンを使って、tup変数の中身を3つの個別の変数(x,y,z)に変換しています。
この過程は、分配と呼ばれます。
単独のタプルを破壊して3分割しているからです。
最後に、プログラムはy変数の値を出力し、6.4と表示されます。
パターンマッチングを通しての分配の他にも、アクセスしたい値の番号をピリオド(.)に続けて書くことで、タプルの要素に直接アクセスすることもできます。
▼ファイル名: src/main.rs
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
このプログラムは、新しいタプルxを作成し、添字アクセスで各要素に対して新しい変数を作成しています。
多くのプログラム言語同様、タプルの最初の添字は0です。
配列型
配列によっても、複数の型のコレクションを得ることができます。
タプルと異なり、配列の全要素は同じ型でなくてはなりません。
Rustの配列は、他の言語と子っとなっています。
Rustの配列は、固定長なのです。
一度宣言されたら、サイズを伸ばすことも縮めることもできません。
Rustでは、配列に入れる要素は、角かっこ内にカンマ区切りリストとして記述します。
▼ファイル名: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
}
配列は**ヒープ(先出し先入れ)よりもスタック(後入れ先出し)**にデータのメモリを確保したい時、または常に固定長の要素があることを確認したい時に有効です。(スタックとヒープについては第4章で詳らか(つまびらか)議論します)
ただ、配列はベクタ型(JSで言う可変長の配列)ほど柔軟ではありません。
ベクタ型は、標準ライブラリによって提供されている配列と似たようなコレクション型で、
こちらはサイズを伸縮させることができます。
配列とベクタ型、どちらを使うべきか確信が持てないときは、おそらくベクタ型を使うべきです。
ベクタ型よりも配列を使いたくなるかもしれない例は、1年の月の名前を扱うプログラムです。
そのようなプログラムで、月を追加したり削除したりすることはまずないので、配列を使用できます。
常に12個要素があることもわかってますからね。
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
## 配列の要素にアクセスする
配列は、スタック上に確保されるひとかたまりのメモリです。
添字によって、配列の要素にこのようにアクセスできます。
▼ファイル名: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
この例では、firstという名前の変数には1という値が格納されます。
配列の[0]番目にある値がそれだからです。
secondという名前の変数には、配列の[1]番目の値2が格納されます。
配列要素への無効なアクセス
配列の終端(しゅうたん)を超えて要素にアクセスしようとしたら、どうなるでしょうか?
先程の例を以下のように変えたとすると、コンパイルは通りますが、実行するとエラーで終了します。
▼ファイル名: src/main.rs
fn main() {
let a = [1, 2, 3, 4, 5];
let index = 10;
let element = a[index];
println!("The value of element is: {}", element); // 要素の値は{}です
}
このコードをcargo runで走らせると、以下のような結果になります。
$ cargo run
Compiling arrays v0.1.0 (file:///projects/arrays)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/arrays`
thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
10', src/main.rs:6
スレッド'<main>'は'範囲外アクセス: 長さは5ですが、添え字は10でした', src/main.rs:6
でパニックしました
note: Run with `RUST_BACKTRACE=1` for a backtrace.
コンパイルでは何もエラーが出なかったものの、プログラムは実行時エラーに陥り、
正常終了しませんでした。
要素に添え字アクセスを試みると、言語は指定されたその添字が配列長よりも小さいかを確認してくれます。
添字が配列長よりも大きければ、言語はパニックします。
パニックとは、プログラムがエラーで終了したことを表すRust用語です。
これは実際に稼働しているRustの安全機構の最初の例になります。
低レベルの言語の多くでは、この種のチェックは行われないため、間違った添字を与えると、無効なメモリにアクセスできます。
Rustでは、メモリアクセスを許可し、処理を継続する代わりに即座にプログラムを終了することで、この主のエラーからプログラマを保護しています。
関数
関数は、Rustのコードにおいてよく見かける存在です。
すでに言語において最も重要な関数のうちの1つを目撃していますね。
そう、main関数です。
これは、多くのプログラムのエントリーポイント(プログラムの実行時に最初に走る関数)になります。
fnキーワードで、新しい関数を宣言することができます。
Rustの関数と変数の命名規則は**スネークケース(_でつなぐ)**を使うのが慣例です。
スネークケースとは、全文字を小文字にし、単語区切りにアンダースコアを使うことです。
▼ファイル名: src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function."); // 別の関数
}
Rustにおいて関数定義は、fnキーワードで始まり、関数名の後ろに丸括弧の組が続きます。
波括弧がコンパイラに関数本体の開始と終了の位置を伝えます。
定義した関数は、名前に丸括弧の組を続けることで呼び出すことができます。
another_function関数がプログラム内で定義されているので、main関数内から呼び出すことができるわけです。
ソースコードの中でanother_functionをmain関数のあとに定義していることに注目してください。
もちろん、main関数の前に定義することもできます。
コンパイラは、関数がどこで定義されているかは気にしません。
どこかで定義されていることのみ気にします。
functionsという名前の新しいバイナリ生成プロジェクトを始めて、関数について更に深く探究していきましょう。
another_functionの例をsrc/main.rsファイルに配置して、走らせてください。
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.28 secs
Running `target/debug/functions`
Hello, world!
Another function.
行出力は、main関数内に書かれた順序で実行されています。
最初に"Hello, world!"メッセージが出て、それからanother_functionが呼ばれています。
関数の引数
関数は、引数を持つようにも定義できます。
引数とは、関数シグニチャの一部になる特別な変数のことです。
関数に引数があると、引数の位置に実際の値を与えることができます。
技術的にはこの実際の値は実引数と呼ばれますが、普段の会話では、仮引数と実引数を関数定義の変数と関数呼び出しの引数に渡す実際の値、両方の意味に区別なく使います。
以下の書き直したanother_functionでは、Rustの仮引数がどのようなものかを示しています。
▼ファイル名: src/main.rs
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {}", x); // xの値は{}です
}
このプログラムを走らせてみてください。
以下のような出力が得られるはずです。
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21 secs
Running `target/debug/functions`
The value of x is: 5
another_functionの宣言には、xという名前の仮引数があります。
xの値は、i32と指定されています。
値5がanother_functionに渡されると、println!マクロにより、フォーマット文字列中の1組の波括弧があった位置に値5が出力されます。
関数シグニチャにおいて、各仮引数型の型を宣言しなければなりません。
これは、Rustの設計において、意図的な判断です。
関数定義で型注釈が必要不可欠ということは、コンパイラがその意図するところを推し量るのに、プログラマがコードの他の箇所で使用する必要がないということを意味します。
関数に複数の仮引数を持たせたいときは、仮引数定義をカンマで区切ってください。
▼ファイル名: src/main.rs
fn main() {
another_function(5, 6);
}
fn another_function(x: i32, y: i32) {
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
この例では、2引数の関数を生成しています。
そして、引数はどちらもi32型です。
それからこの関数は、仮引数の値を両方出力します。
関数引数は、全てが同じ型である必要はありません。
関数本体は、文と式を含む
関数本体は、文が並び、最後に式を置くか文を置くという形で形成されます。
現在までには、式で終わらない関数だけを見てきたわけですが、式が文の一部になっているものなら見かけましたね。
Rustは式思考言語なので、これは理解しておくべき重要な際になります。
他の言語にこの差異はありませんので、式と文がなんなのか、その違いが関数本体にどんな影響を与えるかを見ていきましょう。
実のところ、もう文と式は使っています。
文とは、何らかの動作をして値を返さない命令です。
式は、結果値に評価されます。
letキーワードを使用して変数を生成し、値を代入することは文になります。
▼ファイル名: src/main.rs
// let y = 6は「文」
fn main() {
let y = 6;
}
関数定義も文になります。
つまり、上の例は全体としても文になるわけです。
文は値を返しません。
ゆえに、let文を他の変数に代入することはできません。
以下のコードではそれを試みていますが、エラーになります。
▼ファイル名: src/main.rs
fn main() {
let x = (let y = 6);
}
このプログラムを実行すると、以下のようなエラーが出るでしょう。
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found statement (`let`)
(エラー: 式を予期しましたが、文が見つかりました (`let`))
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: variable declaration using `let` is a statement
(注釈: `let`を使う変数宣言は、文です)
この「let y = 6」という文は値を返さないので、xに束縛するものがないわけです。
式はなにかに評価され、これからあなたが書くRustコードの多くを構成します。
簡単な数学演算(5+6など)を思い浮かべましょう。
この例は値11に評価される四季です。
式は文の一部になりえます。
「let y = 6」という文の6は値6に評価される式です。
関数呼び出しも式です。
マクロ呼び出しも式です。
新しいスコープを作る際に使用するブロック({})も式です。
▼ファイル名: src/main.rs
fn main() {
let x = 5;
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
}
以下の式:
{
let x = 3;
// 式は終端にセミコロンを含まない
x + 1
}
上記は今回の場合、4に評価されるブロックです。
その値がlet文の一部として、yに束縛されています。
今まで見かけてきた行と異なり、文末にセミコロンがついていないx+1の行に気をつけてください。
式は終端にセミコロンを含みません。
式の終端にセミコロンを付けたら、文に変えてしまいます。
そして、文は値を返しません。
次に関数の戻り値や式を見ていく際にこのことを肝に銘じておいてください。
戻り値のある関数
関数は、それを呼び出したコードに値を返すことができます。
戻り値似名前をつけはしませんが、矢印(->)の後に型を書いて確かに宣言します。
Rustでは、関数の戻り値は関数本体のブロックの最後の式の値と同義です。
returnキーワードで関数から早期リターンし、値を指定することもできますが、多くの関数は最後の式を暗黙的に返します。
▼ファイル名: src/main.rs
fn five() -> i32 {
// 式は終端にセミコロンを含まない。
5
}
fn main() {
let x = five();
println!("The value of x is: {}", x);
}
five関数の中には、関数呼び出しもマクロ呼び出しも、let文でさえ存在しません。
数字の5が単独であるだけです。
これはRustにおいて、完璧に問題ない関数です。
関数の戻り値の型が「-> i32」と指定されていることにも注目してください。
five内の5が関数の戻り値です。
だから、戻り値型が「i32」なのです。
これについてもっと深く考察しましょう。
重要な箇所は2つあります。
**「let x = five()」**という行は、関数の戻り値を使って変数を初期化していることを示しています。
関数fiveは5を返すので、この行は以下のように書くのと同義です。
let x = 5;
2番目にfive関数は仮引数をもたず、戻り値を定義していますが、関数本体はセミコロンなしの5単独です。
なぜなら、これが返したい値になる式だからです。
もう一つ別の例を見ましょう。
▼ファイル名: src/main.rs
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1
}
このコードを走らせると、The value of x is:6と出力されるでしょう。
しかし、x+1を含む終端にセミコロンをつけて、式から文に変えたら、エラーになるでしょう。
▼ファイル名: src/main.rs
// エラー
fn main() {
let x = plus_one(5);
println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
このコードを実行すると、以下のようにエラーが出ます。
error[E0308]: mismatched types
(型が合いません)
--> src/main.rs:7:28
|
7 | fn plus_one(x: i32) -> i32 {
| ____________________________^
8 | | x + 1;
| | - help: consider removing this semicolon
9 | | }
| |_^ expected i32, found ()
| (i32を予期したのに、()型が見つかりました)
|
= note: expected type `i32`
found type `()`
メインのエラーメッセージである「型が合いません」でこのコードの根本的な問題が明らかになるでしょう。
関数plus_oneの定義では、i32型を返すと言っているのに、文は値に評価されないからです。
このことは**()、つまり空のタプル**として表現されています。
それゆえに、何も戻り値がなく、これが関数定義と矛盾するので、結果としてエラーになります。
この出力内で、コンパイラは問題を修正する手助けになりそうなメッセージも出しています。
コメント
全プログラマは、自分のコードがわかりやすくなるよう努めますが、時として追加の説明が許されることもあります。
このような場合、プログラマは注釈またはコメントをソースコードに残し、コメントをコンパイラは無視しますが、ソースコードを読む人間には有益なものと思えるでしょう。
こちらが単純なコメントです。
// hello, world
コメントが複数行にまたがる場合、各行に//を含める必要があります。
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.
// ここで何か複雑なことをしていて、長すぎるから複数行のコメントが必要なんだ。
// ふう!願わくば、このコメントで何が起きているか説明されていると嬉しい。
コメントは、コードが書かれた行の末尾にも配置することができます。
fn main() {
let lucky_number = 7; // I’m feeling lucky today(今日はラッキーな気がするよ)
}
フロー制御
条件が真かどうかによってコードを走らせるかどうかを決定したり、条件が真の間繰り返しコードを走らせるか決定したりすることは、多くのプログラミング言語において、基本的な構成ブロックです。
Rustコードの実行フローを制御する最も一般的な文法要素は、if式とループです。
if式
if式によって、条件に依存して枝分かれをさせることができます。
条件を与え、以下のように宣言します。
projectディレクトリにbranchesという名のプロジェクトを作ってif式について掘り下げていきましょう。
src/main.rsファイルに、以下のように入力してください。
▼ファイル名: src/main.rs
fn main() {
let number = 3;
if number < 5 {
println!("condition was true"); // 条件は真でした
} else {
println!("condition was false"); // 条件は偽でした
}
}
また、コード内の条件式は、bool型でなければならないことにも触れる価値があります。
条件式がbool型出ないときは、エラーになります。
// エラーになる
fn main() {
let number = 3;
if number {
println!("number was three"); // 数値は3です
}
}
今回、ifの条件式は3という値に評価され、コンパイラがエラーを投げます。
error[E0308]: mismatched types
(型が合いません)
--> src/main.rs:4:8
|
4 | if number {
| ^^^^^^ expected bool, found integral variable
| (bool型を予期したのに、整数変数が見つかりました)
|
= note: expected type `bool`
found type `{integer}`
このエラーは、コンパイラはbool型を予期していたのに、正数だったことを示唆しています。
Javasctiptなどの言語とは異なり、Rustでは論理値以外の値が自動的に論理値に変換されることはありません。
明示し、必ずifには条件式として、論理値を与えなければなりません。
例えば、数値が0以外のときだけifのコードを走らせたいなら、以下のようにif式を変更することができます。
▼ファイル名: src/main.rs
fn main() {
let number = 3;
if number != 0 {
println!("number was something other than zero"); // 数値は0以外の何かです
}
}
このコードを実行したら、number was something other than zeroと表示されるでしょう。
else ifで複数の条件を扱う
ifとelseを組み合わせて、else if式にすることで複数の条件をもたせることもできます。
▼ファイル名: src/main.rs
fn main() {
let number = 6;
if number % 4 == 0 {
// 数値は4で割り切れます
println!("number is divisible by 4");
} else if number % 3 == 0 {
// 数値は3で割り切れます
println!("number is divisible by 3");
} else if number % 2 == 0 {
// 数値は2で割り切れます
println!("number is divisible by 2");
} else {
// 数値は4、3、2で割り切れません
println!("number is not divisible by 4, 3, or 2");
}
}
このプログラムには通り道が4つあります。
実行後、以下のような出力を目の当たりにするはずです。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
Running `target/debug/branches`
number is divisible by 3
Rustは最初の真条件のブロックのみを実行し、 条件に合ったものが見つかったら、残りはチェックすらしません。
let文内でif式を使う
ifは式なので、let文の右辺に持ってくることができます。
▼ファイル名: src/main.rs
fn main() {
let condition = true;
let number = if condition {
5
} else {
6
};
// numberの値は、{}です
println!("The value of number is: {}", number);
}
このnumber変数は、if式の結果に基づいた値に束縛されます。
$ cargo run
Compiling branches v0.1.0 (file:///projects/branches)
Finished dev [unoptimized + debuginfo] target(s) in 0.30 secs
Running `target/debug/branches`
The value of number is: 5
一連のコードは、そのうちの最後の式に評価され、数値はそれ単独でも式になることを思い出してください。
この場合、このif式全体の値は、どのブロックコードが実行されるかに基づきます。
これはつまり、ifの各アームの結果になる可能性がある値は、同じ型でなければならないということになります。
ifアームもelseアームも結果は、
i32の正数でした。
以下の例のように、型が合わないときにはエラーになるでしょう。
▼ファイル名: src/main.rs
fn main() {
let condition = true;
let number = if condition {
5
} else {
"six"
};
println!("The value of number is: {}", number);
}
このコードをコンパイルしようとすると、エラーになります。
ifとelseアームは互換性のない値の型になり、コンパイラがプログラム内で問題の見つかった箇所をズバリ指摘してくれます。
error[E0308]: if and else have incompatible types
(ifとelseの型に互換性がありません)
--> src/main.rs:4:18
|
4 | let number = if condition {
| __________________^
5 | | 5
6 | | } else {
7 | | "six"
8 | | };
| |_____^ expected integral variable, found &str
| (整数変数を予期しましたが、&strが見つかりました)
|
= note: expected type `{integer}`
found type `&str`
ループでの繰り返し
一連のコードを1回以上実行できると、しばしば役に立ちます。
この作業用に、Rustにはいくつかのループが用意されています。
ループは本体内のコードを最後まで実行し、直後にまた最初から処理を開始します。
Rustには3種類のループが存在します。
loopとwhileとforです。
loopでコードを繰り返す。
loopキーワードを使用すると、同じコードを何回も何回も永遠に、明示的にやめさせるまで実行します。
fn main() {
loop {
println!("again!"); // また
}
}
このプログラムを実行すると、プログラムを手動で止めるまで、何度も何度も続けてagain!と出力するでしょう。
ほとんどの端末でctrl + cというショートカットが使え、永久ループにとらわれてしまったプログラムを終了させられます。
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.29 secs
Running `target/debug/loops`
again!
again!
again!
again!
^Cagain!
^Cという記号が出た場所が、ctrl-cを押した場所です。
幸いなことに、Rustにはループを抜け出す別のより信頼できる手段があります。
ループ内にbreakキーワードを配置することで、プログラムに実行を終了すべきタイミングを教えることができます。
whileで条件付きループ
プログラムにとってループ内で条件式を評価できると、有益なことがしばしばあります。
条件が真の間、ループが走るわけです。
条件が真でなくなった時にプログラムはbreakを呼び出し、ループを終了します。
このタイプのループは、loop,if,else,breakを組み合わせることでも実装できます。
しかし、このパターンは頻出するので、Rustにはそれ用の文法要素が用意されていて、whileループと呼ばれます。
▼ファイル名: src/main.rs
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number = number - 1;
}
// 発射!
println!("LIFTOFF!!!");
}
この文法要素により、loop,if,else,breakを使った時に必要になるネストがなくなり、より明確になります。
条件が真の間、コードは実行されます。
そうでなければ、ループを抜けます。
forでコレクションを覗き見る
while要素を使って配列などのコレクションの要素を覗き見ることができます。
▼ファイル名: src/main.rs
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;
while index < 5 {
// 値は{}です
println!("the value is: {}", a[index]);
index = index + 1;
}
}
ここで、コードは配列の要素を順番にカウントアップして覗いています。
番号0から始まり、配列の最終番号に到達するまでループします。(つまり、index < 5が真でなくなる時です)
このコードを走らせると、配列内の全要素が出力されます。
$ cargo run
Compiling loops v0.1.0 (file:///projects/loops)
Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs
Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50
予想通り、配列の5つの要素が全てターミナルに出力されています。
index変数の値はどこかで5という値になるものの、配列から6番目の値を拾おうとする前にループは実行を終了します。
しかし、このアプローチは間違いが発生しやすいです。
添字の長さが間違っていれば、プログラムはパニックしてしまいます。
また、遅いです。
コンパイラが実行時にループの各回ごとに境界値チェックを行うようなコードを追加するからです。
より効率的な対立案として、forループを使ってコレクションの各アイテムに対してコードを実行することができます。
▼ファイル名: src/main.rs
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
// 値は{}です
println!("the value is: {}", element);
}
}
※JSでは、for inがオブジェクト、for ofが配列。
このコードを走らせたら、while文と同じ出力が得られるでしょう。より重要なのは、コードの安全性を向上させ、配列の終端を超えてアクセスしたり、終端に届く前にループを終えてアイテムを見逃してしまったりするバグの可能性を完全に排除したことです。
forループのこの安全性と簡潔性により、Rustで使用頻度の最も高いループになっています。
whileループを使ったカウントダウンサンプルのように、一定の階数、同じコードを実行したいような状況であっても、forループを使う人のほうが多いです。
どうやってやるかといえば、Range型を使います。
Range型は、標準ライブラリで提供される片方の数字から始まって、もう片方の数字【未満】の数値を順番に生成する型です。
forループと、まだ話していない別のメソッドrevを使って範囲を逆順にしたカウントダウンはこうなります。
▼ファイル名: src/main.rs
fn main() {
// .rev()で逆順にすることができる。(4,3,2,1)
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
まとめ
変数、スカラー値(単独の値)、複合データ型(複数の値を1つの型にまとめる)、関数、コメント、if式、そしてループについて学びました。