自転車本では紹介されてないけれど、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
}
}