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!("Odd number error (found {})", n))
}
}
解説
これでは失敗したとき、エラーメッセージを使わないとしてもString
をヒープに作ってしまいます。
また、エラーメッセージを標準出力に書き出したいとしてもやはり一度String
を作ってから書き込むのは無駄なことです。
文字列を持つのではなく、文字列を作ることのできる情報だけ持たせて必要なとき必要な場所に書き込むのがRustらしいやり方です。
use std::fmt;
use std::error;
#[derive(Debug)]
struct OddNumError {
n: u32
}
impl OddNumError {
fn new(n: u32) -> Error {
OddNumError { n }
}
}
impl fmt::Display for OddNumError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(w, "Odd number error (found {})", self.n)
}
}
impl error::Error for OddNumError {};
fn divide_by_2(n: u32) -> Result<u32, OddNumError> {
if n % 2 == 0 {
Ok(n / 2)
} else {
Err(OddNumError::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_two(values: &[u32]) -> &[u32] { values[0..2] }
のように引数の参照の一部を返すことはできます。)
配列型を新たに作りたい場合、返り値は[T; N]
かVec<T>
になります。
Javaなど言語ではArrayList<T>
とT[]
は数倍の速度差がありますが、RustのVec<T>
と&[T]
はcapacity
分の場所を取るだけで速度差はないはずです。
しかし長さが固定であることを型で明示したい場合Box<[T]>
を返すということもできます。
Box
はメモリを所有するポインタ型なのでそのようなことができます。
Box<[T]>
はVec::into_boxed_slice
というメソッドから作ることができますが、Vec<T>
のときに持っていた余分なキャパシティを切り落とすためにメモリの再確保が行われるかもしれないことには注意したほうがいいでしょう。
二重配列の型に何を使うべきかがわからない
長さがコンパイル時にわかるなら[[T; N]; M]
です。
そうでないならVec<Vec<T>>
です。[^1]
Vec<Vec<T>>
を引数として受け取るときは&[Vec<T>]
になります。
&[&[T]]
を引数にしたくなるかもしれませんがそれはできません。
&[&[T]]
は&[T]
が並んでいるということですが、Vec<Vec<T>>
を作成したとき、メモリ上に&[T]
の並びはないからです。
おわりに
必ずしもやってはいけないことばかりではありませんが、どれもやるならばわかった上でやったほうがいいことだと思います。
他にもハマりがちなことがありましたらコメントでお知らせください。