LoginSignup
211
144

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-03-24

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]の並びはないからです。

おわりに

必ずしもやってはいけないことばかりではありませんが、どれもやるならばわかった上でやったほうがいいことだと思います。
他にもハマりがちなことがありましたらコメントでお知らせください。

211
144
5

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
211
144