APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

Rustの文字列連結

はじめに

こんにちは、コンテナソリューショングループの髙井です。
前回、Azureの各種PaaSにRustアプリケーションをデプロイする記事を書きました。

techblog.ap-com.co.jp

今日は、もう少しRust自体にも触れていきます。
はじめてRustを使うと戸惑いやすい文字列周りをさらっていきましょう。

Rustの文字列連結をPythonと比較する

文字列yes, fall in love!をつなげることを考えます。

プログラミング経験がある方なら「そんな簡単な」と思うかもしれませんが、スクリプト言語と同じノリで突っ込むとRustでは爆死します。

Pythonの場合

Pythonでは何も考えずに以下のような処理を書くことができます。

コード:

text = "yes"
text_extended = text + ", fall in love!"
print(text)
print(text_extended)

実行結果:

yes
yes, fall in love!

Rustの場合

Rustでは、以下のコードはコンパイルに通りません。

コード:

fn main() {
    let text = "yes";
    let text_extended = text + ", fall in love!";
    println!("{}", text);
    println!("{}", text_extended);
}

エラー出力:

error[E0369]: cannot add `&str` to `&str`
 --> src/main.rs:3:30
  |
3 |     let text_extended = text + ", fall in love!";
  |                         ---- ^ ----------------- &str
  |                         |    |
  |                         |    `+` cannot be used to concatenate two `&str` strings
  |                         &str
  |
  = note: string concatenation requires an owned `String` on the left
help: create an owned `String` from a string reference
  |
3 |     let text_extended = text.to_owned() + ", fall in love!";
  |                             +++++++++++

For more information about this error, try `rustc --explain E0369`.

&str型を+演算子で連結することはできないと怒られます。

文字列連結はString+&str

先ほどのエラー文をよく読むと、

note: string concatenation requires an owned `String` on the left

と書いてありますので、Rustでは文字列連結はString&strで行うことがわかります。

今はまだこの2つの型の違いは分からないと思いますが、とりあえず言われたとおりに直しましょう。

元の状態は両方とも&strだったのでtextのほうだけString型にすればよいです。
String型は公式ドキュメントを見るとString::from()で作るのが分かります。

doc.rust-lang.org

コード:

fn main() {
    let text = String::from("yes");
    let text_extended = text + ", fall in love!";
    println!("{}", text);
    println!("{}", text_extended);
}

エラー文:

error[E0382]: borrow of moved value: `text`
   --> src/main.rs:4:20
    |
2   |     let text = String::from("yes");
    |         ---- move occurs because `text` has type `String`, which does not implement the `Copy` trait
3   |     let text_extended = text + ", fall in love!";
    |                         ------------------------ `text` moved due to usage in operator
4   |     println!("{}", text);
    |                    ^^^^ value borrowed here after move
    |
note: calling this operator moves the left-hand side
   --> /home/h_takai/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/arith.rs:114:12
    |
114 |     fn add(self, rhs: Rhs) -> Self::Output;
    |            ^^^^
    = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.

はい、またエラーが出ました。現実はそんなに甘くないですね。

連結の演算子は所有権が移動する

あらためてエラー文を読むと、以下のように書いてあります。

move occurs because `text` has type `String`, which does not implement the `Copy` trait

どうやらコンパイラは「変数textString型だからmove(所有権の移動)が起こっているよ、なぜならString型はCopyトレイトを実装してないから」と言っているようです。

これは、text + ", fall in love!"という表記が、内部的にはadd(text, ", fall in love!")という関数呼び出しに対応することに起因するものです。

所有権とは

Rustでは変数を関数に渡すと所有権が移動します。 所有権は、メモリ安全性を担保するために設定されている概念です。

本記事では詳細は省きますが、補足の記事を別に書きましたので所有権やCopyトレイトについて簡単に把握したい方はぜひご覧ください。

techblog.ap-com.co.jp

あらためてエラー文を読む

ここで、ようやく最初のエラーメッセージである、

move occurs because `text` has type `String`, which does not implement the `Copy` trait

を理解することができます。

String型はベクタの一種であり、不定長でヒープ領域に格納されるデータです。そのため、Copyトレイトは実装されておらず、変数の束縛の際には所有権が移転します(上記の記事で簡単に説明しています)。

先ほど、text + ", fall in love!"という表記が、内部的にはadd(text, ", fall in love!")という関数呼び出しに対応すると書きましたが、関数の引数も変数なので束縛です。つまり所有権が移転します。

したがって、所有権の移転後であるprintln!("{}", text)の時点では参照できなくなっています。もし参照したいなら、明示的にコピーしてあげる必要があります。

所有権を意識して明示的にコピーをおこなう

では、text_extendedを作成する際にtextを直接利用するのではなく、コピーを明示的に作成しましょう。

fn main() {
    let text = String::from("yes");
    let text_extended = text.clone() + ", fall in love!";
    println!("{}", text);
    println!("{}", text_extended);
}

このように書けば、意図したとおり以下の結果を得ます。

yes
yes, fall in love!

めでたしめでたし。

と言いたいところですが、まだやることが残っています。String&strの違いについてです。

スライスとは

ここまで説明を後回しにしてきましたが、&strは文字列スライスと呼ばれています。

スライスといえばPythonでもa[1:5]のように作ったりしますが、これとはちょっと違います。Pythonでスライスを作成するとshallow copyがされて可変のオブジェクトが返りますね。

a = [1, 2, 3]
b = a[:2]
b[0] = 99
print(a) #=> [1, 2, 3]
print(b) #=> [99, 2]

一方で、Rustのスライスは「参照」です。参照なので&が付いていますね。

fn main() {
    let digits = String::from("0123456");
    let slice = &digits[..4];
    println!("{}", digits); // 0123456
    println!("{}", slice); // 0123
}

Pythonと違いコピーではなく参照なので、以下のようなコードはコンパイルエラーです。

fn main() {
    let mut digits = String::from("0123456");
    let slice = &digits[..4]; // ここで参照を取っているので
    println!("{}", digits);
    digits.clear(); // 参照元の消去はコンパイルエラーになる
    println!("{}", slice);
}

ということで、スライスは元のデータ(の一部)を参照する型だということがわかりました。

ちなみに今回のように文字列リテラルが参照なのはなぜかというと、文字列リテラルはコンパイル後のバイナリに直に埋め込まれたデータを参照するものだからです。

詳しく知りたい方は

公式ドキュメントに書いてあります。

doc.rust-jp.rs

おわりに

Rustの文字列連結はString + &strで、スライスは参照!
この2点を押さえれば大丈夫です。Rustをどんどん使っていきましょう。

本記事の投稿者: 髙井 比文
AKSをメインにしたインフラとアプリの領域際をご支援することが多いです。Azureは11冠です。
Hifumi Takai - Credly