102
94

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rust1.0学習用私的メモ

Last updated at Posted at 2015-05-18

2015/5/15にRust 1.0が正式リリースされたので、そこから学習を始めた人間の私的なメモ。

// factorial
fn fact(n: i32) -> i32 {
  if n < 2 { 1 } else { n * fact(n-1) }
}
fn main() {
  let n = 5;
  println!("Hello Rust! {}", fact(n));
}

The Rust Project FAQより:

What is this project's goal, in one sentence?
To design and implement a safe, concurrent, practical, static systems language.

解説記事

(Rust 1.0以前での言語仕様変更は激しかったらしく、Web上で検索して出てきた記事内容を鵜呑みにしないほうがよさげ。1.0では既に廃止された仕様とかも相当あるみたい。)

rust-lang.orgの公式ドキュメントが丁寧に書かれている。ここからはじめるべき。

公式ブログ(http://blog.rust-lang.org/)

ブログ記事

(Rust 1.0対応が確認できたもの)

StackOverflow

言語の特徴

  • ゼロ・コストの抽象化(zero-cost abstractions)
  • ムーブ・セマンティクス(move semantics)
  • メモリ安全性の保証(guaranteed memory safety)
  • データ競合のないスレッド(threads without data races)
  • トレイトに基づくジェネリクス(trait-based generics)
  • パターン・マッチング(pattern matching)
  • 型推論(type inference)
  • 最小のランタイム(minimal runtime)
  • 効率的なCバインディング(efficient C bindings)

コンパイル時の型検査によって、次の未定義動作が生じないことを保証する。

  • データ競合(Data races)
  • ヌルポインタや参照先が無効なポインタの参照剥がし(Dereferencing a null/dangling raw pointer)
  • 未定義(未初期化)メモリの読み取り/(Reads of undef (uninitialized) memory)
  • ...

他プログラミング言語から影響を受けたRustの言語機能。

  • SML, OCaml: algebraic datatypes, pattern matching, type inference, semicolon statement separation
  • C++: references, RAII, smart pointers, move semantics, monomorphisation, memory model
  • ML Kit, Cyclone: region based memory management
  • Haskell (GHC): typeclasses, type families
  • Newsqueak, Alef, Limbo: channels, concurrency
  • Erlang: message passing, thread failure
  • Swift: optional bindings
  • Scheme: hygienic macros
  • C#: attributes
  • ...

構成要素

式(expression)と文(statement)

ほとんど全てが 式(expression) となっている。if/match/loopなどのフロー制御やブロック{}も式として扱う。

// 式(expression)
42
()
[1, 2, 3]
0..10
"foobar".to_string()
if 0 < n { n } else { -n }
match opt { Some(_) => true, None => false }
{ println!("Hello"); 42 }

文(statement) は 宣言文(declaration statement) と 式文(expression statement) の2種類に大別される。

// 宣言文
use std::option::Option;
fn f(x: i32) -> i32 { /*...*/ }
struct S { /*...*/ }
enum E { /*...*/ }
const C: i32 = /*...*/
trait T { /*...*/ }
impl T for S { /*...*/ }

式文は式(e)の末尾に;をつけたもの。式文e;は常に型unit typeの値()となる(値eではない!)。

fn answer_universe() -> i32 {
  42  // OK: i32型の値42
  // `return 42;`とも書ける

  //42;  // NG: unit typeの値()
  // error: not all control paths return a value [E0269]
}

基本型(primitive types)とリテラル(literal)

真偽値型boolは、値truefalseのいずれか。

数値リテラルには型名をサフィックスとして付与する。サフィックス省略時はi32またはf64。整数リテラルはデフォルトで10進数となり、プレフィクス0b(2進), 0o(8進), 0x(16進)で変更可能。また読みやすさのため、数値リテラル中に区切り文字_を含めても良い(任意)。

  • 幅指定の整数型:i8, i16, i32, i64, u8, u16, u32, u64(2の補数表現)
  • 環境依存の整数型:isize, usize(ポインタ型のビット幅)
  • 浮動小数点数型:f32, f64(IEEE 754 binary32/64準拠)
0u8      // u8
42isize  // isize
3.14f32  // f32
10_000   // i32

NOTE: Rust1.0以前を対象にした記事ではint, uintが使われていることがある。それぞれisize, usizeに名称変更されている。RFC 544参照。

文字型charは、有効なUnicode文字を1つ格納する(u32と同ビット幅だが有効範囲チェック付き)。文字の配列[char]はUCS-4/UTF-32文字列に相当する。文字リテラルはシングルクォーテーション'で括る。

文字列型strは、UTF-8エンコーディングされたバイト列を表す。文字列リテラルはダブルクォーテーション"、またはr#""#(raw string)で括る。文字列リテラルはimmutableな文字列。Rustの文字列リテラルはfirst-class型ではないため、使用時にはポインタ型&strとして参照する。1

変更可能な文字列としてString型(std::string::String)が提供されており、文字列リテラル(str)からはto_string()メソッドで明示的に変換する(複製処理のため少々コスト高)。

'H'           // char型リテラル H
"Hello"       // str型リテラル Hello
r#""Hello""#  // str型リテラル "Hello"

let s: &str   = "Hello";             // &str型
let s: String = "Hello".to_string()  // String型

let mut s: String = "Hello".to_string();
s.push_str(", World");
// sは値"Hello, World"

文字リテラル/文字列リテラルにプレフィクスbを付けると、バイト(u8)またはバイト配列([u8; N])リテラルになる。(リテラルが非ASCII文字を含む場合はコンパイルエラー)

b'H'          // u8型リテラル H
b"Hello"      // &[u8; 5]型リテラル Hello

let bytes = b"漢字";
// error: byte constant must be ASCII. Use a \xHH escape for a non-ASCII byte: \u{6f22}
// error: byte constant must be ASCII. Use a \xHH escape for a non-ASCII byte: \u{5b57}

let bytes: &[u8] = b"\x22\x6f\x57\x5b";
let s = String::from_utf8_lossy(bytes);
println!("{}", s);  // "oW[

let chars_u16 : &[u16] = unsafe { std::mem::transmute(bytes) };
let s = String::from_utf16_lossy(chars_u16);
println!("{}", s);  // 漢字

文字列処理全般は http://qiita.com/aflc/items/f2be832f9612064b12c6 が参考になった。

タプル(tuple)

任意の型をもつ複数要素をまとめて タプル(tuple) 型を構成する。タプルは各要素をコンマ,で区切り、全体を丸括弧()で括る。要素値の設定や読み出しには、後述するパターンマッチを利用するか、.の後ろに直接インデクス値を記述する。

let t: (i32, String) = (10, "ABC".to_string());

let (k0, v0) = t;  // k0は10, v0は"ABC"

let k1 = t.0;  // 10
let v1 = t.1;  // "ABC"

(true)   // bool型
(true,)  // 1要素のタプル型`(bool)`

空の値/何も無いことを表すユニット型(unit type)として、値()を用いる。文法的には0要素のタプル型(Haskell由来?)。

範囲(range)

..を用いて整数区間の 範囲(range) を表す。後述するスライスの表現や、for式の反復区間指定に用いる。begin..endは半開区間[begin,end)を表す。(例:0..5は0,1,2,3,4)

1..2;   // std::ops::Range
3..;    // std::ops::RangeFrom
..4;    // std::ops::RangeTo
..;     // std::ops::RangeFull
// 10回ループ
for i in 0..10 {
  println!("{}", i);
}

Range型(b..e)とRangeFrom型(b..)はIteratorトレイト(std::iter::Iterator)を実装するため、rev()collect()関数などを呼び出せる。

// 逆順ループ
for i in (1..10).rev() { /*...*/ }

// Vec型に変換
let list : Vec<_> = (0..5).collect();

// 1?10総和を計算
let sum = (1..).take(10).fold(0, |a,n| a+n);

配列(array)とスライス(slice)

同じ型を持つ複数要素をまとめて 配列(array) 型を構成する。配列型[T; N]は要素型Tと要素数Nで指定される。いわゆる固定長配列のため、可変長配列が必要ならばVec型(std::vec::Vec)を利用する。配列リテラルは各要素値をコンマ,で区切って並べるか、値と要素数を;で区切って、全体を角括弧[]で括る。

let a = [1, 2, 3];  // [i32; 3]型の配列[1,2,3]
let b = [0u8; 5];   // [u8; 5]型の配列[0,0,0,0,0]

要素数の取得はlen()メソッドを利用する。要素アクセスには0始まりの添字(indexing)を利用する。配列の範囲外アクセスは、インデクス値がコンパイル時定数ならばコンパイルエラー、そうでなければpanic(実行時エラー)となる。

let mut x = [1, 2];
x[1] = 100;
// xは配列[1, 100]
let l = x.len();  // 値2

配列要素への範囲(range)アクセス方式として、スライス(slice) がある。スライスは配列と異なり対象データを所有(own)しないため、ポインタ型&[T]として表現される。なお、スライスは配列以外にもVec型に対して適用できる。

let a: [i32; 4] = [1, 2, 3, 4];  // 配列[1,2,3,4]
let s: &[i32] = &a;        // スライス[1,2,3,4]
let s: &[i32] = &a[2..];   // スライス[3,4]
let s: &[i32] = &a[..2];   // スライス[1,2]
let s: &[i32] = &a[1..3];  // スライス[2,3]

let s: &[i32] = a;  // error: mismatched types: expected `&[i32]`, found `[i32; 4]`

let v = vec!['A', 'B', 'C'];
let s: &[char] = &v;  // スライス['A','B','C']

構造体(struct)

複数個の型をまとめて新しい 構造体(struct) 型を宣言する。各要素はフィールド(field)と呼ぶ。

struct Point {
  x: f64,
  y: f64,  // 最後のカンマ(,)はなくても良い
}

let pt = Point { x: 10.0, y: 20.0 };
let x = pt.x;
let Point { x: a, y: b } = pt;  // aに10.0が、bに20.0が代入される

フィールド名のないタプル構造体(tuple struct)や、フィールド0個のユニット風構造体(unit-like struct)も定義できる。

struct Pair(i32, f64);
struct Nil;

..と組み合わせて、一部フィールドだけを更新した新しい値を簡単に作れる。

struct Point3D { x: i32, y: i32, z: i32 }

let a = Point3D {x: 1, y: 2, z: 3};
let b = Point3D {y: 10, .. a};  // Point3D{x:1, y:10, z:3}

変数(variable)

let束縛(let binding)で変数名に値を結びつける。変数はデフォルトでimmutableとなり、mutキーワード指定でmutableな変数となる。

let mutopt <変数名> `=` <値> ;
または
let mutopt <変数名> `:` <型名> `=` <値> ;

immutable変数への再代入、およびオブジェクトに対する変更操作が不可能(コンパイルエラー)。mutableとした場合でも、immutable変数で十分な場合はコンパイラが警告してくる。2

let x = 3;  // immutable変数
x = 5;  // error: re-assignment of immutable variable `x`

let mut y = 5;  // mutable変数
y = 10;  // OK

変数の型は周囲のコンテキストから型推論される。上記コードではx, yは整数リテラルのデフォルト型i32となる。また下記コードではvVec<f64>と型推論される(3.14は浮動小数点数リテラルのデフォルト型f64)。

let mut v = Vec::new();
v.push(3.14);

束縛先には単純な変数名だけでなく、タプルや構造体などによるパターンマッチを利用できる。読み捨てる箇所には_を用いる。

let t = (1, 2, 3);
let (_, y, _) = t;  // OK: yは値2

struct Point { x: f64, y: f64 }
let pt = Point { x: 5.0, y: -8.0 };
let Point { x: a, y: b } = pt;  // OK: aは値5.0, bは値-8.0

すでにある変数名(val)で再束縛を行うと、前の変数を隠す(再定義エラーとならない)。同じ意味で型だけが異なる場合にいちいち変数名をつけなくて済む。

let val: Option<i32> = Some(100);
// ...
let val: i32 = v.unwrap_or(42);

関数(function)

fnキーワードで 関数(function) を定義する。全ての引数型を明記する必要がある。戻り値型を省略した場合はユニット型()となる。引数部分ではlet束縛同様にパターンマッチも使える。

関数の戻り値は、関数本体ブロックにある最後の式の値となる。またはreturn式で即時に戻り値を設定して関数を抜ける。

fn add(x: i32, y: i32) -> i32 {
  x + y
  // `return x + y;`でもOK
}

fn hello(name: String) {
  println!("Hi, {}!", name);
}

fn mid((_, v, _): (i32, i32, i32)) -> i32 { v }
let x = mid((1, 2, 3));  // xは値2

また、関数自身の型もfnキーワード+引数型リスト+戻り値型で記述できる。高階関数の記述などで使う。

fn mul(x: i32, y: i32) -> i32 { x * y }
let op: fn(i32, i32) -> i32 = mul;  // fn(i32, i32) -> i32型

let x = op(2, 3);  // xは値6 

関数オーバーロード

Rustでは直接の関数オーバーロードは出来ない。トレイトにより実現する。

fn dump(x: i32) { println!("i32: {}", x) }
fn dump(x: &str) { println!("str: {}", x) }
// error: duplicate definition of value `dump`
trait DumpVal { fn dump(self); }

impl DumpVal for i32 {
  fn dump(self) { println!("i32: {}", self) }
}
impl DumpVal for &'static str {
  // トレイト実装対象には生存期間'staticの明示的な指定が必要
  // &strだけではコンパイルエラー"error: missing lifetime specifier"
  fn dump(self) { println!("str: {}", self) }
}

DumpVal::dump(42);
DumpVal::dump("hello");
// または
42.dump();
"hello".dump();

main関数

プログラムのエントリポイントはmain()関数。引数無しかつ戻り値型()固定。コマンドライン引数はstd::env::args()関数で取得し、プロセス終了コードはstd::process::exit()関数で設定できる。

fn main() {
  for arg in std::env::args() {
    println!("{}", arg);
  }
  std::process::exit(-1);
}

ラムダ式(lambda expression)

ラムダ式(lambda expression)は、バーティカルバー||で囲った引数リスト+本体式(ブロック式{}でもOK)で記述する。ラムダ式は一意なクロージャ(closure)型の値となる。fnキーワードによる関数と異なり、ラムダ式の引数型と戻り値型は型推論される(明示してもよい)。

// 引数リスト(i32, i32)をとって戻り値i32を返すクロージャ
let add = |x, y| { x - y };
let sub = |x, y| x + y;
println!("{} {}", add(1,2), sub(1,2));

// 引数なし/戻り値()のクロージャを宣言&呼出し(意味はない)
(||{})();

ラムダ式は環境をキャプチャ(capture their environment)する。キャプチャ操作は、ラムダ式中でアクセスする変数を借用(borrowing)するのと等価。moveキーワードにより所有権(ownership)を移動するキャプチャ操作に変更できる(thread::spawn(move || /*...*/);で利用するパターンが多い)。

let s = "X".to_string();
{
  let lm = || {  // sを借用
    println!("{}", s)
  };
  println!("{}", s);
  lm();
}
    
{
  let lm = move || {  // sの所有権を移動
    println!("{}", s)
  };
  println!("{}", s);  // error: use of moved value: `s`
  lm();
}

Fn/FnMut/FnOnceトレイト

クロージャや関数は特殊なトレイトFn, FnMut, FnOnceを自動的に実装する。これらのトレイトは、オブジェクトfに対して関数呼び出し形式f()で呼び出せる型を表しており、それぞれ第一引数&self, &mut self, selfとした関数定義に対応する。(例:FnOnceは所有権を移動してしまうため、同オブジェクトに対して1回だけ呼び出せるトレイト)

// ジェネリクス関数版
fn apply1<F>(f: F, v: i32) where F: Fn(i32) -> i32 {
  // 型Fは引数リスト(&self,i32)をとってi32型を返す呼び出し可能なオブジェクト
  // &selfはキャプチャ環境(変数&m)アクセスのために内部的に用いられる
  println!("{}", f(v));
}

let m = 2;
apply1(|n| n * m, 21);  // クロージャを値渡し(所有権を移動)

fn triple(n: i32) -> i32 { n * 3 }
apply1(triple, 21);  // 関数を値渡し(コピー)
// トレイトオブジェクト版
fn apply2(f: &Fn(i32) -> i32, v: i32) {
  println!("{}", f(v));
}

let m = 2;
apply2(&|n| n * m, 21);  // クロージャを貸出し

fn triple(n: i32) -> i32 { n * 3 }
apply2(&triple, 21);  // 関数を貸出し

関数型fn(i32) -> i32とトレイトFn(i32) -> i32の混同に注意。関数型にはクロージャを代入できない。

実装(implementation)

Rustの構造体は、他のオブジェクト指向言語でいう"クラス"に相当する。implキーワードを用いた 実装(implementation) によってクラスの"メソッド"相当を定義する。

第一引数に&self, &mut self, selfのいずれかをとる関数は、インスタンスメソッドに相当する。そうでない関数はクラスメソッドに相当する。Selfは実装対象の型(構造体)を指す。

struct S { x: i32, y: i32 }

impl S {
  // インスタンスメソッド(的な関数)
  fn f(&self, a: i32) -> &Self {
    // &selfはいわゆるthisポインタに相当
    println!("{},{}:{}", self.x, self.y, a);
    self  // selfを返すとメソッドチェインをサポート
  }

  // クラスメソッド(的な関数)
  fn h(x: i32) -> () {
    println!("x={}", x);
  }
}

let s = S { x: 1, y: -1 };

s.f(1).f(2);
// S::f(S::f(&s, 1), 2);でもOK

S::h(42);

他のオブジェクト指向言語でいう"コンストラクタ"や"デストラクタ"に対応する、直接的なRustの言語要素は存在しない。

  • (慣例的に)コンストラクタはメソッド名newの関数を定義する。
  • デストラクタ相当はDropトレイト(std::ops::Drop)を実装する。
struct S { x: i32, y: i32 }

impl S {
  // コンストラクタ(相当)
  fn new() -> S {
    // フィールドに初期値を与えた構造体を返す
    S { x: 0, y: 0 }
  }
}

impl Drop for S {
  // デストラクタ(相当)
  fn drop(&mut self) -> () {
    println!("destroyed")
  }
}

{
  let s = S::new();
} // "destroyed"が出力される

トレイト(trait)

traitキーワードにより、特定の関数・定数・型を含むといった「型の性質」を記述する トレイト(trait) を宣言する。トレイトは抽象的なインターフェイスであり、構造体などの具象的な型とは異なる。

trait Shape {
  fn draw(&self, Surface);
  fn bounding_box(&self) -> BoundingBox;
}

Rustのトレイトは、他のオブジェクト指向言語でいう"インターフェイス"や"抽象クラス"に似ている一方で、Haskellの"型クラス(type class)"相当の性質もある。継承関係を用いずに型の性質を表現し、オブジェクト多態性や演算子オーバーロードの実装、ジェネリクスでの型制約指定に用いる。

struct Circle {
  radius: f64,
  center: Point,
}

impl Shape for Circle {
  fn draw(&self, s: Surface) { do_draw_circle(s, *self); }

  fn bounding_box(&self) -> BoundingBox {
    let r = self.radius;
    BoundingBox {
      x: self.center.x - r, y: self.center.y - r,
      width: 2.0 * r, height: 2.0 * r }
  }
}

トレイトTは具象型ではなく抽象インターフェイスにすぎないため、参照型&TBox<T>型のような使い方のみ許容される。直接トレイトT型のオブジェクトとして変数を保持することはできない。3

struct S;
trait T { fn f(&self); }
impl T for S {
  fn f(&self) { println!("f(&S)") }
}

let x: S = S;
let t: &T = &x;  // OK
t.f();

let b: Box<T> = Box::new(S);  // OK
b.f();

let u: T;
// error: the trait `core::marker::Sized` is not implemented for the type `T`

ディスパッチ(dispatch)

Rustではトレイトを用いて、ジェネリクス関数によるコンパイル時**静的ディスパッチ(static dispatch)と、トレイトオブジェクト(trait object)による実行時動的ディスパッチ(dynamic dispatch)**の2種類をサポートする。

静的ディスパッチでは実行時オーバーヘッドが最小となるが、それぞれの具象型に対してジェネリクス関数が実体化されるため生成コードが肥大化する。動的ディスパッチではコード肥大化を避けられるが、実行時コストは仮想関数呼び出しにより若干のペナルティを受ける。

trait Animal { fn cry(&self); }

struct Dog;
impl Animal for Dog {
  fn cry(&self) { println!("Bow wow") }
}

struct Cat;
impl Animal for Cat {
  fn cry(&self) { println!("Meow") }
}

// ジェネリクスによる静的ディスパッチ
fn func_static<T: Animal>(pet: &T) {
  // コンパイル時に決定するT型により呼び出し関数を決定
  pet.cry()
}

// トレイトオブジェクトによる動的ディスパッチ
fn func_dynamic(pet: &Animal) {
  // Animalトレイトオブジェクトを経由して実行時に呼び出し関数を決定
  pet.cry()
}

let (dog, cat) = (Dog, Cat);

// ジェネリクスによる静的ディスパッチ
func_static(&dog);
func_static(&cat);

// トレイトオブジェクトによる動的ディスパッチ
func_dynamic(&dog);
func_dynamic(&cat);

マーカ・トレイト

一部トレイトは型の性質を示すだけのマーカーであり(中身のないトレイト)、型に関する振る舞いの指定や、コンパイル時の型検査に利用される。

  • Copyトレイト:バイトコピー(memcpy)で複製可能。Copyトレイトを実装する型はコピー・セマンティクスを持つ。
  • Sizedトレイト:コンパイル時に型サイズが確定する。
  • Sendトレイト:スレッド境界をまたいで転送できる。
  • Syncトレイト:スレッド境界をまたいで共有できる。いわゆる同期化された型。

derive属性

derive属性(attribute)を用いると、構造体に対してトレイトを自動的に実装できる(可能ならば)。下記例では構造体SCopy, Clone両トレイトを実装している。4

#[derive(Copy, Clone)]
struct S;

// 下記宣言に相当する
struct S;
impl Copy for S {}
impl Clone for S { fn clone(&self) -> S { *self } }

列挙型(enumeration)

とりうる値を列挙した 列挙型(enumeration) を宣言する。

enum Animal {
  Dog,
  Cat
}

let a: Animal = Animal::Dog;

Rustの列挙型は単純な値列挙だけではなく、代数的データ型(algebraic data type)/タグ付き共用体(tagged union)となっており、それぞれの列挙値がさらにデータを保持できる。

enum List<T> {
  Nil,                   // List終端マーカ
  Cons(T, Box<List<T>>)  // Consセル: 値と次セルへのポインタ
}

impl<T> List<T> {
  // リスト長を返す
  fn len(&self) -> usize {
    match *self {
      List::Nil => 0,
      List::Cons(_, ref cdr) => 1 + cdr.len()
    }
  }
}

let a: List<i32> = List::Cons(7, Box::new(List::Cons(13, Box::new(List::Nil))));
let l = a.len();  // lは値2

Rust標準ライブラリでは、Option型とResult型を全面的に利用している。値を取りだすときはmatch式でパターンマッチを行うか、unwrap()関数を呼び出す(None/Errの場合はpanic)。

  • Option型:有効値(Some<T>)または無効値(None)を表す。
  • Result型:関数戻り値として、成功(Ok(T))または失敗(Err(E))を表す。

ジェネリクス(generics)

関数(fn)/構造体(sturct)/列挙体(eunm)/トレイト(trait)において、ジェネリクス(generics) により型情報をパラメータ化できる。型パラメータに関する制約(bound)は、whereキーワードとトレイトを用いて記述する。

use std::fmt::Debug;
fn dump<T>(v: T) where T: Debug {
  // "{:?}"書式にはDebugトレイトが実装されている必要あり
  println!("{:?}", v)
}
dump(1);
dump("hello");

// 制約は型パラメータTの直後に書いてもよい
fn dump<T: Debug>(v: T) { /*...*/ }
struct Pair<T> {
  first: T,
  second: T,
}

fn swap<T>(pair: Pair<T>) -> Pair<T> {
  let Pair { f, s } = pair;
  Pair { first: s, second: f }
}

型パラメータの制約では+により複数トレイトの指定や(例:T: Eq + Ord)、!による否定制約の指定(例:T: !Send)、生存期間(lifetime)を指定することもできる(例:F: Send + 'static)。また、型パラメータTでは暗黙にSizedマーカ・トレイトを要求するため、動的サイズ型(DST; dynamically sized type)も許容するにはT: ?Sized制約を指定する。5

マクロ(macro)

任意の マクロ(macro) をユーザ定義/利用できる。マクロ名の末尾文字は必ずエクスクラメーション!とする(例:println!, panic!)。Rustのマクロは、C/C++言語マクロのようなトークン置換ではなく、Scheme言語のようなHygienic Macro(直訳すると"健全なマクロ")となっている。

標準ライブラリで提供される代表的なマクロ:

  • format!:書式指定にしたがった文字列化/Stringを返す
  • vec!:可変長配列オブジェクトを生成/Vecを返す
  • println!:標準出力に文字列出力(改行付き)
  • concat!:複数リテラルを連結して文字列リテラル化
  • print!:標準出力に文字列出力(改行なし)
  • try!Result型を返す関数でのErr処理補助ラッパー
  • panic!:スレッドを異常停止
  • assert!:実行時アサーション

動作モデル

所有権(ownership)

Rustのオブジェクトはただ1つの変数によって 所有権(ownership) を管理される。所有権が手放されたタイミングで、オブジェクトは破棄されメモリも回収される。(Rustには非決定動作のGC機構は存在しない。)

ある変数を別の変数に束縛した場合、オブジェクトの所有権が移動する(デフォルトでムーブ・セマンティクス)。所有権の移動はコンパイル時に追跡されており、所有権を持たない変数にアクセスするとコンパイルエラーとして検知する。

let a: String = "ABC".to_string();  // 変数aがオブジェクト"ABC"を所有
let b: String = a;  // 変数bにオブジェクトの所有権を移動

println!("b={}", b);  // OK
println!("a={}", a);  // NG: 既に変数aは所有権をもっていない
// error: use of moved value: `a`

オブジェクトをコピーするには、Cloneトレイト(std::clone::Clone)のclone()関数を明示的に呼び出す。

let a: String = "ABC".to_string();
let b: String = a.clone();  // オブジェクトを複製して変数bに束縛

println!("a={}, b={}", a, b);  // OK

i32, f64などプリミティブ型のように"値のセマンティクス"を持つ型では、Copyトレイトを実装するとコピー・セマンティクスに変更される(std::marker::Copy)。Copyトレイトはコンパイラに対する型の動作指定として機能する。

let a: i32 = 42;  // 変数aがオブジェクト42を所有
let b: i32 = a;  // 変数bにオブジェクト42を複製して束縛

println!("a={}, b={}", a, b);  // OK

独自定義の型でもCopyトレイトとCloneトレイトを実装しておくことで、コピー・セマンティクスに変更できる。(Debugトレイトは書式{:?}対応に必要。)

#[derive(Copy,Clone,Debug)]
struct Point (i32, i32);

let a: Point = Point (0, 1);
let b: Point = a;  // コピー・セマンティクス
println!("a={:?}, b={:?}", a, b);  // OK

借用(borrowing)

Rustの参照(reference)型変数では、参照元の変数が所有するオブジェクトの所有権を、参照変数の有効期間の間だけ 借用(borrowing) する。ある変数の参照を得る(所有権を借りる)には、&または&mut単項演算子を用いる。

所有権の貸出元である変数には、その貸出期間中にオブジェクトの所有権を手放す操作が許可されない(ダングリング参照をつくらせない)。また&mut演算子によるmutableな参照は同時に1つだけしか貸出しできないことに注意。借用による所有権の一時移動もコンパイル時に追跡されている。

let mut a: String = "abc".to_string();
{
  // aの変更可能な参照(&mut String)をbに束縛
  let b: &mut String = &mut a;
  b.push_str("xyz");
  println!("b={}", b);  // OK: bは値"abcxyz"

  // aは所有権を貸出し中のため所有権を手放せない
  let x: String = a;
  // error: cannot move out of `a` because it is borrowed

  // aは所有権を貸出し中のため所有権を手放せない
  a = "other".to_string();
  // error: cannot assign to `a` because it is borrowed

  // aはmutable参照を貸出し中のため2度目以降の貸出し不可
  let c: &mut String = &mut a;
  // error: cannot borrow `a` as mutable more than once at a time

} // bが借用していた所有権をaに返す
println!("a={}", a);  // OK: aは値"abcxyz"

{
  let b: &mut String = &mut a;  // aのmutalbe参照をbに束縛
  let c: &mut String = b;       // bが借用中の所有権をcに又貸し

  // この時点でbは所有権を借用していない
  b.push_str("Nope");
  // error: cannot borrow `*b` as mutable more than once at a time
}

{
  // immutable参照ならばいくらでも借用できる
  let b: &String = &a;
  println!("a={}, b={}", a, b);  // OK
  let c: &String = &a;
  println!("a={}, b={}, c={}", a, b, c);  // OK
}

##自動デリファレンス
オブジェクトに対する関数呼び出しでは、自動デリファレンス(auto dereference) という仕組みにより関数探索が行われる。デリファレンス演算子*Derefトレイト(std::ops::Deref), DerefMutトレイト(std::ops::DerefMut)により演算子オーバーロードできるが、関数呼び出し時には対象関数名がみつかるまでコンパイラによって再帰的にデリファレンス演算子*が適用される。

例えばVec<T>型オブジェクトviter()関数を直接実装しないが、deref()によりスライス型&[T]に自動変換され、スライスのiter関数が呼び出される。参照型&Vec<T>Box<T>, Rc<T>などの場合は2回デリファレンスが行われる。

let v: Vec<i32> = vec![1, 2, 3];
for e in v.iter() { ... }
for e in (*v).iter() { ... }

let r: &Vec<i32> = &v;
for e in r.iter() { ... }
for e in (*r).iter() { ... }
for e in (**r).iter() { ... }

use std::boxed::Box;
let bv = Box::new(vec![1, 2, 3]);
for e in bv.iter() { ... }

use std::rc::Rc;
let rcv = Rc::new(vec![1, 2, 3]);
for e in rcv.iter() { ... }

この動作を理解していないとAPIリファレンスをまともに読めない。http://qiita.com/wada314/items/8b63d66b26abaacab456 でも触れられているが、2015年6月現在のAPIリファレンスでは自動デリファレンス先も含めて表示されるよう改善されている。

生存期間(lifetime)

全てのオブジェクトは 生存期間(lifetime) をもっており、Rustではこの生存期間も型情報の一部として扱う。生存期間はオブジェクト生成地点から、所有権を持つ変数が属するブロックスコープ({})終端までで定義され、生存期間に関する不整合はコンパイル時にチェックされる。例えばオブジェクト生存期間(val)より広い生存期間の参照変数(r)では、そのオブジェクトを借用(r = &val)できない。

{
  let mut r: &String;
  {
    let val: String = "X".to_string();
    r = &val;  // error: `val` does not live long enough
  } // valが所有権を持つ値"X"の生存期間はここまで

  // rが参照&valを保持するべきでない区間!

} // 変数rの生存期間はここまで

let outer_val: String;
{
  let mut r: &String;
  {
    let val: String = "OK".to_string();
    outer_val = val;  // 所有権をouter_valに移動
    r = &outer_val;
  } // valは何の所有権も持っていない

  println!("{}", r);  // OK

} // outer_valが所有権を持つ値"OK"の生存期間ここまで

プログラム全域の生存期間を表す'staticを除いて、生存期間を直接表す名前は存在しない。ジェネリクス関数や構造体の定義では、アポストロフィー'+生存期間パラメータ名で扱う(慣例的に'a, 'b...を用いる)。

// 参照先の生存期間を暗黙にとる関数
fn foo(x: &i32) { /*...*/ }

// 参照先の生存期間を明示的にとる関数(ジェネリクス)
fn bar<'a>(x: &'a i32) { /*...*/ }

// 戻り値の参照は、関数引数の参照と同じ生存期間を持つ
fn max<'a>(x: &'a Foo, y: &'a Foo) -> &'a Foo {
  if x.f > y.f { x } else { y }
}

// 生存期間を明示した参照をフィールドに持つ構造体
struct RefS<'a> { x: &'a i32 }

// 文字列リテラルは'static生存期間を持つ
let s: &'static str = "literal";

ヒープとBox型

Rustのオブジェクトは、デフォルトではスタック上に確保される。ヒープ上に動的メモリ確保&管理を行うにはBox型(std::boxed::Box)を利用する。RustのBox<T>はC++のunique_ptr<T>の動作が近い。Box<T>型オブジェクトを複製(clone())する場合、ヒープ上のT型オブジェクトごと複製される(オブジェクトは共有されない)。Box<T>型オブジェクトの生存期間が終わるときにヒープメモリが自動解放される。

#[derive(Debug)]
struct Person { id: u32, name: String, age: i8 }

impl Person {
  fn new(id: u32, name: &'static str, age: i8) -> Person {
    Person { id: id, name: name.to_string(), age: age }
  }
}

let stack : Person = Person::new(1, "foo", 17);
let heap : Box<Person> = Box::new(Person::new(2, "bar", 34));
println!("{:?} {:?}", stack, heap);

// Person型がCloneトレイトを実装しないため、Box<Person>も複製不可
let a = heap.clone();
// error: type `Box<Person>` does not implement any method in scope named `clone`

NOTE: 一部の記事では、Box::new()関数ではなくboxキーワードを利用しているものがある。Rust 1.0 Stable Channelではboxキーワードはまだunstableのため利用不可。
NOTE: Rust1.0以前を対象にした記事では、チルダ~を型修飾子のように利用しているものがある。型Tに対する~Tは概ねBox<T>と読み替えればよい。RFC 59参照。

Rc型

複数の変数からヒープ上のimmutableオブジェクトを共有する仕組みとして、参照カウント(reference count)方式によるRc型(std::rc::Rc)が提供される。RustのRc<T>はC++のshared_ptr<T>の動作が近い。Rc<T>型オブジェクトを複製(clone())すると、参照カウンタを増やしてT型オブジェクトを共有する。Rc<T>型オブジェクトの生存期間が終わり、参照カウントがゼロになったときにヒープメモリが自動解放される。

#[derive(Debug)]
struct Person { id: u32, name: String, age: i8 }

impl Person {
  fn new(id: u32, name: &'static str, age: i8) -> Person {
    Person { id: id, name: name.to_string(), age: age }
  }
}

let mut a = Rc::new(Person::new(2, "bar", 34));
let mut b = a.clone();
// a,bから参照するため参照カウンタは2

Rc<T>型はmutableなオブジェクトを直接共有できない。変更可能なフィールドは後述するRefCell型またはCell型で保持する必要がある。

Rc型はスレッド境界をまたぐことが出来ない(コンパイルエラーになる)。複数スレッド間でオブジェクトを共有するには後述するArc型を用いる。

let a = Rc::new(100);

std::thread::spawn(move || {
// error: the trait `core::marker::Send` is not implemented for the type `alloc::rc::Rc<i32>` [E0277]
  println!("{}", a);
});

NOTE: Rust1.0以前を対象にした記事では、アットマーク@を型修飾子のように利用しているものがある。型Tに対する@Tは概ねRc<T>と読み替えればよい(既にセマンティクスも異なるが大雑把な対応付けとして)。RFC 256参照。

RefCell/Cell型

immutableなオブジェクトの一部としてmutableなサブオブジェクトを保持する仕組み(interior mutability)として、RefCell型(std::cell::RefCell)およびCell型(std::cell::Cell)が提供される。RefCellは通常の型に、Cellはコピー・セマンティクスの型に対して使用する。

use std::cell::RefCell;

#[derive(Debug)]
struct Bar { v: i32 }
struct Foo {
  bar: RefCell<Bar>
}

impl Foo {
  fn new(v: i32) -> Foo {
    Foo { bar: RefCell::new(Bar{ v: v }) }
  }
}

// fooはimmutable変数/foo.barオブジェクトもimmutable
let foo: Foo = Foo::new(0);
{
  // RefCell<Bar>内部Bar型オブジェクトのmutable参照を取得
  let mut b = foo.bar.borrow_mut();
  b.v = 42;
}
println!("{:?}", foo.bar.borrow());  // Bar { v: 42 }

RefCell型の中身にアクセスするためborrow()borrow_mut()関数が提供されるが、既にmutable参照を借用中/他の参照を借用中の場合はpanicを引き起こすことに注意(コンパイル時borrow checkerの検査対象外)。

Cow型

Clone-on-WriteスマートポインタとしてCow型(std::borrrow::Cow)が提供される。mutable(Write)操作または所有権を必要とするまでは、immutableな単一オブジェクトを共有できる。

Cow<'static, str>型は、immutalbe文字列リテラル'static strとmutable文字列Stringを効率的に取り扱うのにも利用できる。

use std::borrow::Cow;

fn fizz_buzz(i: i32) -> Cow<'static, str> {
  if i % 15 == 0 {
    "FizzBuzz".into()
    // Cow::Borrowed("FizzBuzz")でもOK
  } else if i % 5 == 0 {
    "Buzz".into()
  } else if i % 3 == 0 {
    "Fizz".into()
  } else {
    i.to_string().into()
    // Cow::Owned(i.to_string())でもOK
  }
}

for i in 1..21 {
  print!("{} ", fizz_buzz(i));
}

上記コードでは、Intoトレイト(std::convert::Into)のinto()関数を利用して適切なCow::Borrowed/Cow::Ownedに自動変換している。6

Arc型とSyncトレイト

異なるスレッド間でimmutableオブジェクトを共有する仕組みとして、アトミックな参照カウント(atomically reference count)方式によるArc型(std::sync::Arc)が提供される。Arc型はスレッド間共有を許可するRc型といえる。

use std::sync::Arc;

let shared_v: Arc<Vec<i32>> = Arc::new(vec![1, 2, 3, 4]);
for i in 0..4 {
  // 新スレッド用にArc参照カウント+1
  let thread_vec: Arc<Vec<i32>> = shared_v.clone();

  // 変数thread_viewをムーブキャプチャ
  thread::spawn(move || {
    // thread_vecから読み取り操作
    println!("{}", thread_vec[i]);
  });
}

スレッド境界を越えてオブジェクトを安全に共有するには、Syncトレイト(std::marker::Sync)が実装されている必要がある。mutableサブオブジェクトを格納するRefCell/Cell型は同トレイトを実装しないため、スレッド境界をまたいで共有することが出来ない(データ競合可能性をコンパイル時に検査)。7

use std::cell::Cell;
use std::sync::Arc;

let shared_v: Arc<Cell<i32>> = Arc::new(Cell::new(42));
for i in 0..3 {
  let thread_view: Arc<Cell<i32>> = shared_v.clone();

  std::thread::spawn(move || {
  // error: the trait `core::marker::Sync` is not implemented for the type `core::cell::UnsafeCell<i32>` [E0277]
    thread_view.set(i);
  });
}

スレッド間でmutableオブジェクトを共有するには、後述するMutex型やRwLock型を利用する必要がある。

Mutex/RwLock型

異なるスレッド間でmutableなオブジェクトを共有するには、排他制御Mutex型(std::sync::Mutex)またはReader-Writerロック制御RwLock型(std::sync::RwLock)を用いる。これらの型は保護対象オブジェクトを内包し、アクセス時にはロック獲得が必須となっている。また各スレッドで排他制御オブジェクトを共有するには、Arc型と組み合わせる必要がある(常にArc<Mutex<T>>, Arc<RwLock<T>>の形で利用する)。

use std::sync::{Arc, Mutex};

let shared_v: Arc<Mutex<i32>> = Arc::new(Mutex::new(42));
for i in 0..3 {
  // 新スレッド用にArc参照カウント+1
  let thread_view: Arc<Mutex<i32>> = shared_v.clone();

  // 変数thread_viewをムーブキャプチャ
  std::thread::spawn(move || {
    // thread_vecの排他ロックを獲得
    let mut obj: &mut i32 = thread_view.lock().unwrap();

    *obj = i;

    // objの所有権放棄=ロック解放
  });
}

チャネル

メッセージ・パッシング方式のスレッド間通信をサポートするため、Multi-Producer/Single-Consumer FIFOキュー(std::sync::mpscモジュール)によるチャネル機構が提供される。非同期送信チャネルchannel()と、上限付き同期送信チャネルsync_channel()の2種類がある。受信側は非ブロッキング動作try_recv()とブロッキング動作recv()をサポートする。

use std::thread;
use std::sync::mpsc::channel;

let (tx, rx) = channel();
for i in 0..10 {
  let tx = tx.clone();
  thread::spawn(move|| {
    // データ送信(10スレッド並行)
    tx.send(i).unwrap();
  });
}

for _ in 0..10 {
  // データ受信
  let j = rx.recv().unwrap();
  assert!(0 <= j && j < 10);
}

NOTE: 複数チャネルからの選択的受信処理(Go言語のselect/case構文に相当)には、Select型(std::sync::mpsc::Select)とselect!マクロが提供される。Rust 1.0 Stable Channelではまだunstableのため利用不可。手軽に使えないとメッセージ・パッシング方式の実用は厳しい…


  1. 文字列リテラルはstatic lifetime、つまりプログラム全域の生存期間(lifetime)を持つ。生存期間を明示する場合は&'static str型で参照する。

  2. "warning: variable does not need to be mutable, #[warn(unused_mut)] on by default"

  3. コンパイルエラーは「型TSizedトレイトを実装しない」となる。トレイトオブジェクト(trait object)型Tのサイズがコンパイル時には定まらないという意味。

  4. CopyトレイトはCloneトレイトを継承しているため、Copyトレイト実装にはCloneトレイトの実装が必須。逆に、Cloneトレイトのみ実装は可能(clone()関数によって明示的に複製可能だが、基本はムーブセマンティクスの型となる)。

  5. スライス型[T]はdynamically sized type。

  6. Intoトレイト実装ではFromトレイト(std::convert::From)に転送し、impl From<&'a str> for Cow<'a, str>またはimpl From<String> for Cow<'a, str>から適切な関数が自動選択されている。ような気がする。ドキュメンテーションの圧倒的な不足感。

  7. rustcコンパイラのエラーメッセージは「UnsafeCell型がSyncトレイト未実装」となる。これはRefCell, CellUnsafeCell型フィールドを内包し、かつUnsafeCell<T>Syncトレイトを明示的に実装しないよう宣言されているため。エラーメッセージが不親切な気がする。

102
94
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
102
94

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?