錆:牛から&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関数は非常に簡単に定義されますselfIntoを実装するもの)を受け取り、 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汎用引数があります。文字列は、可能なタイプSInto<String>を実装するものに制限します。
標準ライブラリは既に&strおよびStringInto<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&strString両方を保存でき&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")); 

これで、トークンは&strString両方から透過的に作成できます。 トークン関連のライフタイムはもはや問題ではありません
スタック上に作成されたデータ。 スレッド間でトークンを送信することもできます!


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



Source: https://habr.com/ru/post/J282708/


All Articles