関数
関数の宣言はfn
を用います。引数がある場合はfn foo(bar: i32)
のように()
の中に引数名とその型をペアにして列挙します。
戻り値は-> <型>
の形で宣言します(戻り値が無い場合は省略できます)。
Rustの特徴として、関数の最後にreturn
を明示的に書かなくても値を返すことができます。Rustでは文末にセミコロンがないものを「式」と呼び、何らかの値に評価されます。逆に、セミコロンが付いている場合はそれが「文」となり、値に評価されません。関数やブロックの最後にセミコロンを付けない「式」を書くと、その値がその関数やブロックの戻り値として扱われます。そのため、関数の最後に戻り値を示す式(セミコロンを付けない形)を書くことで、return
を使う場合と同じように値を返せます(もちろん、return
文を明示的に使うことも可能です)。
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
let result = add(5, 3);
println!("The sum is: {}", result);
}
TypeScriptと違い、関数の戻り値は型推論されないので、明示的に指定する必要があります。
// `:number`が無くても暗黙的にnumberを返す関数と推論される
const multiply = (x: number, y: number) => {
return x * y;
};
// `-> i32`は省略できない
fn multiply(x: i32, y: i32) -> i32 {
x * y
}
スコープ
ブロック{}
内で宣言された変数は、そのブロックの中でしか利用できません。この有効範囲を「スコープ」と呼びます。
ある変数のスコープを抜けると、その変数のメモリは解放され、もうアクセスすることはできません。
fn main() {
{
let x = 42; // xはこのスコープ内でのみ有効
println!("x: {}", x);
}
// println!("x: {}", x); // エラー: xはスコープ外
}
引数のコピー
関数が引数を取る場合、その引数は型によってコピーされるかどうかが決まります。i32
やbool
のような型の引数の場合、コピーされて関数に渡されます。一方で、String
や独自の構造体のような型の引数の場合、コピーされません。
fn takes_ownership(s: String) {
println!("{}", s);
}
fn makes_copy(n: i32) {
println!("{}", n);
}
fn main() {
let s = String::from("hello");
takes_ownership(s); // sの所有権が関数に移動
// println!("{}", s); // エラー: sはもう使えない
let n = 42;
makes_copy(n); // nはコピーされる
println!("{}", n); // nはそのまま利用可能
}
クロージャ
クロージャは関数の一種で、定義外の変数を捕捉することが可能です。記法が通常の関数宣言と異なっており、()
の代わりに||
を利用します。ブロック部分も、式が一つだけでそれをそのまま戻り値として使うのであれば{}
も不要になります。
また、変数にバインディングすることができます。
fn main() {
let add = |a: i32, b: i32| a + b;
println!("5 + 3 = {}", add(5, 3));
let x = 10;
let print_x = || println!("Captured x: {}", x);
print_x();
}
クロージャに関しては強力な型推論が働きます。通常の関数宣言と異なり、型アノテーションは省略することができます(引数名は省略できません)。
fn main() {
let square = |n| n * n;
println!("Square of 4: {}", square(4));
}
このクロージャという仕組みはTypeScriptでいうアロー関数と似た使用感を持っています。
const add = (a: number, b: number) => a + b;
console.log(`5 + 3 = ${add(5, 3)}`);
筆者の感想ですが、アロー関数は単に記法の話で無名関数として使うことが多い一方、Rustでは明確に「スコープ外の変数を利用できる」という特徴を持たせているのが印象的です。
また、引数の型アノテーションまで不要なのに驚きました。
クロージャがスコープ外の変数を参照するとき、以下のどれかの方法で参照します。基本的には(イミュータブルな)参照を取得しようとし、場合に応じてミュータブルな参照もしくは値そのものを取得します。
値そのものを取得するとき、それがコピー可能であればコピーします。コピー不可能である場合、所有権そのものをクロージャに移動します。move
キーワードを使うと、所有権の移動を強制できます。
fn main() {
let mut x = 5;
let immut_closure = || println!("x: {}", x); // 共有参照
immut_closure();
let mut_closure = || {
x += 1; // 可変参照
println!("x after mutation: {}", x);
};
mut_closure();
let move_closure = move || {
println!("x moved: {}", x); // 所有権移動
};
move_closure();
}