予測性
スマートポインタがinherentメソッドを持っていない (C-SMART-PTR)
例えば、Box::into_raw
は次のように定義されています。
#![allow(unused)] fn main() { impl<T> Box<T> where T: ?Sized { fn into_raw(b: Box<T>) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; let ptr = Box::into_raw(boxed_str); }
もしこれがinherentメソッドであったら、呼び出そうとしているメソッドがT
のものなのか
Box<T>
のものなのか区別が付かなくなります。
#![allow(unused)] fn main() { impl<T> Box<T> where T: ?Sized { // Do not do this. fn into_raw(self) -> *mut T { /* ... */ } } let boxed_str: Box<str> = /* ... */; // スマートポインタのDerefを経由してstrのメソッドにアクセスしている boxed_str.chars() // これは`Box<str>`のメソッド……? boxed_str.into_raw() }
変換メソッドが最も関係の深い型に付いている (C-CONV-SPECIFIC)
迷ったら_from
よりもto_
/as_
/into_
を選んでください。
後者の方がより使いやすく、また他のメソッドにチェーンすることもできるからです。
2つの型の間の変換において、多くの場合どちらか一方が明らかに特徴的です。
すなわち、他方にはない不変条件や解釈が追加されています。
例えばstr
はUTF-8でエンコードされたバイト列ですから、単なるバイト列である&[u8]
より特徴的です。
変換メソッドは、関係する型の中で、より特徴的なものが持つべきです。
従って、str
はas_bytes
メソッド及びfrom_utf8
コンストラクタを持つのです。
この方が直感的であるだけでなく、&[u8]
のような型が無数の変換メソッドで汚染されていくという事態が避けられます。
明確なレシーバを持つ関数がメソッドになっている (C-METHOD)
特定の型と強く関連した操作についてはメソッドにしてください。
#![allow(unused)] fn main() { impl Foo { pub fn frob(&self, w: widget) { /* ... */ } } }
関数にしてはいけません。
#![allow(unused)] fn main() { pub fn frob(foo: &Foo, w: widget) { /* ... */ } }
関数でなくメソッドを選ぶことには多数の利点があります。
- インポートしたり関数へのパスを記述したりする必要がない。その型の値さえあれば必要な操作ができます。
- 呼び出し時に自動借用が働きます。 (可変借用も含めて)
- 「この型
T
で何ができるんだろう」という疑問への答えが簡単になります。 (特にrustdocを使用している場合) self
記法が使われるため、より簡潔かつ明白に所有権の区別が示されます。
関数がoutパラメータを持たない (C-NO-OUT)
例えば複数のBar
を返すときはこのようにしてください。
#![allow(unused)] fn main() { fn foo() -> (Bar, Bar) }
このようにoutパラメータのようなものを取ってはいけません。
#![allow(unused)] fn main() { fn foo(output: &mut Bar) -> Bar }
タプルや構造体を使って複数の値を返しても効率のよいコードにコンパイルされますし、 ヒープの確保も行われません。複数の値を返す必要があるならこれらの型を利用すべきです。
例外は関数が呼び出し側の所有するデータを変更する場合です。 例えば、バッファの再利用をする場合は次のようになるでしょう。
#![allow(unused)] fn main() { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> }
奇妙な演算子オーバーロードを行っていない (C-OVERLOAD)
組み込みの演算子(*
や|
など)はstd::ops
にあるトレイトを実装することで使えるようになります。
これらの演算子には元から意味が付与されています。
例えば、Mul
は乗算のような(そして結合性などの特性を共有した)演算にのみ実装されるべきです。
Deref
とDerefMut
を実装しているのはスマートポインタだけである (C-DEREF)
Deref
トレイトはコンパイラによって様々な状況で暗黙的に使われ、メソッドの解決と関わります。
その周辺の規則はスマートポインタを念頭において設計されているため、
これらのトレイトはスマートポインタに対してのみ実装されるべきです。
標準ライブラリでの例
コンストラクタはスタティックなinherentメソッドである (C-CTOR)
Rustにおいて、「コンストラクタ」は単なる慣習に過ぎません。 コンストラクタの命名には様々な慣習があり、その区別が分かり辛いことが多々あります。
最も基本的なコンストラクタの形は引数のないnew
メソッドです。
#![allow(unused)] fn main() { impl<T> Example<T> { pub fn new() -> Example<T> { /* ... */ } } }
コンストラクタは、その生成する型のスタティック(self
を取らない)なinherentメソッドです。
型をインポートする慣習と併せれば、分かりやすく簡潔にその型を生成することができます。
#![allow(unused)] fn main() { use example::Example; // Construct a new Example. let ex = Example::new(); }
The name new
should generally be used for the primary method of instantiating
a type. Sometimes it takes no arguments, as in the examples above. Sometimes it
does take arguments, like Box::new
which is passed the value to place in the
Box
.
主にI/Oリソースを表す型では、File::open
、Mmap::open
、
TcpStream::connect
、あるいは [UpdSocket::bind
]のように
コンストラクタの命名が異なっていることがあります。
これらは、各々の領域で適した名前が選ばれています。
ある型の値を生成する方法が複数存在することは多くあります。
そういった場合、2個目以降のコンストラクタには_with_foo
などと名前の最後に付けることが一般的です。
例えば、Mmap::open_with_offset
などです。
複数のオプションがあるならばビルダーパターン(C-BUILDER)の使用も考えてください。
別の型の値を取って変換を行うコンストラクタというものもあります。
それらはstd::io::Error::from_raw_os_error
のように、一般にfrom_
から始まる名前を持ちます。
ここで、よく似たものにFrom
トレイト(C-CONV-TRAITS)が存在します。
from_
の付いた変換コンストラクタとFrom<T>
実装の間には3つの相違点があります。
from_
コンストラクタはunsafeにすることができますが、From
の実装ではできません。 例:Box::from_raw
。from_
コンストラクタは、u64::from_str_radix
のように 元になるデータを区別するための追加の引数を取ることができます。From
実装は元のデータから出力の型のエンコード方法を決定できる場合にのみ適しています。u64::from_be
やString::from_utf8
のように入力の型が単なるデータ列であるとき、 コンストラクタの名前によってその意味を伝えることが可能です。
標準ライブラリでの例
std::io::Error::new
はIOエラーの生成に使われるコンストラクタstd::io::Error::from_raw_os_error
は OSから与えられたエラーコードを変換するコンストラクタBox::new
は引数を1つとり、コンテナ型を生成するコンストラクタFile::open
はファイルをオープンするMmap::open_with_offset
は指定されたオプションでメモリーマップをオープンする