Rustで最初に書いたものの1つは、 &str
フィールドを持つ構造です。 ご存知のように、借用アナライザーでは多くのことを実行できず、APIの表現力が大幅に制限されていました。 この記事の目的は、構造のフィールドに生のリンクとstrリンクを保存するときに生じる問題と、それらを解決する方法を示すことです。 その過程で、このような構造の使いやすさを向上させる中間APIをいくつか示しますが、同時に生成されたコードの効率を低下させます。 最終的には、表現力と非常に効果的な実装を提供したいと思います。
example.comサイトAPIで動作する何らかのライブラリを作成していることを想像してみましょう。次のように定義するトークンで各呼び出しに署名します。
次に、 &str
からトークンのインスタンスを作成するnew
関数を実装し&str
。
impl<'a> Token<'a> { pub fn new(raw: &'a str) -> Token<'a> { Token { raw: raw } } }
このようなナイーブトークンは、バイナリに直接埋め込まれている&'static str
な行&'static str
に対してのみ有効です。 ただし、ユーザーがコードに秘密鍵を埋め込むことを望まないか、何らかの秘密ストアからそれをロードしたいとします。 次のようなコードを書くことができます。
このような実装には大きな制限があります。トークンは秘密鍵を生き残ることができません。つまり、スタックのこの領域を離れることはできません。
しかし、 Token
が&str
代わりにString
を格納する場合はどうなり&str
か? これは、構造の寿命を示すパラメータを取り除き、所有タイプに変換するのに役立ちます。
トークンと新しい関数に変更を加えましょう。
struct Token { raw: String, } impl Token { pub fn new(raw: String) -> Token { Token { raw: raw } } }
String
提供されるすべての場所を修正する必要があります。
ただし、これは&'str
使いやすさを損ない&'str
。 たとえば、このようなコードはコンパイルされません。
このAPIのユーザーは、明示的に&'str
を文字&'str
に変換する必要があり&'str
。
let token = Token::new(String::from("abc123"));
実装内でString::from
にすることで、新しい関数でString
代わりに&str
を使用できますが、 String
の場合、これはあまり便利ではなく、ヒープに追加のメモリ割り当てが必要になります。 それがどのように見えるか見てみましょう。
ただし、Stringを渡す場合にメモリを割り当てる必要なく、newに両方のタイプの引数を強制的に受け入れる方法があります。
会う
標準ライブラリには、新しい問題を解決するのに役立つInto
特性があります。 タイプ定義は次のようになります。
pub trait Into<T> { fn into(self) -> T; }
into
関数は非常に簡単に定義されますself
( Into
を実装するもの)を受け取り、 T
型の値を返しますT
これを使用する方法の例を次に示します。
impl Token {
ここでは多くの興味深いことが起こっています。 まず、この関数にはraw
タイプS
汎用引数があります。文字列は、可能なタイプS
をInto<String>
を実装するものに制限します。
標準ライブラリは既に&str
およびString
にInto<String>
を提供しているため、ケースは追加の身体の動きなしですでに処理されています。 [1]
このAPIを使用する方がはるかに便利になりましたが、まだ顕著な欠点があり&str
に&str
を渡すには、 String
として格納するメモリを割り当てる必要があり&str
。
タイプカウは私たちを救います[2]
標準ライブラリには、 std :: borrow :: Cowという特別なコンテナがあります。
これにより、一方でInto<String>
利便性を維持し、他方で構造体が型&str
値を所有できるようになり&str
。
恐ろしい見た目の牛の定義は次のとおりです。
pub enum Cow<'a, B> where B: 'a + ToOwned + ?Sized { Borrowed(&'a B), Owned(B::Owned), }
この定義を理解しましょう:
Cow<'a, B>
は、2つの一般化されたパラメーターがあります:ライフタイム'a
およびいくつかの一般化されたタイプB
には、 'a + ToOwned + ?Sized
制限があります。
それらをさらに詳しく見てみましょう。
- タイプ
B
のライフタイムを'a
より短くすることはできません ToOwned
- B
はToOwned
実装する必要がありToOwned
。これにより、借りたデータをコピーして所有権を移すことができます。?Sized
-タイプB
サイズは、コンパイル時に不明な場合があります。 私たちのケースではこれは重要ではありませんが、特性オブジェクトをCow
で使用できることを意味します。
Cow
コンテナが保存できる値には2つのオプションがあります。
Borrowed(&'a B)
-タイプB
オブジェクトへの参照。コンテナのライフタイムは、それに関連付けられたB
の値とまったく同じです。Owned(B::Owned)
-コンテナは、関連付けられたタイプB::Owned
値を所有します
enum Cow<'a, str> { Borrowed(&'a str), Owned(String), }
要するに、 Cow<'a, str>
は、 'a
のライフタイムを持つ&str
なるか、このライフタイムに関連付けられていないString
になります。
それは私たちのタイプのToken
にとってはクールに聞こえToken
。 &str
とString
両方を保存でき&str
。
struct Token<'a> { raw: Cow<'a, str> } impl<'a> Token<'a> { pub fn new(raw: Cow<'a, str>) -> Token<'a> { Token { raw: raw } } }
これで、所有型と借用型の両方からToken
を作成できますが、APIの使用はあまり便利ではなくなりました。
Into
は、以前の単純なString
場合と同じように、 Cow<'a, str>
に対して同じ改善を行うことができCow<'a, str>
。 トークンの最終的な実装は次のようになります。
struct Token<'a> { raw: Cow<'a, str> } impl<'a> Token<'a> { pub fn new<S>(raw: S) -> Token<'a> where S: Into<Cow<'a, str>> { Token { raw: raw.into() } } }
これで、トークンは&str
とString
両方から透過的に作成できます。 トークン関連のライフタイムはもはや問題ではありません
スタック上に作成されたデータ。 スレッド間でトークンを送信することもできます!
let raw = String::from("abc"); let token_owned = Token::new(raw); let token_static = Token::new("123"); thread::spawn(move || { println!("token_owned: {:?}", token_owned); println!("token_static: {:?}", token_static); }).join().unwrap();
ただし、静的でないリンクの有効期間でトークンを送信しようとすると失敗します。
実際、上記の例はエラーでコンパイルされません。
error: `raw` does not live long enough
他のサンプルが必要な場合は、Cowを多用するPagerDuty APIクライアントをご覧ください。
読んでくれてありがとう!
注釈1
&strおよびStringのInto<String>
実装を探している場合、それらは見つかりません。 これは、Fromトレイトを実装するすべてのタイプの一般化されたInto実装があるためです;このように見えます。
impl<T, U> Into<U> for T where U: From<T> { fn into(self) -> U { U::from(self) } }
2
翻訳者のメモ:オリジナルの記事では、CowまたはCopy on writeセマンティクスの原則については何も言われていません。
要するに、コンテナのコピーを作成するときに実際のデータがコピーされない場合、実際の分離は、コンテナ内に格納されている値を変更しようとしたときにのみ行われます。