錆:牛から&str

Rustで最初に書いたものの1つは、 &str



フィールドを持つ構造です。 ご存知のように、借用アナライザーでは多くのことを実行できず、APIの表現力が大幅に制限されていました。 この記事の目的は、構造のフィールドに生のリンクとstrリンクを保存するときに生じる問題と、それらを解決する方法を示すことです。 その過程で、このような構造の使いやすさを向上させる中間APIをいくつか示しますが、同時に生成されたコードの効率を低下させます。 最終的には、表現力と非常に効果的な実装を提供したいと思います。







example.comサイトAPIで動作する何らかのライブラリを作成していることを想像してみましょう。次のように定義するトークンで各呼び出しに署名します。







 // Token  example.io API pub struct Token<'a> { raw: &'a str, }
      
      





次に、 &str



からトークンのインスタンスを作成するnew



関数を実装し&str









 impl<'a> Token<'a> { pub fn new(raw: &'a str) -> Token<'a> { Token { raw: raw } } }
      
      





このようなナイーブトークンは、バイナリに直接埋め込まれている&'static str



な行&'static str



に対してのみ有効です。 ただし、ユーザーがコードに秘密鍵を埋め込むことを望まないか、何らかの秘密ストアからそれをロードしたいとします。 次のようなコードを書くことができます。







 // ,     let secret: String = secret_from_vault("api.example.io"); let token = Token::new(&secret[..]);
      
      





このような実装には大きな制限があります。トークンは秘密鍵を生き残ることができません。つまり、スタックのこの領域を離れることはできません。

しかし、 Token



&str



代わりにString



を格納する場合はどうなり&str



か? これは、構造の寿命を示すパラメータを取り除き、所有タイプに変換するのに役立ちます。







トークンと新しい関数に変更を加えましょう。







 struct Token { raw: String, } impl Token { pub fn new(raw: String) -> Token { Token { raw: raw } } }
      
      





String



提供されるすべての場所を修正する必要があります。







 //    let token = Token::new(secret_from_vault("api.example.io"))
      
      





ただし、これは&'str



使いやすさを損ない&'str



。 たとえば、このようなコードはコンパイルされません。







 //   let token = Token::new("abc123");
      
      





このAPIのユーザーは、明示的に&'str



を文字&'str



に変換する必要があり&'str









 let token = Token::new(String::from("abc123"));
      
      





実装内でString::from



にすることで、新しい関数でString



代わりに&str



を使用できますが、 String



の場合、これはあまり便利ではなく、ヒープに追加のメモリ割り当てが必要になります。 それがどのように見えるか見てみましょう。







 //  new  -  impl Token { pub fn new(raw: &str) -> Token { Token(String::from(raw)) } } // &str    let token = Token::new("abc123"); // -   String,     //   new       let secret = secret_from_vault("api.example.io"); let token = Token::new(&secret[..]); // !
      
      





ただし、Stringを渡す場合にメモリを割り当てる必要なく、newに両方のタイプの引数を強制的に受け入れる方法があります。







会う



標準ライブラリには、新しい問題を解決するのに役立つInto



特性があります。 タイプ定義は次のようになります。







 pub trait Into<T> { fn into(self) -> T; }
      
      





into



関数は非常に簡単に定義されますself



Into



を実装するもの)を受け取り、 T



型の値を返しますT



これを使用する方法の例を次に示します。







 impl Token { //    // //    &str   String pub fn new<S>(raw: S) -> Token where S: Into<String> { Token { raw: raw.into() } } } // &str let token = Token::new("abc123"); // String let token = Token::new(secret_from_vault("api.example.io"));
      
      





ここでは多くの興味深いことが起こっています。 まず、この関数には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



制限があります。

それらをさらに詳しく見てみましょう。









Cow



コンテナが保存できる値には2つのオプションがあります。









 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 } } } //    let token = Token::new(Cow::Borrowed("abc123")); let secret: String = secret_from_vault("api.example.io"); let token = Token::new(Cow::Owned(secret));
      
      





これで、所有型と借用型の両方から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() } } } //  . let token = Token::new("abc123"); let token = Token::new(secret_from_vault("api.example.io"));
      
      





これで、トークンは&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();
      
      





ただし、静的でないリンクの有効期間でトークンを送信しようとすると失敗します。







 //       let raw = String::from("abc"); let s = &raw[..]; let token = Token::new(s); //     thread::spawn(move || { println!("token: {:?}", token); }).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セマンティクスの原則については何も言われていません。

要するに、コンテナのコピーを作成するときに実際のデータがコピーされない場合、実際の分離は、コンテナ内に格納されている値を変更しようとしたときにのみ行われます。








All Articles