Edited at

Rustを覚えて間もない頃にやってしまいがちなこと

Rustを使っていくうちにやらないほうがいいと気づいたことを挙げていこうと思います。

少なくとも自分はプログラミング言語Rustを読んだだけでは以下で説明することに気づけませんでした。


v: Vec<T>&[T]に変換するためにいちいちv.as_slice()&v[..]と書く


解説

Vec<T>[T]へのDerefトレイトを実装しているので、v: Vec<T>&をつけるだけで必要に応じて&[T]に変換されます。(参照: TRPL, 1st ed, Derefによる型強制)

Vec[T]どちらであるべきかコンパイラが判断できない文脈では、スライスがほしい場合明示的に&v[..]などと書いてやる必要がありますが、基本的には適当に&をつけるだけで問題ありません。

もちろん何らかの意図があってそう書かないのはまったく問題ありません。


イミュータブルなVecを関数の引数に取る


fn second(values: &Vec<usize>) -> usize {

v[2]
}


解説

イミュータブルな場合、スライスとVecの違いはcapacityメソッドがあるかどうかだけです。

たいていの場合capacityメソッドは使っていませんし、fn second(values: &[usize]) -> usize としておくと配列の場合にも関数が利用できます。(しかもVecは内部の配列データへの参照なので、&Vec<T>&[T]に比べて参照が一段多く、パフォーマンス上のコストがあるかもしれません)

つまりVec関連の関数の引数の型としてありえるのは基本的に&mut Vec<T>, &[T], &mut [T]ということになります。

&mut Vec<T>&mut [T]はプッシュやポップのような構造的な変更を行うかどうかで使い分けます。


基本的なトレイトを積極的に実装しない

自分は構造体を定義して、コンパイラに怒られては#[derive(Copy, Clone)]等を書き足すといったことをしていました。

しかし個人プロジェクトなら構造体を定義した時点でderiveで実装できるトレイトは実装してしまうのがいいかと思います。

煩わしいコンパイルエラーは少しでも見ずに済みたいですよね。

また、外部から利用される可能性がある場合でも、将来的に実装の変更でそのトレイトが実装できなくなる恐れがない限り基本的なトレイトはできるだけ実装することが推奨されます。(参照: Rust APIガイドライン, 相互運用性)

外部のクレートが利用しているとき、Debugのような自分のクレートで定義されていないトレイトの実装を追加したいとしてもできないからです。

deriveできるトレイトの一覧はRust by Exaple, deriveにあります。


一々Stringを作る


偶数のときに2で割った値を返し、奇数のときにエラーを返す関数を作りたいときに次のようなコードを書いていました。

use std::fmt;

use std::error;

#[derive(Debug)]
struct Error {
msg: String
}

impl Error {
fn new(msg: String) -> Error {
Error { msg }
}
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(w, "{}", &self.msg)
}
}

impl error::Error for Error {};

fn divide_by_2(n: u32) -> Result<u32, Error> {
if n % 2 == 0 {
Ok(n / 2)
} else {
Err(Error::new(format!("`n` should be even, but found {}", n))
}
}


解説

これでは失敗したときにエラーメッセージを使わないとしてもStringをヒープに作ってしまいます。

また、エラーメッセージを標準出力に書き出したいとしてもやはり一度Stringを作ってから書き込むのは無駄なことです。

文字列を持つのではなく、文字列を作ることのできる情報だけ持たせて必要なとき必要な場所に書き込むのがRustらしいやり方です。

use std::fmt;

use std::error;

#[derive(Debug)]
struct DivBy2Error {
n: u32
}

impl DivBy2Error {
fn new(n: u32) -> Error {
DivBy2Error { n }
}
}

impl fmt::Display for DivBy2Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(w, "`n` should be even, but found {}", self.n)
}
}

impl error::Error for Error {};

fn divide_by_2(n: u32) -> Result<u32, Error> {
if n % 2 == 0 {
Ok(n / 2)
} else {
Err(Error::new(n))
}
}


スライスを作る関数を書こうとする

結論から言うとこれは不可能です (引数にVecなどスライスの元になる型を取らない限り)。

長さが固定の配列を初期化したかったのですが、スライスは長さが固定の配列のことだという知識をから以下のようにスライスを返す関数を作ろうとしました。

/// いい感じに初期化されたスライスを返す。

fn new(n: usize) -> [u32] {
let mut a = [0; n];

// aをいい感じに初期化

a
}

これはコンパイルが通りません。

返り値や引数の型はコンパイル時に大きさがわかるものでなければなりませんが、スライスは大きさが実行時にしかわからない型です。

さらに[0; n]の部分も配列の長さはコンパイル時にわかってなければならないので実行時の情報である引数を利用していることをコンパイラが許しません。


やってみたこと

スライス自体は大きさが不定でもスライスへの参照はポインタだから大きさが定まるはずだということでスライスへの参照を返そうと思いました。

また、引数nに応じて必要なスライスのサイズnは変わってくるので一時的にVecを作ることにしました

fn new(n: usize) -> &[u32] {

let mut v = Vec::with_capacity(n);

// vをいい感じに初期化

&v
}

しかしこれもコンパイルが通りません。

返り値の参照のライフタイムを明示するよう言われます。

そこで fn new<'a>(n: usize) -> &'a [u32] のように書くと今度はローカル変数への参照を返していると咎められます。(ローカル変数はスコープから抜けるとき寿命を終えてその変数が所有するメモリも解放されるので、スライスは解放されたメモリへの参照になってしまいます)

他にもいろいろ頑張りましたがうまくいきませんでした。


解説

実行時に大きさが決定するものはヒープに置かなければなりません。

そしてRustではメモリの所有者が1つだけであることがよく強調されますが、メモリの所有者が1つは存在しなければならないというところが今回の鍵です。

つまりn: usizeという値だけからメモリを所有しない参照を返すことはできないのです。(fn first_to_second(values: &[u32]) -> &[u32] { values[0..2] }のように引数の参照の一部を返すことしかできない。)

ですので、基本的に配列状の型を新たに作りたい場合返り値は[T; N]Vec<T>になります。

RustのVec<T>とスライスはJavaなどのArrayListと配列ほどの速度差はないのですが、長さが固定であることを型で明示したい場合Box<[T]>を返すということもできます。

Boxはメモリを所有するポインタを表す型なのでそのようなことができます。

Box<[T]>Vec::into_boxed_sliceというメソッドから作ることができますが、Vec<T>のときに持っていた余分なキャパシティを切り落とすためにメモリの再確保が行われるかもしれないことに注意が必要です。


二重配列の型に何を使うべきかがわからない

長さがコンパイル時にわかるなら[[T; N]; M]です。

そうでないならVec<Vec<T>>です。(ただし、Vec<T>を内部に持ち、Index<[usize; 2]>を実装する型を作ると参照の回数が減りインデクシングがより効率的になります)

引数として取るときは少し汚く見えるかもしれませんが&[Vec<T>]です。

&[&[T]]を引数にしたくなるかもしれませんがVec<Vec<T>>を扱っているときにそれはできません。

&[&[T]]は入れ子の内側が&[T]であるのに対し、Vec<Vec<T>>は入れ子の内側がVec<T>と異なっているため、新たにVec<&[T]>を作成してそこから更にスライスを作成しない限り得ることができないからです。


おわりに

ここまで読んでいかがでしたでしょうか。

必ずしもやってはいけないことばかりではありませんが、どれもやるならばわかった上でやったほうがいいことだと思います。

他にもハマりがちなことがありましたらコメントでお知らせください。