Help us understand the problem. What is going on with this article?

[Rust] &strとStringを理解しようと思ったらsliceやmutを理解できてないことに気づいた話

More than 1 year has passed since last update.

Rust入門メモ。

Rustは文字列を表す型として &strString がある
これが、結構ややこしくて、どういうときにどっちを使うべきなのかがよくわからない

環境

OS: macOS High Sierra 10.13.3
Rust: rustc 1.26.0-nightly (c08480fce 2018-03-23)

Rustの文字列型

Rustの日本語Document: 文字列

Rustには主要な文字列型が二種類あります。&str と Stringです。 まず &str について説明しましょう。 &str は「文字列スライス」と呼ばれます。 文字列スライスは固定サイズで変更不可能です。文字列スライスはUTF-8のバイトシーケンスへの参照です。

Rustには &str だけでなく、 String というヒープアロケートされる文字列もあります。 この文字列は伸張可能であり、またUTF-8であることも保証されています。 String は一般的に文字列スライスを to_string メソッドで変換することで作成されます。

https://qiita.com/Mizunashi_Mana/items/db88cb0bff002abce1ae

↑このQiitaの解説が一番わかりやすい

Rustには文字列を表す型が二つあります。
一つはプリミティブ型のstr、もう一つが標準ライブラリが提供するStringです。といってもそれほど大した違いはありません。
str型はu8のスライス、String型はu8のベクタと同じです。ただ両方とも、列の内容としてUTF-8しか許可していないというその一点だけが異なります。なので、実質並びに制限のある8byteの配列スライスとベクタです。strの方はプリミティブ型ですが、Stringの方は標準ライブラリの提供型なので実装が単純に見て取れます。

&str -> slice
String -> Vector

ということだ。

そもそも、sliceってちゃんと理解してないな…
だから、まずはsliceに関して理解していこうと思う

sliceと配列とVector

https://qiita.com/Mizunashi_Mana/items/db88cb0bff002abce1ae#%E6%96%87%E5%AD%97%E5%88%97%E3%81%A8%E6%96%87%E5%AD%97%E5%88%97%E3%82%B9%E3%83%A9%E3%82%A4%E3%82%B9%E3%81%AE%E9%81%95%E3%81%84

ここにも書いてある

つまりsliceってのは、 配列とかVectorへの参照ということですかね。
sliceの型は &[T] なので、参照なのはわかる

ちなみに、文字列リテラルの型は &’static str

ちなみに、Goのsliceとarrayの解説を見つけた
https://qiita.com/seihmd/items/d9bc98a4f4f606ecaef7

これを見ていて、いろいろ気になったので
Rustでどうなるのか気になる事をやってみることにした

配列の参照(スライス)を別の変数に束縛して、ポインタを確認してみる

let a: [i32; 2] = [1,2];
let b = &a;

// これは同じ
println!("{:p}", &a);
println!("{:p}", b);
0x7ffee9a76dd8
0x7ffee9a76dd8

配列を別の変数にそのまま代入してみる

let a: [i32; 2] = [1,2];
let b = a;

// これは違う
println!("{:p}", &a);
println!("{:p}", &b);
0x7ffee9a76eb8
0x7ffee9a76ec0

これはコピーされる(?)ので、参照先が違うようだ

配列を書き換える

let mut a = [1,2,3];
let b = a; // ここでコピーされる?
a[0] = 0;

println!("{:?}", a); // #=> [0, 2, 3]
println!("{:?}", b); // #=> [1, 2, 3]

これもコピーされるっぽいので、bは安全

スライスを書き換える

let a = &mut [1,2,3];
let b = a;
a[0] = 0;

println!("{:?}", a); // #=> [0, 2, 3]
println!("{:?}", b); // #=> [1, 2, 3]

さて、これはコンパイルエラーになる

error[E0382]: use of moved value: `*a`
  --> main.rs:56:9
   |
55 |         let b = a;
   |             - value moved here
56 |         a[0] = 0;
   |         ^^^^^^^^ value used here after move
   |
   = note: move occurs because `a` has type `&mut [i32; 3]`, which does not implement the `Copy` trait

error[E0382]: use of moved value: `a`

a自体が既にsliceなので、所有権はbに代入した時点でmoveされる(?)
つまり、bに代入した時点でaはもういじれなくなるということですかね

これは、めんどくさいように見えて、実は超安全だなぁ、と思った。
だって、書き換えられる人は一人だけで、bをこの先いじいじしてる間に、aを誰か別のやつが書き換える、なんてことが出来ないのだから
書き換えるどころか、参照することさえできない。printlnしようとするとコンパイルエラー。
(これ、かっこいいなぁ。)

mutableにしたい時は

let mut a = 1;
a = 2;
println!("{:?}", a);

こんな風に let mut って定義する
(ちなみに let mut a = 1; はwarningが出ます)

しかしながら、mutableなsliceを定義する時は

let mut a = &[1];
a[0] = 0;

println!("{:?}", a);

これだとコンパイルエラー

error[E0594]: cannot assign to indexed content `a[..]` of immutable binding
  --> main.rs:70:9
   |
70 |         a[0] = 0;
   |         ^^^^^^^^ cannot mutably borrow field of immutable binding

これは、何故か。

答えはここに書いてある んだけど。

let mut x = 1;

これ(x)は、 ミュータブルな変数束縛

let mut x = 1;
let y = &mut x;

これ(y)は、 ミュータブル参照

ということらしい。

y はミュータブル参照へのイミュータブルな束縛であり、 y を他の束縛に変える(y = &mut z)ことはできません。 しかし、y に束縛されているものを変化させること(*y = 5)は可能です。微妙な区別です。

とのこと。

やってみよう。

let a = &mut [1];
a[0] = 0;

println!("{:?}", a);

aはimmutableなのに、aの要素を書き換えることが出来る。
これが、ミュータブル参照へのイミュータブルな束縛ということか。

let a = &mut [1];
a[0] = 0; // これはできる

a = &mut [2]; // これはできない

println!("{:?}", a);

aをmutableに束縛していないので、aという変数自体を書き換えることはできない。

んー、すっきりした!(気がする)

&strとString

&strどっかへの参照
StringVec<u8>

なので、

  • 参照だけでいい場合は、&strを使う
  • 文字列の中身を書き換えたい場合はStringを使うとよい

ってことなんだろうか。

&strで良い例

https://qiita.com/Mizunashi_Mana/items/db88cb0bff002abce1ae#%E9%96%A2%E6%95%B0%E3%81%AE%E5%BC%95%E6%95%B0%E3%81%AB%E3%81%8A%E3%81%84%E3%81%A6

fn puts(s: &str) {
    println!("{}", s);
}

受け取った文字列をただ標準出力に出力するだけ。
書き換えもしないので、ただただ参照を貰えばいい。

って書いてあると思うんですが

let a = "hoge".to_string();
println!("{:p}", &a);

puts(&a);

println!("{:p}", &a);

こんなことしてみると

0x7ffee61881d8
0x109e17018
0x7ffee61881d8

参照先が違う気がする。。。?
これは、何故だろう、、、

パフォーマンス上ではスライスの作成はコピーが発生しませんが、

って書いてあるけど、本当にコピー発生してないのだろうか

fn puts(s: &String) {
    println!("{:p}", s);
}

こうすると、もちろん同じになる

0x7ffee3e521d8
0x7ffee3e521d8
0x7ffee3e521d8

んー。これは分からなくなってきた…

Stringの方が良い例

https://qiita.com/Mizunashi_Mana/items/db88cb0bff002abce1ae#%E9%96%A2%E6%95%B0%E3%81%AE%E8%BF%94%E3%82%8A%E5%80%A4%E3%81%AB%E3%81%8A%E3%81%84%E3%81%A6

文字列を破壊的に書き換えたい場合

fn append_world(str: &mut String) {
  str.push_str(" World");
}

let mut string = "Hello".to_string();
append_world(&mut string);
println!("{}", string); // => Hello World

まぁ、これはたしかにその通りだな、と思った。
でも、そこまでシビアなパフォーマンスを要求されない限りは、
破壊的に変更したい事ってあまりなくて、大抵は新しい文字列を返したい気がする。

戻り値に文字列

これは、よくあると思う。

&str を返そうとするとコンパイルエラーになる

fn hoge() -> &str {
    "hoge"
}

これは、そもそも ”hoge”&’static str が型なので、型が合ってない

fn hoge() -> &str {
    let a = "hoge".to_string();
    &a
}

これは、型は合ってるけど、&a を返すということは、実体の a はこのscopeに生きているわけで
a の生存期間以上に長い生存期間の参照を返す事になるので、lifetime parameterが必要になるらしい。
(ライフタイムについては、また別途)

つまり、実体のStringをそのまま返しちゃうほうがいい、ということですかね。

まとまらないまとめ

&strの引数の時に String -> &str でコピーされるとしたら、あまり&strにうまみがない(?)
だとすると、全部Stringでいいのではなかろうかという気さえしてくる。

結局、どう使い分ければよいのか、はっきりと理解できずに終わってしまった。

yagince
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした