始めに
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++のような低水準言語に近い言語だと改めて実感しました。
参考文献