はじめに
はじめまして、task4233です。
これはあくまでメモなので、基本的にググってわかるようなことは詳しく書きません。
よろしくお願いします。
なお、以下のプログラムでは面倒なのでfn main() {}
を省略することがあります。
ご承知おきください
おさらい
前回までは1-2章に目を通して、以下のことを学びました。
- Rustとはどのような言語なのか
- Rustプログラムの作成、コンパイルおよび実行
- Cargoの基本的な使い方
以下のリンクで参照できます。
https://qiita.com/task4233/items/84466b51adf42803c2a1
この記事の目的
Rust Tutorialの各章ごとでメモを残しておくことにより、一通り目を通した後に見返せるようにすることを目的としています。
そのため、各章の最初に何を目的とするか、最後に小さなまとめをメモしています。
なお、Rust Tutorialのリンクはこちらです。
https://doc.rust-jp.rs/the-rust-programming-language-ja/1.9/book/README.html
では書いていきます。
4. シンタックスとセマンティクス
Rustをボトムアップで学びたいなら、この章を順番に読んでいくのが近道です。
だそうです。
4.1. 変数束縛
変数束縛のミュータブルおよびイミュータブルを理解して扱えるようになること。
スコープ、シャドーイングの動きを理解して、コードを読めるようになること。
変数束縛
事実上全ての「Hello World」でないRustのプログラムは 変数束縛 を使っています。 変数束縛は何らかの値を名前へと束縛するので、後でその値を使えます。 このように、
let
が束縛を導入するのに使われています。
変数を変数名とリンクするということでしょうか?
いわゆる変数宣言のことですかね。
Rustでは変数宣言をするときに以下のように書くようです。
fn main() {
let x = 5;
}
パターン
Rustでは以下のように書くことで、xは1になりyは2になります。
let (x, y) = (1, 2);
この記法の左側は「パターン」であって、ただの変数名ではないとのことです。
「パターン」については4.15.で詳しく触れます。
型アノテーション
Rustは静的な型付言語であり、前もって型を与えておいて、それがコンパイル時に検査されます。
言い換えると、型を宣言しないとコンパイルエラーが吐かれるということです。
しかし、先ほどまでの例ではコンパイルが通りました。
なぜでしょう?
それは、Rustの「型推論」と呼ばれるものが働いているからです。
「型推論」とは、確定している部分から型が伝播されて、指定されていない部分の型も確定されるというものです(間違っていたらまさかり投げてください)。
「型推論」に関しては、先日のこの記事が参考になりました。
https://qiita.com/uint256_t/items/7d8c8feeffc03b388825
話を戻しますが、型を省略せずに書くと以下のようになります。
なお、理解のために以下推論する型をコメントで注釈します。
let x = 5; // x: i32
let x: i32 = 5;
この行を日本語で言い換えるならば「xは型i32を持つ束縛で、値は5である」となります。
ここで「i32」とは「符号付き型の32ビットサイズの整数型」を指します。
Rustには他にも「プリミティブ型」が存在し、4.3.で詳しく触れます。
可変性
デフォルトで、 束縛はイミュータブル(不変)です。
要するに、デフォルトでは一度束縛すると値を変更できません。
したがって、以下のようなコードはコンパイルエラーになります。
let x = 5;
x = 7;
エラー内容は以下の通りです。
error[E0384]: re-assignment of immutable variable `x`
--> main.rs:3:5
|
2 | let x = 5;
| - first assignment to `x`
3 | x = 7;
| ^^^^^ re-assignment of immutable variable
error: aborting due to previous error
re-assignment of immutable variable
、つまり「イミュータブルな変数の再定義」が問題でコンパイルエラーとなっています。
束縛をミュータブル(可変)にする際はmut
を使用して先ほどのコードを以下のように書けます。
let mut x = 5; // mut x:i32
x = 10;
束縛がデフォルトでイミュータブル(不変)である理由の1つは、変更するつもりのない変数を誤って変更してしまった際にコンパイラが指摘してくれるという利点があるからです。
また、変数宣言時にmut
が付いていれば、この変数はミュータブル(可変)なのだと一目で判断できます。
これらが、1章で触れたRustの「安全性」の一翼を担っているのかもしれません。
他にも理由があるようですが、省きます。
つよいひとに聞いてください。
束縛を初期化する
Rustでの束縛された変数は、値で初期化されている必要があります。
そのため、以下のようなコードはコンパイルエラーになります。
let x: i32;
println!("The value of x is: {}", x);
エラー内容は以下の通りです。
error[E0381]: use of possibly uninitialized variable: `x`
--> main.rs:3:39
|
3 | println!("The value of x is: {}", x);
| ^ use of possibly uninitialized `x`
error: aborting due to previous error
use of possibly uninitialized 'x'
、つまり「おそらく初期化されてないxの使用」が問題でコンパイルエラーとなっています。
言い忘れましたが、println!
は出力に使用され、"{}"
は後に来るx
で補完するという意味です。
なので、以下のようなコードにすることでコンパイルエラーが解消され、実行すると「The value of x is: 5」が表示されるはずです。
let x: i32 = 5;
println!("The value of x is: {}", x);
スコープ
変数束縛にはスコープがあり、定義されたブロック内のみで有効です。
したがって、以下のコードにおいて1つ目のprintln!
は動作しますが、2つ目のprintln!
は動作しません。
fn main() {
let x: i32 = 5;
{
let y: i32 = 7;
println!("The value of x is {} and value of y is {}", x, y);
}
println!("The value of x is {} and value of y is {}", x, y); // yにアクセスできないので動作しない
}
シャドーイング
変数束縛は、後に出てくる同じ名前の変数束縛で上書きされます。これをシャドーイングと呼びます。
シャドーイングの働きについては以下のコードが理解の助けになるでしょう。
let x: i32 = 5;
{
println!("{}", x); // "5"を表示する
let x = 7;
println!("{}", x); // "7"を表示する
}
println!("{}", x); // "5"を表示する
let x = 57;
println!("{}", x); // "57"を表示する
また、シャドーイングは異なる型の値の再束縛に対しても可能です。
let mut x: i32 = 5;
x = 7;
let x = x; // mut x: i32で値は7
let y = 4 // y: i32
let y = "I can also be bound to text!"; // y: String
イミュータブルな変数宣言は
let x = 5;
ミュータブルな変数宣言は
let mut y = 7;
のように書く。
ここで、
x = 7;
とするとコンパイルエラー、
y = 5
は問題ない。
スコープ、シャドーイングの動きは以下のコードのようになる。
let a: i32 = 5;
{
println!("{}", a); // 5を表示
let a = 7;
println!("{}", a); // 7を表示
let b: i32 = 7;
}
println!("{}", b); // bはスコープ外なのでコンパイルエラー
let a = "Other type"; // シャドーイング
4.2. 関数
関数の扱い方を知り、コード読めるようになること。
関数
fnは関数であることを示します。
したがって、いつも書いているmain関数も関数の1つな訳です。
返り値ナシの関数
まず、返り値ナシの関数から。
返り値ナシの関数は以下のような形で書きます。
fn [関数名]([引数]) {
// do something
}
基本的には他の言語と同様に関数名(引数)
のような形をとりますが、以下のコードのように引数を型指定しないとコンパイルエラーになります。
fn print_x(x) {
println!("{}", x);
}
fn main() {
let x: i32 = 5;
print_x(x);
}
このエラーは以下のように引数を型指定すると解消されます。
fn print_x(x: i32) {
println!("{}", x);
}
fn main() {
let x: i32 = 5;
println!("{}", x);
}
返り値アリの関数
次に返り値アリの関数です。
返り値アリの関数は以下のように書きます。
fn [関数名]([引数]) -> [返り値の型] {
// do something
}
型指定が必要なのは返り値ナシの関数と同じですが、返り値アリの関数は以下のコードのように -> [返り値の型]
を書く必要があります。
fn add_one(x: i32) -> i32 {
x + 1
}
ここで、add_one関数内の最後に;
が無い点に注意してください。
;
をつけてしまうとエラーが発生します。
そのため、私はRustの関数はマクロのように埋め込むという印象を持ちました。
式と文
Rustは主として式ベースの言語です。 文には2種類しかなく、その他の全ては式です。
とあります。
Rustでの式と文の違いは、値を返すか否かです。
式は値を返し、文は値を返しません。
これらを混同すると、コンパイルエラーを引き起こす原因の1つになります。
以下のようなコードではコンパイルエラーが起きます。
let x = (let y = 5);
これは、「let
は文であるため文の先頭にしかなれない」のでエラーが起きています。
早期リターン
関数内でreturn
を行うことで、値を先に返します。
以下のようなコードではreturn
以下は動きません。
fn func(x: i32) -> i32 {
return x;
x + 1 // このコードは動かない
}
fn main() {
let x: i32 = 5;
println!("{}", func(x));
}
発散する関数
値を返さない関数のための特別な構文として以下のような特別な構文が存在します。
ここで使用されているpanic!
はprintln!
と同様にマクロですが、この関数はクラッシュを引き起こすので値を返すことはありません。
fn diverges() -> ! {
panic!("This function never returns.")
}
fn main() {
diverges();
}
使い道がよくわかりませんが……
関数ポインタ
関数を指す変数束縛は以下のような形で書きます。
// 型推論ナシ
let [関数ポインタ名]: fn([引数の型]) -> [返り値の型] = [関数名];
// 型推論アリ
let [関数ポインタ名] = [関数名];
具体的には、先ほどのadd_one関数に対しては以下のように書くことができ、通常の関数と同様に使用できます。
fn add_one(x: i32) -> i32 {
x + 1
}
fn main() {
// 型推論ナシ
let f_with_type: fn(i32) -> i32 = add_one;
// 型推論アリ
let f_without_type = add_one;
let mut x: i32 = f_without_type(5);
println!("{}", x);
x = f_with_type(5);
println!("{}", x);
}
返り値ナシの関数は、
fn [関数名]([引数]) {
}
のように書き、
返り値アリの関数は、
fn [関数名]([引数]) -> [返り値の型] {
}
のように書く。
ここで、引数は型指定をしないとコンパイルエラーを引き起こす。
そして、関数の値を途中で返したい時はreturnを用いる。
また、関数ポインタを利用する時は、
let [関数ポインタ名]: fn([引数の型]) -> [返り値の型] = [関数名];
もしくは
let [関数ポインタ名] = [関数名];
のように書くことができ、関数と同様の振る舞いをする。
おわりに
4章 4.1-4.2では大まかに以下の2つが書かれていました。
- 変数宣言とその振る舞い
- 関数の書き方とその振る舞い
これで、変数宣言と関数を作ることが可能となりました。
次はプリミティブ型、コメント、ifを見ようと思います。