自転車本では紹介されてないけれど、Rustならでは必要とされる面白い型、Copy on Writeではなくて、Clone on Writeを意味するstd::borrow::Cow
の紹介です。
str
と String
の違いstr
-- (固定長の)UTF-8文字のスライス&str
-- UTF-8文字のスライスへのポインタString
-- ヒープ上に置かれた、追加を含めた変更ができる文字列まず、以下のように関数内のローカルデータとして確保された文字列定数を参照する変数の型を確認しておきます。
/// コンパイルできる
fn f() {
let s = "a fixed string";
...
これは &str
です。
/// コンパイルできる
fn f() {
let s: &str = "a fixed string";
...
"a fixed string"
はローカルなデータなので(Boxも使ってないし)ヒープに置く必要はありません。s
はその確保された領域を指すように型付けられます。str
は通常Rustのプログラムには出てきません。ちなみにs
がグローバル変数S
になるとどうなるでしょう。
/// 設計中
const S: ??? = "a fixed string";
グローバル変数も他の言語同様にヒープではなくOSで言うところのデータ領域に置かれるのでやはりstr
であり、 その領域を指すので変数S
は&str
型になります。
/// コンパイルできる
const S: &str = "a fixed string";
str
とString
間の変換&str
からString
へ&str
型の変数があればその指している対象からto_string
メソッドを使ってString
を作ることができます。
/// コンパイルできる
fn f() {
let s: &str = "A fixed string"
let t = s.to_string(); // ヒープ操作を必要とする
...
この時t
の実体はヒープ上に置かれたfat pointerです。固定長なのでメモリを大きく消費するわけではありませんが、実体へのポインタを含む構造体をヒープ上に構成する必要があります。
String
から&str
へ逆の操作はas_str()
です。この操作はString
を構成するfat pointerを流用すればいいので極めて軽量です。 これは&srt
からString
への変換がto_*
系なのに対し、String
から&str
への変換がas_*
系の命名になっていることからもわかります。
/// コンパイルできる
fn f() {
let s: String = "A fixed string".to_string();
let t: &str = s.as_str(); // 軽量な操作
...
ここまでが前提知識でした。
&str
と String
の混在さて、以下のような構造体S
に対してその文字列表現を返すrep()
メソッドを定義するとします。
struct S {
index: usize,
vec: Vec<usize>,
}
ただし、
index
が0の時は固定のメッセージを返す。とします。
/// 設計中
impl S {
fn rep(&self) -> ??? {
if self.index == 0 {
...
} else {
...
}
}
}
この場合、rep()
内部で固定のメッセージを保持するローカル変数mes
の値をそのまま返すことにします。 mes
の型は &str
であることからrep()
の返値型も&str
になります。
/// コンパイルできるはず
impl S {
fn rep(&self) -> &str {
if self.index == 0 {
let mes: &str = "null object";
mes
} else {
...
}
}
}
フィールドvec
の値を埋め込んだ文字列を作るためformat!
を使うことにしました。
format!
の返す型はString
なのでrep()
の返値型もString
になります。
/// コンパイルできない
impl S {
fn rep(&self) -> String {
...
} else {
format!("S{{{:?}}}", self.vec)
}
}
}
ここで型が一致しない問題に直面します。
&str
への統一既に見たようにどちらの方向にも変換できるのでまず&str
へ統一することを考えてみます。
fn rep(&self) -> &str {
if self.index == 0 {
let mes: &str = "null object";
mes
} else {
format!("S{{{:?}}}", self.vec).as_str()
}
}
}
これはライフタイム制約を満足しないエラーになります。
error[E0515]: cannot return value referencing temporary value
|
| format!("S{{{:?}}}", self.vec).as_str()
| ------------------------------^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| temporary value created here
以下のようにローカル変数res
にバインドしても、
fn rep(&self) -> &str {
if self.index == 0 {
let mes: &str = "null object";
mes
} else {
let res = format!("S{{{:?}}}", self.vec);
res.as_str()
}
}
}
ライフタイムが短すぎることには変わりはないので、エラーになります(res
はヒープに置かれても所有者であるrep
からexitする時点で回収されてしまう)
error[E0515]: cannot return value referencing local variable `res`
|
| res.as_str()
| ---^^^^^^^^^
| |
| returns a value referencing data owned by the current function
| `res` is borrowed here
なので、format!
で作ったString
実体を呼び出し側に渡さないといけません。
String
への統一ということで&str
型のmes
を返しているパスの型を変えることにします。関数の返値型を変えてコンパイルすると以下のようなエラーメッセージが表示されます。
error[E0308]: mismatched types
--> src/main.rs:13:10
|
| fn rep(&self) -> String {
| ------ expected `std::string::String` because of return type
...
| mes
| ^^^^
| |
| expected struct `std::string::String`, found `&str`
| help: try using a conversion method: `mes.to_string()`
ヘルプに従って修正すれば問題はなくなります。
/// コンパイルできる
fn rep(&self) -> String {
if self.index == 0 {
let mes: &str = "null object";
mes.to_string()
} else {
let res = format!("S{{{:?}}}", self.vec);
res.as_str()
}
}
}
しかしこれは、必要とは思われないヒープでのオブジェクト生成をしているため、時間的にも空間的にも(できれば避けたい)コストをかけてしまっています。ゼロコストアブストラクションをうたうRustのプログラムとしては是非とも避けたいところです。
この問題を解決するには「借用」と「実体」のどちらも返せるようなenum
を用意するという手が使えます。
/// 設計中(ライフタイム指定がまだついていない)
enum WrapStr {
from_str(&str),
from_format(String),
}
impl S {
fn rep(&self) -> WrapStr {
if self.index == 0 {
let mes: &str = "null object";
WrapStr::from_str(mes)
} else {
WraStr::from_format(format!("S{{{:?}}}", self.vec))
}
}
}
こうすれば型の問題は解決するし、見かけ上構造体で包むコストは(おそらく)コンパイラの最適化中に何もしないコードに変換されることが期待できます。ということでWrapStr
を追加定義すれば問題解決します。ポインタを含むのでライフタイム制約が必要かな。。。
しかし自分で定義するよりも、このような状況のための型がすでに標準ライブラリに用意されているのでそれを使いましょう。 それがClone on Write, Cow
型です。これは以下のように定義されています。 https://doc.rust-lang.org/std/borrow/enum.Cow.html
/// https://doc.rust-lang.org/std/borrow/enum.Cow.html
pub enum Cow<'a, B>
where
B: 'a + ToOwned + ?Sized,
{
Borrowed(&'a B),
Owned(<B as ToOwned>::Owned),
}
ToOwned
は借用したデータから、所有権を持つ実体を構成することができるというトレイトです。 文字列関連では以下のようになっています。 https://doc.rust-lang.org/std/borrow/trait.ToOwned.html
/// https://doc.rust-lang.org/std/borrow/trait.ToOwned.html
impl ToOwned for str
type Owned = String
/// Examples
let s: &str = "a";
let ss: String = s.to_owned();
つまりstr
からString
が作れると。これを見ながらCow
の定義のB
をstr
に変換してやると以下のようになります。
pub enum Cow<'a, str>
where
str: 'a + ToOwned + ?Sized, // 条件OK
{
Borrowed(&'a str),
Owned(String),
}
ということで、借用(&str
)はCow::Borrowed
で実体(String
)はCow::Owned
で包んでやればいいことがわかりました。
最終的なプログラムはこうなります。
use std::borrow::Cow;
impl S {
/// コンパイルできる
fn rep(&self) -> Cow<'_, str> {
if self.index == 0 {
Cow::Borrowed("Null S") // 場所は確保済み => 借用したい
} else {
Cow::Owned(format!("S[{:?}]", self.vec)) // 借用ではダメ =>実体を渡したい
}
}
}
使う側では一回derefしてやれば借用であったか実体であったかを気にする必要はありません。
println!("{}", *s.rep());
ちなみにderefしたものが何型になっているかというと、
// コンパイルできる
let temp: &str = &*s.rep();
だそうです。文字列のスライスみたいですね。
derefのコストは以下に引用したようにポインタ辿りだけなので、「軽量」と言ってしまっていいでしょう。
https://doc.rust-lang.org/src/alloc/borrow.rs.html#320
/// https://doc.rust-lang.org/src/alloc/borrow.rs.html#320
#[stable(feature = "rust1", since = "1.0.0")]
impl<B: ?Sized + ToOwned> Deref for Cow<'_, B> {
type Target = B;
fn deref(&self) -> &B {
match *self {
Borrowed(borrowed) => borrowed,
Owned(ref owned) => owned.borrow(),
}
}
}
https://doc.rust-lang.org/src/core/borrow.rs.html#212
/// https://doc.rust-lang.org/src/core/borrow.rs.html#212
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Borrow<T> for T {
fn borrow(&self) -> &T {
self
}
}
https://doc.rust-lang.org/src/core/borrow.rs.html#226
/// https://doc.rust-lang.org/src/core/borrow.rs.html#226
#[stable(feature = "rust1", since = "1.0.0")]
impl<T: ?Sized> Borrow<T> for &T {
fn borrow(&self) -> &T {
&**self
}
}