最速で知る! プログラミング言語Rustの基本機能とメモリ管理【第二言語としてのRust】

Rustは、新しいシステムプログラミング言語です。本稿では、基本的な構文に加えて、所有権、参照と借用、ライフタイムといった特徴的な機能によるメモリ管理を解説します。

最速で知る! プログラミング言語Rustの基本機能とメモリ管理【第二言語としてのRust】

κeen@blackenedgoldです。Rustの入門を担当することになりました。基本的な文法と使い方を説明しつつ、Rustの特徴的な機能と、なぜその機能が必要かというモチベーションを紹介していけたらと思います。

Rustは非常に高機能であり、この記事ですべてを紹介できません。興味を持った方は、ぜひ公式ドキュメントを読んでみてください。私が管理している和訳もあります。

Rustはシステムプログラミング言語

Rustは、たとえば次のような、さまざまな用途で採用されています。

また、C ABI(Application Binary Interface)互換性を生かしたRubyNodeのNative Extension(Module)の作成や、最小限のランタイムを生かしてWebAssemblyへコンパイルしてWebブラウザで動かすといった形でも利用されています。

これらの利用例からわかるように、Rustは比較的新しいシステムプログラミング言語だといえます。そのため、無駄のなさや、ユーザが細かいところまで制御できることが主眼となっています。

その特徴を、公式サイトから引用してみましょう。

  • ゼロコスト抽象化
  • ムーブセマンティクス
  • 保証されたメモリ安全性
  • データ競合のないスレッド
  • トレイトによるジェネリクス
  • パターンマッチング
  • 型推論
  • 最小限のランタイム
  • 効率的なCバインディング

見慣れない用語もあると思いますが、この記事を読み終わるころには、これらのRustの特徴について概要が理解できるようになるはずです。

もう一つ、公式サイトでは特に明記されていない点ですが、Rustは多くのことをコンパイル時に静的に解決する言語で、

  • ランタイムのGCがないものの、コンパイル時解析のおかげで自動メモリ管理ができる
  • マルチスレッドプログラミングで悩ましいデータ競合(競合状態ではない)をコンパイル時に防げる
  • ポリモーフィズムをコンパイル時に解決できる

などの特徴があり、すべてを実行時に解決する動的な言語とは対極にあります。

よく、「プログラマは軽量級言語と重量級言語を1つずつくらい使えるようになっていれば困らない」などと言われることがあります。第一言語として動的な言語を使ってきた方は、静的な世界に飛び込んでみるという意味で、Rustを第二言語に選んでみるのもよいのではないでしょうか。

Rustのインストール

Rustコンパイラのインストールは簡単です。

公式ページの「インストール · プログラミング言語Rust」に従えば、コンパイラであるrustcと、ビルドツールであるcargoがインストールできます。

そのほか、racerrustfmtなどのツールもあると便利ですが、この記事を読むにあたってはコンパイラとビルドツールがあれば十分です。ツールの環境構築についてはここでは触れないので、Web上の情報などを参考にセットアップしてみてください。

この記事のソースコードは、執筆時点での最新版である1.16.0で動作を確認しています。

Hello World

さっそくRustのコードを書いてみましょう。最初はもちろん「Hello, World」です。

次のコードを、hello.rsという名前で保存してください。

fn main() {
  println!("Hello, World");
}

このRustコードをコンパイルするには、次のようにrustcコマンドを実行します。

$ rustc hello.rs

これで、同じディレクトリ内にhelloという実行可能なバイナリファイルができているはずです。

このhelloをシェルから実行すれば、Hello, Worldと表示されます。

$ ./hello
Hello, World
$

これで皆さんもRustプログラマ(Rustaceanと呼ばれます)の仲間入りですね!

それでは、hello.rsとして保存したコードを解説していきましょう。

Rustでは、コンパイル後の実行ファイルは、mainという名前で定義されている関数から実行が開始されることになっています。そのため、このコードでもmain関数を定義しています。

Rustでの関数定義は、一般的には次のような形になります。

fn 関数名(引数) -> 返り値の型 {
    関数本体
}

返り値が何もない場合には、-> 返り値の型は省略できて、次のように書けます。

fn 関数名(引数) {
    関数本体
}

hello.rsmainは返り値が不要なので、この形の関数定義を使っていました(なお、返り値がない関数の型はUnit型といいます。Unit型については後ほど説明します)

関数定義の本体は、println!("Hello, World");ですね。

文字列の出力に利用しているprintln!は、関数とは少し違ったマクロの呼び出しです(Rustでは、末尾に!がついているとマクロの呼び出しになります)。マクロについてはとりあえず気にせず、関数のようなものを呼び出していると思っておいてください。

FizzBuzz

次は、ループと条件分岐ifが出てくる例を見てみましょう。題材は、FizzBuzzです。

FizzBuzzにはいくつも書き方がありますが、一番愚直な方法でRustで書いてみます。

// '//' 以降はコメントとして扱われる。

// 関数の引数は`(変数名: 型名, …)`で書く。
fn fizzbuzz(n: usize) {
    // `for 変数 in イテレータ {…}`で繰り返しができる。
    // 指定回数の繰り返しなら`m..n`のレンジリテラルが便利。m, m+1, …, (n-1)で繰り返す。
    for i in 0..n {
        // `if 条件式 { then式 } else { else式 }`で条件分岐できる。条件の括弧は不要。
        // 条件式にはbool型しか書けないので注意。
        if i % 15 == 0 {
            println!("FizzBuzz");
        // else if はこう書く。
        } else if i % 3 == 0 {
            println!("Fizz");
        } else if i % 5 == 0 {
            println!("Buzz");
        } else {
            // `println!`は文字列に`{}`を使うことでフォーマッティングできる。
            println!("{}", i);
        }
    }
}

fn main() {
    fizzbuzz(20);
}

関数fizzbuzzの定義では、FizzBuzzを表示したい数の上限を指定できるように、引数を設定しています。引数をとる関数をRustで定義するには、この例のようにfn 関数名 (変数名: 型名, …)とします。

Rustには、ループの方法としてloopwhileforがありますが、この例ではforを使っています。Rustにおけるforループは、C言語のような

for (変数; 終了条件; ステップ) {…}

という形ではなく、

for 変数 in イテレータ {…}

という形になっています。この例のように、レンジリテラルm..nを使うことで、指定範囲の数字を繰り返すイテレータが作れます。

if式の書式は、ほかの言語を知っていればそれほど難しくないでしょう。注意が必要かもしれないのは、ifの条件式についてです。

真偽値以外を条件に指定できる言語もありますが、静的型付き言語であるRustでは、bool型になる式しかifの条件に指定できません。なお、C言語と違って条件式に()は不要ですが、then節とelse節の{}は必要です。また、else { if …という書き方をする必要はなく、ネストせずにelse if …と書けます。

数値を文字列としてprintln!で出力するために、{}によるフォーマット機能(詳しくは公式マニュアルを参照)を使っています。

最後にmainの定義内で、引数に20を指定してfizzbuzz関数を呼び出しています。

偶数二乗合計

次は、0からnまでの偶数の二乗の和を返すプログラムです。今度は、もうちょっとRust的な書き方をしてみます。

fn square_sum(n: isize) -> isize {
    // FizzBuzzと同じくレンジリテラル
    (0..n)
      // 高階関数の`filter`とクロージャリテラルの`|i| i % 2 == 0`
      .filter(|i| i % 2 == 0)
      // 同じく高階関数の`map`
      .map(|i| i * i)
      // イテレータへの演算`sum`
      .sum()
      // returnを書かなくても最後の値が返り値になる。
}

fn main() {
    println!("{}", square_sum(10));
}

ここではじめて、返り値のある関数の例が登場しました。square_sumは、isize(符号付き整数型の一種)の引数を1つとって、同じくisize型の値を返す関数として定義しています。

square_sumの定義本体は、関数型言語に慣れていないと読み難いかもしれません。やっていることは、以下の通りです。

  1. 0から引数で指定された値までの区間を作り(0..n
  2. そこから「偶数である(i % 2 == 0)」を満たす要素だけを残し(filter
  3. そこから「値を二乗する(i * i)」という操作を各要素に施し(map
  4. そこから全要素の総和を求める(sum

イテレータ、クロージャ、高階関数が使えるおかげで、かなり「高級」に書けることがわかります。

しかも、このように「高級」な書き方をしても、強力なRustのコンパイラのおかげで、最適化すればループを使って書いたのと同じ速度で動きます。最適化を有効にするには、コンパイル時に-Oを指定します。

$ rustc -O sum.rs
$ ./sum
120

なお、次のように--emit asmをつけてコンパイルすれば、sum.sにアセンブリが吐かれます。アセンブリを読めるなら、ただのループになっていることが読みとれるでしょう。

$ rustc --emit asm -O sum.rs

変数束縛

3つほどコードを見たところで、これまでの例には登場しなかった文法を説明していきます。まずは、変数束縛です。

Rustでは、let 変数名 = 値;とすることで変数束縛が作れます。その際、変数の型は自動で推論してくれます。

fn main() {
    let x = 1 + 2;
    println!("{}", x); // => 3
}

Rustにおける変数束縛は、デフォルトでイミュータブルです。したがって、再代入はできません。

fn main() {
    let x = 1 + 2;
    x = 5;  // error[E0384]: re-assignment of immutable variable `x`
}

再代入できるようにするには、let mut 変数名 = 値;のように、ミュータブルな変数であると宣言する必要があります。

fn main() {
    let mut x = 1 + 2;
    // 再代入できる。
    x = 5;
    println!("{}", x); // => 5
}

このときに指定するmutは、あくまでも変数につく属性なので、変数ごとに設定できます。

fn main() {
    // イミュータブルな変数
    let x = 1 + 2;
    // ミュータブルな変数に束縛できる。
    let mut y = x;
    y = 5;
    // さらにイミュータブルな変数に束縛できる。
    let z = y;
    // z = 10; // これはエラーになる。

    println!("{}", z); // => 5
}

先ほど、変数は型推論されると言いましたが、変数の宣言時に型注釈を書くことも可能です。型注釈は、let 変数名: 型名 = 値;のように、:に続けて書きます。

型注釈が必要になることは多くないですが、以下のように説明のために型を明示する目的で使われることもあります。

fn main() {
    // `i32`型と明示する。
    let x: i32 = 1 + 2;
    println!("{}", x); // => 3
}

なお、変数への「再代入」はできないと言いましたが、「再束縛」はいつでも可能です。

fn main() {
    // 1つ目の`x`を束縛する。
    let x: i32 = 1;
    println!("{}", x); // => 1
    // 2つ目の`x`を束縛する。これは先のxとは別物。
    let x: &str = "abc";
    // 以後、`x`は`"abc"`を指すようになる。
    println!("{}", x); // => abc
}

束縛は、あくまでも「変数名と値を結び付ける関係」なので、以前の関係を忘れることさえ認めれば関係の更新はいくらでもできます。「変数は箱」という教わり方をした人にはちょっと馴染みづらいかもしれませんが、Rustではどちらかというと「変数は値につけた名前」です。

3

変数は値につけた名前

再束縛と再代入の違いを説明するため、次のようなコードを用意しました。じっくり見比べてみてください。

fn rebind() {
    let sum = 0;
    for i in 0..10 {
        // 新しい束縛を作っているので上の束縛には影響がない。
        let sum = sum + i;
    }
    println!("{}", sum); // => 0
}

fn reassign() {
    let mut sum = 0;
    for i in 0..10 {
        // 上の束縛の値を書き換える。
        sum = sum + i;
    }
    println!("{}", sum); // => 45
}

fn main() {
    rebind();
    reassign();
}

束縛には、ほかにもいろいろ説明することがあるのですが、ひとまずこれだけ理解して次に進みましょう。

Rustの基本的な型

Rustには、さまざまな型が用意されています。そのうち、特に基本的(プリミティブ)なものを、少し多めですが、一挙に紹介します。

名前 説明 リテラル例
() Unit型。何もないことを表わす ()
bool 真偽値 true, false
char 文字型 'x', '💕'
i8, i16, i32, i64 nビット符号付き整数 1, 2i8, -3_000i32
u8, u16, u32, u64 nビット符号無し整数 1, 2u8, 3_000u32
isize マシンに合わせた符号付き整数 1, -3_000isize
usize マシンに合わせた符号無し整数 1, 3_000usize
f32 32ビット浮動小数点数 1.0, -1.0f32
f64 64ビット浮動小数点数 1.0, -1.0f64
&T T型への参照型 -
&mut T T型へのミュータブルな参照型 -
[T; n] T型のn個の要素を持つ配列 [1, 2, 3], [-1.0; 256]
&[T] T型の要素を持つスライス -
str 文字列型。通常は参照として&strの形で使われる "abcd"
(S, T, ...) 任意個の型を並べたタプル型 (1, 1.0, false, "abc")
fn (S, T, ..) -> R 関数型 -

()型は何もないことを表わす型です。唯一の値()を持ちます。

真偽値を表すbool型は、すでにif式を使ったときに登場しましたね。bool型には、truefalseという2つの値があります。

数値を表す型

Rustはシステムプログラミング言語なので、ビット数ごとに整数型が用意されています。

また、isizeusizeという、配列などのコレクションのサイズを表わすのに十分な大きさの整数型もあります。これらの型で表される整数のビット数は、マシンによって変わります。

参照型

参照型は、C言語などのポインタ型に似た概念で、「T型の値のありか」を表わす型です。Rustにはあとで説明する所有権の概念があるので「T型の借用」とも呼ばれます。&値で参照型の値が作れます。

また、&mutのミュータブルな参照型は参照先を書き換えることができます。こちらも&mut 値で参照型の値が作れます。変数のときと違って、こちらは型の一部です。

どちらも、*値で参照を外し、参照先の値を取得することができますが、Rustはデータの扱いに厳しいので一定の条件を満たさないと参照外しができません。

fn main() {
    // イミュータブルな束縛を作っておく。
    let x = 1;
    // `&値` で参照がとれる。
    let y: &isize = &x;
    // ミュータブルな束縛を作っておく。
    let mut a = 1;
    // `&mut 値`でミュータブルな参照がとれる。値もミュータブルである必要がある。
    let b = &mut a;
    // `*参照 = 値`で代入できる。これは`&mut`型ならいつでも可能。
    *b = 2;
    // bの参照先が書き変わっている。aは一定の条件を満たしている(Copyな)ため参照外しができる。
    println!("{}", *b); // => 2
}

配列とスライス

配列型は、配列まるごとを表わす型です。

たとえば、[i64;256]という配列は、64ビット×256=16Kビットのデータを表わします。この配列をコピーするときは、16Kビットのデータがコピーされます。扱うデータサイズもユーザ側で制御できるのが、Rustの特徴です。

一方、バイト列を表わすのに&[u8]のような型を使うこともあります。&[]というのは、配列への参照(ビュー)を表す型で、スライスと呼ばれます。スライスは、それ自体がデータを持っているわけではなく、データのありかを指すだけです。したがって配列に比べてデータサイズはずっと小さくなります。

fn main() {
    let a: [isize;3] = [1, 2 , 3];
    // `&配列` でスライスが作れる。
    let b: &[isize] = &a;
    // スライスをフォーマットするにはプレースホルダが`{:?}`になる。
    println!("{:?}", b); // => [1, 2, 3]
    for elm in b {
      println!("{}", elm);
    }
    // => 1
    //    2
    //    3

    // あるいは`(スライス/配列)[インデックス]`で要素にアクセスできる。
    println!("{:?}", b[0]); // => 1
}

文字と文字列

Rustでは、ユニコード文字を扱えます。'x''💕'のようにシングルクォーテーションでくくることで、char型の値を作れます。

文字列について、Rustでは普段、2種類の型を使います。String&strです。Stringは、それ自体が文字列の所有者で、文字列を伸ばすなどの操作も可能です。&strは、スライスと同じように、文字列への参照を表わす型です。

4

2種類の文字列

Rustの文字列は、すべてUTF-8でエンコードされている必要があります。UTF-8以外のエンコーディングを扱いたい場合は、ライブラリに頼ることになるでしょう。

Stringから&strは低コストで作れますが、&strからStringは文字列のコピーが必要なのでコストがかかります。

リテラルの文字列は&str型です。つまり、変更不能です。Rubyを使っている人なら、「frozen string literal」で話題になったので理解しやすいと思います。

Stringは柔軟性が高い反面、作るのにはコストがかかり、&strは気軽に作れる反面、柔軟性に欠けます。この2つを上手く使い分けましょう。

所有権が絡むので解説は次のセクションに回しますが、リードオンリーなら&strを、書き換えたいなら&mut Stringを、そのまま値をずっと持っておきたいならStringを使うことが多いようです。

fn main() {
    // `&str`は`to_string()`メソッドで`String`にできる。
    let mut a: String = "abc".to_string();
    // 少しややこしいが、`String`に`&str`を足すと`String`ができる。
    // `&str`に`String`を足したり`String`に`String`を足したりはできない。
    a += "def";
    println!("{}", a); // => abcdef

    // `.to_string()`は様々な型に用意されている。
    let x = 1.0.to_string();
    println!("{}", x); // 1

    // `String`を`&str`にするには`as_str()`が使える。
    a += x.as_str();
    println!("{}", a); // => abcdef1
}

ちなみに、&strStringの関係と同じような関係にある型はたくさんあります。たとえば&[T]に対応するVec<T>という型もありますし、&Tに対するBox<T>というのもあります。

タプル

タプルは、複数の値を組にして扱う機能です。それぞれ型が違っても問題ありません。

fn main() {
    // 型を混合したタプルが作れる。
    let a: (isize, f64, &str) = (1, 1.0, "abc");
    // `タプル.インデックス`でタプルの要素にアクセスできる。
    println!("{}, {}, {}", a.0, a.1, a.2); // => 1, 1, abc
}

関数

関数も、第一級の値として扱うことができます。

// 関数を定義する。
fn add(x: isize, y: isize) -> isize {
    x + y
}

fn main() {
    // 関数は`名前(引数)`で呼び出せる。
    println!("{}", add(1, 2)); // => 3
    // 関数を変数に束縛できる。
    let f: fn(isize, isize) -> isize = add;
    // 変数に束縛した関数も`名前(引数)`呼び出せる。
    let a = f(1, 2);
    println!("{}", a) // => 3
}

強力な構文、match式

エンジニアHubに会員登録すると
続きをお読みいただけます(無料)。
登録のメリット
  • すべての過去記事を読める
  • 過去のウェビナー動画を
    視聴できる
  • 企業やエージェントから
    スカウトが届く