歴史: Rustの標準ライブラリがCow(Clone-on-Write)を導入した際、その目的は即座にアロケーションを強制せずに借用データまたは所有データに対して抽象化することでした。Cloneトレイトが最初に考慮されましたが、それは同じ型の同一のコピーを生成することのみを許可します。借用データである**&strをクローンすると、変換のために必要な所有するStringではなく別の参照が生成されます。ToOwnedトレイトは、借用された形と所有された形の関係をその関連するOwned**型を通じて表現するために特別に設計されました。
問題: CowがCloneに依存している場合、変更のためにCow::Borrowed(&str)を所有表現に変換するには外部の変換ロジックが必要になります。Cloneは&strをStringに変換するための型レベルのメカニズムが欠けており、これにより構築時に早すぎるアロケーションが強いられるか、複雑な手動状態管理が必要になります。これは、変更が実際に必要になるまでヒープアロケーションを遅延させることが不可能になるため、Cowのゼロコスト抽象化の原則に違反します。
解決策: ToOwnedはtype Ownedとfn to_owned(&self) -> Self::Ownedを定義し、&strがOwned = Stringを指定することを可能にします。これにより、Cow::to_mut()は変更が要求された場合にのみ遅延アロケーションを実施します。CowがすでにOwnedであれば、アロケーションなしで既存データへの可変参照を返します。以下の例がこの効率性を示しています:
use std::borrow::Cow; fn normalize_whitespace(input: &str) -> Cow<'_, str> { if input.contains(" ") { let cleaned = input.replace(" ", " "); Cow::Owned(cleaned) // ここでのみアロケート } else { Cow::Borrowed(input) // ゼロコストの借用 } }
高スループットのログ処理サービスがメモリマップされたファイルからソースされたエントリのタイムスタンプを正規化する必要がありました。入力はマップを指す**&strスライスとして到着しましたが、約10%のエントリはタイムゾーンの調整が必要で、Stringのアロケーションが必要でした。最初の実装では、Stringと&str**のバリアントを持つカスタム列挙型が使用され、すべてのアクセス点で徹底したパターンマッチングとエラーが発生しやすい冗長な手動のクローンロジックが必要とされました。
代替案1: 文字列への早期変換。 チームは、すべての入力を摂取時に即座にStringに変換することを検討しました。このアプローチはデータモデルを単純化し、ライフタイムの懸念を排除しましたが、深刻なメモリーオーバーヘッドを強いました。ピーク時には、変更を必要としない90%のログに対してメモリ使用量が倍増し、10GBファイルを処理する際にOOMエラーが発生しました。
代替案2: copy-on-writeを使用したArc<str>。 別のオプションは、不変共有のためにArc<str>を使用し、変更のためにはArc::make_mutを使用することでした。これにより共有所有セマンティクスが提供されましたが、すべてのアクセスに対して原子参照カウントのオーバーヘッドが導入されました。加えて、共有からミュータブルへの移行を処理するためには明示的なロジックが必要であり、借用モデルが複雑化し、望ましい使いやすさを提供しませんでした。
代替案3: Cow<'_, str>を採用する。 チームはこの2つの状態を抽象化するためにCowを選びました。Borrowedバリアントはアロケーションなしでメモリマップを直接指しましたが、Ownedバリアントは修正された文字列を保持しました。この解決策は、**to_mut()**が最初の変更が発生するまでアロケーションを遅延させ、読み取り専用の経路にゼロコストを維持しながら統一されたAPIを提供するために選ばれました。
結果: パーサーは高いスループットを維持し、実際のヒープアロケーションが200MBのみで10GBのログファイルを処理しました。Cowを活用することで、システムは手動の状態追跡を排除し、並列処理のためのSendおよびSyncプロパティを維持し、カスタム列挙型アプローチと比較してコードの複雑さを60%削減しました。
into_ownedはToOwned::Ownedを値として返しますが、これはスタックスペースをアロケートするためにコンパイル時に知られたサイズを必要とします。CowはCow<', str>を介して未サイズ型のような型をラップできますが、Owned型(String)はサイズを持っています。候補者はしばしばCow<', T>とCow<'_, &T>を混同し、参照ではなく借用型に対してトレイトを実装しようとします。ToOwned::OwnedにSized束縛がなければ、コンパイラはinto_ownedの戻り値を構築できず、未サイズのstrを直接返そうとすることになってしまいます。
CowはBorrow<Borrowed>を実装しており、ここでBorrowed: ToOwnedであるため、Cow<String>は&strを使ってルックアップできます。しかし、Borrowは厳密な契約を課します:もし2つの値がEqを介して等しい場合、それらは同一のハッシュ値を生成しなければなりません。候補者はしばしばCowのカスタムPartialEqを実装(例:大文字小文字を区別しない比較)しながら、標準のHash実装を保持します。これは契約に違反します。なぜなら、2つのCow値はカスタムロジックの下で等しいと比較されるかもしれませんが、Hash実装が元のバイトを見ると異なるハッシュを持つ可能性があるからです。これにより、鍵が存在するように見えても見つけられないHashMapのルックアップ失敗が発生します。
Borrowedバリアントを構築するために、Cowはライフタイム**'aを持つ参照&'a Bを必要とします。一般的なDefault実装は、'static**(例:&'static strで**"")に有効な参照を生成する必要がありますが、&str自体はDefaultを実装していません。候補者はしばしばCow::Borrowed("")をデフォルトにすることを提案しますが、これにはBに対する'staticライフタイムの束縛や、安定したRustでは利用できない特殊化が必要です。したがって、標準ライブラリはToOwned::Owned: Default**を要求し、空のデフォルト値に対しても(アロケーションである)Cow::Owned(String::new())を強制します。候補者は、特定のスコープでの文字列リテラルの可用性を借用型に対する一般的なDefault実装と混同してこの違いを見落とします。