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セマンティクスの原則については何も言われていません。
要するに、コンテナのコピーを作成するときに実際のデータがコピーされない場合、実際の分離は、コンテナ内に格納されている値を変更しようとしたときにのみ行われます。