【Rust入門】String 型と文字列スライス &str 型の基本について分かりやすく解説

Rust で文字列を扱うための String
型と文字列スライス &str
型の基本を初心者にも分かりやすく解説します。
Rust の文字列
どのプログラミング言語においても文字列を適切に扱えるようになることは非常に重要です。Rust では、文字列を扱う際に主に登場するのが「String
型」と「文字列スライス &str
型」の2つです。
String
型は Rust の標準ライブラリとして提供される可変長の文字列型です。String
型は、文字列リテラル(例:"Hello"
)とは異なり、ヒープ領域に格納されて動的にメモリが確保されます。
文字列スライス(&str
) は、UTF-8 エンコードされた文字列データへの参照(借用)です。データ本体は別の場所(静的領域やヒープ領域)に存在し、&str
はその一部または全体を参照する仕組みです。
この記事では、Rust の String
型と文字列スライス &str
型について分かりやすく解説します。
メモリ領域の中には「テキスト領域」「静的領域」「スタック領域」「ヒープ領域」があります。以降の説明で必要になりますので簡単に紹介します。
- テキスト領域:プログラムの実行コードが格納されます。
- 静的領域:
static
変数など、プログラム全体を通じて使う値を格納します。 - スタック領域:基本型(
i32
など)のサイズが決まった小さな値が置かれます。 - ヒープ領域:
String
のようにサイズが実行時に決まるデータが使用する領域です。
String
型
String
型とは
Rust における String
型は、可変長でありかつ所有権を持つ文字列型です。String
は、ヒープ領域に格納され、実行時にサイズを変更したり、編集したりすることが可能です。ユーザーからの入力やファイルから読み込むデータ等、動的に得られる文字列を保持したいときには String
を使用します。
String
型の使い方
String::from() を使用する
String
を生成する一般的な方法は、標準ライブラリの String
型の関連関数 String::from()
を用いて生成する方法です。ヒープ上に新たに String
の文字列が確保されます。
fn main() { // 関連関数 from を使って生成 let s = String::from("HelloWorld"); println!("{}", s); }
【実行結果】 HelloWorld
to_string() メソッドを使用する
文字列スライスに対して to_string()
メソッドを呼び出すことで、String
に変換することも可能です。
fn main() { // to_string メソッドを使って生成 let s = "HelloWorld".to_string(); println!("{}", s); }
【実行結果】 HelloWorld
format
マクロを使用する
format
マクロを使うことで String
文字列を生成できます。
fn main() { // format マクロを使って生成 let part = "World"; let s = format!("Hello{}", part); println!("{}", s); }
【実行結果】 HelloWorld
print
マクロが、コンソールに文字列を出力するのに対して、format
マクロは String
オブジェクトを返します。変数の値を埋め込んで文字列を生成したい場合に便利です。
関連関数 String::new()
を使用する
関連関数である String::new
関数を使うことでも生成ができます。
fn main() { // 関連関数 new を使って生成 let mut s = String::new(); // 文字列を追加 s.push_str("HelloWorld"); println!("{}", s); }
【実行結果】 HelloWorld
変数宣言時点では、文字列が決まっておらず後で決まるような場合には、あらかじめ new
で用意しておいて、 push_str
メソッドなどで文字列を追加して使用します。String
型の各種メソッドについては後ほど詳しく説明します。
String::from()
と to_string()
の違い
String::from()
と to_string()
メソッドはどちらも同じ動作をするため、どちらの方法を利用しても構いません。内部的には、String::from()
は、String
型の関連関数として定義されており、一方で to_string()
メソッドは、ToString
トレイトの実装に依存しています。
例えば、上記例の "HelloWorld"
は、文字列スライス &'static str
型です。文字列スライスについては後ほど説明しますが、&str
型は ToString
トレイトを実装しているため String
型に変換できます。
どちらを利用するかは好みやスタイルの違いで選んでも基本的には問題ありませんが、to_string()
は ToString
トレイトの実装に依存しており場合によっては期待しない型変換が行われない可能性もあります。明示的な意図を示す場合や型安全性を重視する場面では String::from()
を使用するとより安全です。
String 変数の変更(文字列の追加・結合・削除)
String
型の変数はヒープ領域に格納されているので文字列の追加や削除といったことが可能です。文字列の変更では、以下のようなメソッドを使用できます。
区分 | メソッド・演算子 | 概要 |
---|---|---|
追加 | push(char) | 1文字(char 型)を末尾に追加 |
追加 | push_str(&str) | 文字列スライス(&str 型)を末尾に追加 |
追加 | insert(index, char) | 指定バイト位置に1文字を追加 |
追加 | insert_str(index, &str) | 指定バイト位置に文字列スライス(&str 型)を追加 |
結合 | + 演算子 | 複数の文字列を結合(右辺は参照である必要があり、左辺はムーブされるので注意) |
結合 | format マクロ | 複数の文字列を結合 (元の変数の所有権はムーブせずに結合可能) |
削除 | pop() | 末尾の1文字を削除 ( Option<char> 型で返却) |
削除 | remove(index) | 指定バイト位置の1文字を削除 |
削除 | truncate(index) | 指定バイト位置以降を削除 |
削除 | clear() | 文字列を全て削除 |
文字列の追加
文字列の追加では、push
、push_str
、insert
、insert_str
メソッドを使用できます。
push
:末尾に 1 文字追加します。push_str
:末尾に文字列を追加します。insert
:指定バイト位置に 1 文字追加します。insert_str
:指定バイト位置に文字列を追加します。
fn main() { // push(char) で1文字末尾に追加 let mut s1 = String::from("HelloWorld"); s1.push('!'); println!("{}", s1); // push_str(&str) で文字列スライスを末尾に追加 let mut s2 = String::from("Hello"); s2.push_str("World!"); println!("{}", s2); // insert(index, char) で指定バイト位置に1文字を追加 let mut s3 = String::from("HeloWorld!"); s3.insert(3, 'l'); println!("{}", s3); // insert_str(index, &str) で指定バイト位置に文字列スライスを追加 let mut s4 = String::from("Helrld!"); s4.insert_str(3, "loW"); println!("{}", s3); }
【実行結果】 HelloWorld! HelloWorld! HelloWorld! HelloWorld!
文字列の結合
文字列の結合では、+
演算子か format
マクロを使用できます。
fn main() { // +演算子で文字列を結合する let s1 = String::from("Hello"); let s2 = String::from("World"); // s1 の所有権はムーブするので注意、s2 は参照を渡す必要がある let s3 = s1 + &s2; println!("{}", s3); // s1 の所有権は移動するので以下はコンパイルエラー // println!("{}", s1); // format マクロで結合する(ムーブなしで結合可能) let s1 = String::from("Hello"); let s2 = String::from("World"); // format で結合 let s3 = format!("{}{}", s1, s2); println!("{}", s3); // ムーブしないので元の変数も使用できる。 println!("{}", s1); println!("{}", s2); }
【実行結果】 HelloWorld HelloWorld Hello World
+
演算子の場合、左辺の変数の所有権はムーブするため、以降では使えなくなるので注意が必要です。また、右辺の変数は参照を渡す必要があります。
一方、format
マクロで結合する場合には、元の変数の所有権を保持したまま String
を生成するため、結合後も元の変数を使用することができます。また、format
マクロは内部的には参照として処理されるため、変数はそのまま渡すことができます。
文字列の削除
文字列の削除では、pop
、remove
、truncate
、clear
メソッドを使用できます。
pop
:末尾から1文字を削除し、Option<char>
で削除した文字列を返します。remove
:指定したバイト位置の 1 文字を削除し、char
型を返します。truncate
:指定したバイト位置以降を削除します。返却値はありません。clear
:文字列の全てを削除します。
各方法で、返却値の有無や型が異なっているので注意してください。
fn main() { let mut s = String::from("HelloWorld"); // pop() で末尾の文字を取得 match s.pop() { Some(c) => println!("pop: {}", c), None => println!("文字列が空なので取り出せません"), } println!("s: {}", s); // remove(index) で指定バイト位置の1文字を削除 let c = s.remove(1); println!("remove idx:{}, removed:{}", 1, c); println!("s: {}", s); // truncate で指定バイト位置以降を削除 s.truncate(4); println!("s: {}", s); // 文字列を全て削除する s.clear(); println!("s: {}", s); }
【実行結果】 pop: d s: HelloWorl remove idx:1, removed:e s: HlloWorl s: Hllo s:
返却値の有無や型が異なっているので取り出し方がそれぞれ異なっています。なお、remove
や truncate
は、Option
型のような安全な型を返すメソッドではないため、範囲外インデックスを指定すると実行時に panic
を引き起こし、プログラムが異常終了します。
insert
、remove
、truncate
はバイト位置であることに注意
insert
、remove
、truncate
といったメソッドは、位置の指定がバイト位置となっています。そのため、マルチバイト文字を含む場合で、マルチバイト文字の途中のバイト位置を指定すると panic
で実行時にエラーとなるので注意が必要です。
マルチバイト文字を含む場合は、以下のように char_indices()
メソッドを使ってバイト位置を検索してから安全に処理することが推奨されます。
fn main() { // バイト位置を指定する場合は、マルチバイト文字の時に注意 // ===== insert の場合 let mut s1 = String::from("Rust、こんにちは。"); // 以下は panic となる // s1.insert(5, '!'); // 文字数でバイト位置を検索してからであれば安全 if let Some((idx, _)) = s1.char_indices().nth(10) { s1.insert(idx, '!'); } println!("{}", s1); // ===== remove の場合 let mut s2 = String::from("Rust、こんにちは。"); // 以下は panic となる // s1.remove(5); // 文字数でバイト位置を検索してからであれば安全 if let Some((idx, _)) = s2.char_indices().nth(4) { s2.remove(idx); } println!("{}", s2); // ===== truncate の場合 let mut s3 = String::from("Rust、こんにちは。"); // 以下は panic となる // s3.truncate(5); // 文字数でバイト位置を検索してからであれば安全 if let Some((idx, _)) = s3.char_indices().nth(4) { s3.truncate(idx); } println!("{}", s3); }
【実行結果】 Rust、こんにちは!。 Rustこんにちは。 Rust
上記例でコメントアウトしたの部分を外すと panic
で実行時にエラーとなります。例の文字列で 5 バイト目は「、
」の途中のバイトを指すためです。
char_indices().nth(x)
で x
文字目のバイト位置を調べることができます。このメソッドは、Option
型で位置と文字のタプルを取得でき、match
や if let
を使えば安全に処理できます。
String
型の内部構造
String
型の変数は、ヒープ領域に確保されることが特徴ですが、後ほど説明する文字列スライスとの違いを明確に理解するために、内部構造を理解しましょう。String
型の変数は、スタック上に以下の内容を含む情報を格納しています。
- ヒープアドレス上の先頭アドレス(8バイト)
- ヒープ上で使用中のバイト数(8バイト)
- ヒープ上で確保済のバイト数(8バイト)
このように String
型は 8 バイト × 3 の 24 バイト で構成される変数としてスタックのメモリ領域を使用します。概要イメージとしては以下のようになっており、実際の文字列はヒープ領域に格納されます。

具体的に以下のコードで内部情報を確認してみます。
use std::mem; fn main() { let s1 = String::from("HelloWorld"); let s2 = String::from("Rust こんにちは!"); println!("==========================================="); println!("s1 の文字列の内容: {}", s1); println!("s1 のスタック上のアドレス: {:p}", &s1); println!("ポインタ: {:p}", s1.as_ptr()); println!("長さ: {}", s1.len()); println!("容量: {}", s1.capacity()); println!("String のサイズ(byte): {}", mem::size_of_val(&s1)); println!("==========================================="); println!("s2 の文字列の内容: {}", s2); println!("s2 のスタック上のアドレス: {:p}", &s2); println!("ポインタ: {:p}", s2.as_ptr()); println!("長さ: {}", s2.len()); println!("容量: {}", s2.capacity()); println!("String のサイズ(byte): {}", mem::size_of_val(&s2)); println!("==========================================="); }
【実行結果】 =========================================== s1 の文字列の内容: HelloWorld s1 のスタック上のアドレス: 0xddb70ff510 ポインタ: 0x2655f95eff0 長さ: 10 容量: 10 String のサイズ(byte): 24 =========================================== s2 の文字列の内容: Rust こんにちは! s2 のスタック上のアドレス: 0xddb70ff528 ポインタ: 0x2655f95f2f0 長さ: 23 容量: 23 String のサイズ(byte): 24 ===========================================
上記例では、s1
と s2
という String
型の変数を用意し、内部の情報を調べています。
まず、変数のスタック上のアドレス(&s1
と &s2
)は、println!
で {:p}
とすることで確認できます。また、std::mem::size_of_val(&s1)
により具体的なメモリ上の大きさを調べられます。 これにより、String
型の変数がスタック上で 24 バイトのメモリを確保していることを確認できます。
String
型を構成する、ヒープ領域の先頭アドレス値は as_ptr()
メソッド、長さは len()
メソッド、容量は capacity()
メソッドで確認することができます。上記例では、長さと容量が一致していますが、コンパイラが長さに対して余裕をもって確保することが多いです。
文字列スライス(&str
型)
文字列スライス(&str
型)とは
Rust において、文字列スライス(&str
)は、UTF-8 でエンコードされた文字列の一部または全体を参照するための型です。これは、借用(参照)型であり、データの所有権を持ちません。データの本体は静的領域やヒープ領域に存在し、&str はそこへのポインタと長さを持つだけの軽量な構造を持っています
プログラム上の文字列リテラル(例:"HelloWorld"
)は、プログラムの静的領域に格納されており、型は &'static str
型です。ここで、'static
は静的なライフタイムを表しており、プログラムの開始から終了までのライフタイムを持つ型であることを示しています。
また、String
型の変数は、&str
型に暗黙的に変換することができるため、関数等で &str
型を引数として受け取る設計にすることで、文字列リテラルでも String
型でも扱う関数とすることができます。
文字列スライス(&str
型)の使い方
&str
型は、以下のような状況でよく使用されます。
- 文字列リテラルとして使用する
- 文字列リテラルや
String
からの部分文字列として使用する - 関数の引数として、汎用的に文字列を受け入れる
文字列リテラルとして使用
最も単純な使い方は、文字列リテラルとして使用する方法です。
fn main() { let s = "HelloWorld"; println!("s: {}", s); }
【実行結果】 s: HelloWorld
文字列リテラルや String
の部分文字列として使用する
文字列スライスは、スライスという名の通り、文字列リテラルや String
の部分文字列への参照を取得することができます。文字列リテラルや String
の s
があった時に &s[開始..終了]
のように部分文字列の範囲を指定します。
開始と終了はバイト位置で、終了位置の文字は含まないことに注意してください。また、開始を省略して [..終了]
すると開始から終了位置までの文字列を、終了を省略して [開始..]
とすると開始位置から終了までの文字列を取得できます。なお、[..]
は文字列全体を表します。
fn main() { let s1 = "HelloWorld"; let s2 = String::from("HelloWorld"); let s1_part = &s1[2..5]; let s2_part = &s2[5..]; println!("s1_part: {}", s1_part); println!("s2_part: {}", s2_part); }
【実行結果】 s1_part: llo s2_part: World
文字列スライスもバイト位置を指すことから、 String
の注意事項と同様に、マルチバイト文字の途中のバイト位置を指定すると panic
が発生します。事前に char_indices()
を使用してバイト位置を確認してから処理すると安全です。
関数の引数として、汎用的に文字列を受け入れる
関数の引数として文字列を受け取る場合にも、&str
型をよく使用します。String
型の参照は、自動で &str
へ変換されるため、以下の例では String
の参照である &s2
についても show_str
関数は受け取ることができます。
fn show_str(s: &str) { println!("{}", s); } fn main() { let s1 = "HelloWorld"; let s2 = String::from("HelloWorld"); show_str(&s1); show_str(&s2); }
【実行結果】 HelloWorld HelloWorld
文字列スライス &str
型の内部構造
&str
型の変数は、静的領域に格納される文字列リテラル、またはヒープ領域に格納される String
を指しています。内部構造としては、&str
型の変数は、スタック上に以下の内容を含む情報を格納しています。
- 文字列が格納されている先頭アドレス(8バイト)
- スライスのバイト数(8バイト)
このように &str
型は 8 バイト × 2 の 16 バイト で構成される変数としてスタックのメモリ領域を使用します。概要イメージとしては以下のようになっており、文字列の実体は、文字列リテラルの場合は静的領域に、String
の場合はヒープ領域に格納されます。

具体的に以下のコードで内部情報を確認してみます。
use std::mem; fn main() { let s1 = "HelloWorld"; let s2 = String::from("Rust こんにちは!"); let s2_slice = &s2[..]; println!("==========================================="); println!("s1 の文字列の内容: {}", s1); println!("s1 のスタック上のアドレス: {:p}", &s1); println!("ポインタ: {:p}", s1.as_ptr()); println!("長さ: {}", s1.len()); println!("&str のサイズ(byte): {}", mem::size_of_val(&s1)); println!("==========================================="); println!("s2_slice の文字列の内容: {}", s2_slice); println!("s2_slice のスタック上のアドレス: {:p}", &s2_slice); println!("ポインタ: {:p}", s2_slice.as_ptr()); println!("長さ: {}", s2_slice.len()); println!("&str のサイズ(byte): {}", mem::size_of_val(&s2_slice)); println!("==========================================="); }
【実行結果】 =========================================== s1 の文字列の内容: HelloWorld s1 のスタック上のアドレス: 0x80d46ff2c8 ポインタ: 0x7ff61530b4f0 長さ: 10 &str のサイズ(byte): 16 =========================================== s2_slice の文字列の内容: Rust こんにちは! s2_slice のスタック上のアドレス: 0x80d46ff2f0 ポインタ: 0x26c1d91f010 長さ: 23 &str のサイズ(byte): 16 ===========================================
上記例では、文字列リテラルである s1
と String
s2
の参照としての s2_slice
の &str 型
で内部情報を取得して表示しています。
上記例の s1.as_ptr()
は静的領域を、s2_slice.as_ptr()
はヒープ領域を指すアドレスとなっています。また、std::mem::size_of_val()
により取得できるバイト数が 16 であることから、&str
型のスタック上でのメモリサイズが 16 バイトであることが確認できます。
まとめ
Rust で文字列を扱うための String
と &str
の基本的な使い方と内部的な構造の違いについても詳しく説明しました。
動的な操作には String
を、参照だけで十分な場合は &str
を使うなど、用途に応じた選択ができるようになることが重要です。また、各種メソッドなどについても紹介しましたが、マルチバイト文字を含む場合のバイト位置指定の扱いには十分に注意し、char_indices()
を活用した安全な操作を心がけましょう。
文字列操作は、どのようなプログラミング言語においても非常に重要な要素です。String
と &str
について、基本をしっかり押さえてプログラミングできるようになりましょう。