見出し画像

Rust: 何だかピンと来ないRustの文字周り事情…

C++は文字を扱う時にstd::stringを使うのが一般的です。もちろんC言語のようにcharの配列を操作してstr系の関数を通して云々…でも良いのですが、std::stringは楽ですから。で、そんなお気軽感覚でRustでの文字列を扱おうとしたら…あれ…あれあれ?何だかやけに扱いづらい?後発言語なのにどういう事だ?思った事が中々出来ない…。C++的な利便さを想像していた僕は大きく出鼻をくじかれたのでした(-_-;

Rustの文字周りがどうにもピンと来ない…。そこで、その辺りを調べて見る事にしました。

Rustには可変長な「String」と固定長な「&str」がある

 Rustには文字列を扱う型としてStringと&strの2種類があります。他に"文字"の型となるchar型もありますが、今回は"文字列"なのでこの2つに話を絞ります。

 String型はUTF-8な文字列をVec<u8>型として内部に格納する型です。Vecは動的配列(サイズの伸縮が可能な配列)でu8型は1バイトな数値を意味します。UTF-8の実体は単なる1バイト単位の数字の並びなのでそういう型になっているわけです。UTF-8なのでASCII文字列ならそのまま1バイト単位で文字を表現できますし、日本語なども問題無く格納出来ます。そしてString型はVec<u8>を保持しているため文字列の伸長が可能です。C++と違うのは「終端文字\0」を含まない事です。ここはちょっと注意ですね。

 一方&str型もUTF-8な文字列を表す型なのですが、こちらは配列では無くてスライスです。スライスというのは固定配列の一部分で自分を表現する型の事で、元となる配列(サイズ変更が出来ないコンパイル時にサイズが確定する文字配列)が必要になります。つまり&str型は「元となる固定配列を参照する事で存在出来る型」という事です。そのため&(参照)が付いているんですね。

文字列リテラルは&str型

いわゆる文字列リテラル("Hello World"のように""で囲んだ文字列)は固定配列です。その為型としては&str型になります:

let s1 : &str = "Hello, world!";  // OK: 明示的に&str
let s2        = "Hello, world!";  // OK: 暗黙的に&str型
let s3 : str  = "Hello, world!";  // NG: str型というのは無い!

上のように変数として表す時には&strと明示しても良いですし、暗黙的にも出来ます。ただし3行目にあるようにstr型には出来ません。あくまでも参照なんです。

&str型をスライスしたのも&str型

&str型をスライスした物も固定的な配列であるため&str型になります:

let s = "Hello, world!";

let s1 = &s[..5];   // OK: "Hello"
println!("{}", s1);

let s2 = &s[7..13]; // OK: "World!"
println!("{}", s2);

&str型のスライスには書き方があり、上のように元の&str型の変数名に[s..e]のように切り取る開始文字位置と終了文字位置を与えます。開始点が最初からの場合は[..e]と開始位置を省略できます。逆にある位置から最後までスライスするなら[s..]と終了位置を省略可能です。そして参照になるので&を付けて&str型の変数に渡します。

String型には文字列リテラルを直接渡せないのでto_string関数で

固定長配列である文字列リテラルや&str型はString型ではないので、以下のような直接代入は出来ません:

let s3 : String = "No...";  // NG

let s  : &str = "Hello, World";
let s4 : String = s;        // NG

&str型をString型に代入するにはstr::to_string関数で変換する必要があります:

let s : &str = "Hello, World";
let s3 : String = "No...".to_string();  // OK
let s4 : String = s.to_string();        // OK

String型の変数を文字列で初期化するのはちょっと面倒臭いという事です(^-^;

String型を&strにするには参照で

逆にString型を&str型に代入する時にはその参照を渡せます:

let s5 : String = "Hello, World!".to_string();
let s6 : &str = &s5;  // OK
println!("{}", s6);

上のコードは一旦String型にコンバートした"Hello, World!"文字列を2行目で&strに渡すため&s5と参照を渡しています。元のString型はmutableでも構いません。

文字列同士をくっつけただけなのに…

 文字列操作で頻出するのが文字列同士の連結です。Rustは基本的にimmutable(不変)をデフォルトとしているため、可変前提な文字列連結をすんなりさせてくれません。ここが結構分かりにくいし、人の感覚とちょっと異なる所がある為混乱します(-_-;

 少なくとも固定長である&str同士の直接連結は出来ない(concat!マクロでは可)ので、String型で扱う必要があります。String型は連結用に+演算子がサポートされています。ただし、右辺に来れるのは&strです:

let s7 = "Hello, ".to_string();
let s8 = s7 + "World!";  // OK
println!("{}", s8);

1行目でString型の変数s7を作り、2行目でそれに"World!"という&str型を足し算(連結)して、s8に格納しています。「え、何だ、別に問題無いじゃん」と思うかもしれませんが、このコードの後に、

println!("{}", s7);  // NG: ムーブ後なので無効

このようにs7の文字列を表示しようとするとコンパイルエラーになってしまうんです。見た目は何も悪い事していないのにです。

これは先の文字列連結により、s7の文字列がs8に移動(ムーブ)してしまったためなんです。いったんムーブしてしまった変数は扱ってはいけないため、コンパイラに怒られる事になります。C++ではこういう問題は起こりません。ここがRust特有の値の寿命管理の難しさで「文字列を取っておいて再利用」という事がこの方法ではうまくいかないのです。

format!マクロでの連結

 String型の+演算子による文字列連結はムーブの制約があるため、そうして構わない所以外は正直使えません(T_T)。連結前の文字列をムーブせずに、連結したStringを新規に作成するにはformat!マクロを使います:

let s9  = "Hello, ".to_string();
let s10 = format!( "{}{}", s9, "World!" );  // OK: 2つの文字列を組み合わせた新しいStringが返る
println!("{}", s10);
println!("{}", s9);   // OK: s9はムーブしていないので有効

s10の右辺がそれです。第1引数にフォーマット文字列、第2引数以降にフォーマットに沿った文字列などを並べると、新規のStringを返してくれます。第2引数以降に与えた変数はムーブが起こらないので、4行目にあるようにs9を再利用しても怒られません。フォーマットの書き方は改めて別にお話したい所ですが、上のように一つの値に対して"{}"とするとだいたい宜しくやってくれますw。

ムーブが起こらないという事はこれはコピーが起きている事に他なりません。つまりムーブよりずっと遅いという事。ムーブで良い所でformat!マクロを使うのは得策では無いのでご注意を。

Stringの伸長

String型は内部にVec<u8>を保持しているので文字列を伸ばす事が出来ます。先の文字列連結の代わりとしてそれを使うのもありです。

文字列を伸ばすにはString::push_str関数を使います:

let mut s11 = String::new();
s11.push_str("Hello,");
s11.push_str(" World");
s11.push_str(" from");
s11.push_str(" Japan!");
println!("{}", s11);

伸ばす=可変ですから、String型はmutableにする必要があります。後はpush_str関数に後端に連結したいstring型の参照を渡していくだけです。

push_str関数は内部で文字列を複製してヒープを拡張して連結していくので、引数で渡した変数にムーブは発生しません。なので、文字列を連結する方法として分かりが良いと思います。

という事で、Rustの文字列周りについてさわりですがざっとまとめてみました。細かい事はまだ色々ありますが、String型と&str型の違いを押さえておけば、大分見通しが良くなるんじゃないかなと思います。連結はC++やC#などと比べるとかなり面倒くさいですが、それらとは概念がそもそも違うimmutableベースな言語ですから、これは仕方ないと飲み込みましょうw。ではまた(^-^)/

この記事が気に入ったらサポートをしてみませんか?