始めに
Rustの文字列型にはStringと&strがあります。
正直違いがよく分かっておらずStringは可変長、&strは固定長くらいの
イメージしかなかったので、今回内部の仕組みを調べてまとめました。
ベクタやスライスの話が出てくるのでぜひ前回の記事も参考にどうぞ。
String
Stringは標準ライブラリが提供する型です。
実体はバイト列のベクタ(Vec<u8>)なので、追加や削除が可能です。
文字列はヒープに格納されます。
Stringのコピー
Stringをコピーするにはcloneを呼び出します。
するとヒープにある文字列も丸ごとコピーされます。
このように要素も含め全てコピーすることをディープコピーと言います。
let s1: String = String::from("ABC");
let s2: String = s1.clone();
&str
&strはプリミティブな型です。
実体はバイト列のスライス(&[u8])です。
&strとなるのはStringのスライスや文字列リテラル等です。
文字列リテラルとはソースコードにべた書きされている文字列のことです。
let s1: String = String::from("Hello World!!"); // これはString
let s2: &str = &s1[1..4]; // Stringのスライスは&str
let s3: &str = "ABC 123"; // 文字列リテラルは&str
let s4: &str = &s3[0..2]; // 文字列リテラルのスライスも&str
文字列リテラル
この文字列リテラルは少し変わった特徴を持っています。
文字列リテラルは静的領域というスタックでもヒープでもない領域に格納されます。
静的領域に格納されるデータには以下のような特徴があります。
- プログラムの実行開始から終了まで常に存在している
そのため、文字列リテラルの型は厳密には&'static strという表記になります。
'staticはその変数のライフタイム(生存期間)がプログラム実行中の全期間であることを示しています。
なぜ文字列リテラルは&strなのか
文字列リテラルとは静的領域というスタックから離れた場所にあるバイト列です。
そのバイト列を参照するためにスライスのポインタを用いています。
文字列リテラル自体を所有せず参照しているため、型はstrではなく&strとなります。
以下の様な場合のメモリ配置は下図の通りです。
let s: &str = "ABC";
&str → String
&strをStringに変換するにはString::fromやto_stringを使います。
この時、ヒープに文字列リテラルと同じ文字列がコピーされます。
文字列リテラルは常に静的領域に存在し続けます。
let s1: String = String::from("ABC");
let s2: String = "DEF".to_string();
String → &str
Stringを&strに変換するにはStringの参照やスライスを取得します。
この場合はヒープに文字列がコピーされることはありません。
変数s2もs3も同じヒープを参照しています。
let s1: String = String::from("ABCDE");
let s2: &str = &s1;
let s3: &str = &s1[2..4];
文字列の連結
文字列を連結させるにはString + &strの演算を行います。
左側の変数は文字列が追加されるため必ずStringでなければいけません。
右側の変数は&str(文字列リテラル、Stringの参照やスライス)となります。
let mut s1: String = String::from("ABC");
let s2: String = String::from("DEF");
s1 = s1 + "!?" + &s2;
おわりに
やはりRustは低レイヤーの知識がないと理解するのが難しいです。
C/C++のような低水準言語に近い言語だと改めて実感しました。
参考文献




