Rust入門

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

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

Rust で文字列を扱うための String 型と文字列スライス &strの基本を初心者にも分かりやすく解説します。

Rust の文字列

どのプログラミング言語においても文字列を適切に扱えるようになることは非常に重要です。Rust では、文字列を扱う際に主に登場するのが「String」と「文字列スライス &str」の2つです。

String 型は Rust の標準ライブラリとして提供される可変長の文字列型です。String 型は、文字列リテラル(例:"Hello")とは異なり、ヒープ領域に格納されて動的にメモリが確保されます。

文字列スライス(&str) は、UTF-8 エンコードされた文字列データへの参照(借用)です。データ本体は別の場所(静的領域やヒープ領域)に存在し、&str はその一部または全体を参照する仕組みです。

この記事では、Rust の String 型と文字列スライス &str 型について分かりやすく解説します。

所有権や借用については以下の記事も参考にしてください。

【Rust入門】所有権と借用の基本を分かりやすく解説

メモリ領域

メモリ領域の中には「テキスト領域」「静的領域」「スタック領域」「ヒープ領域」があります。以降の説明で必要になりますので簡単に紹介します。

  • テキスト領域:プログラムの実行コードが格納されます。
  • 静的領域: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()文字列を全て削除

文字列の追加

文字列の追加では、pushpush_strinsertinsert_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 マクロは内部的には参照として処理されるため、変数はそのまま渡すことができます。

文字列の削除

文字列の削除では、popremovetruncateclear メソッドを使用できます。

  • 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:

返却値の有無や型が異なっているので取り出し方がそれぞれ異なっています。なお、removetruncate は、Option 型のような安全な型を返すメソッドではないため、範囲外インデックスを指定すると実行時に panic を引き起こし、プログラムが異常終了します。

insertremovetruncate はバイト位置であることに注意

insertremovetruncate といったメソッドは、位置の指定がバイト位置となっています。そのため、マルチバイト文字を含む場合で、マルチバイト文字の途中のバイト位置を指定すると 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型で位置と文字のタプルを取得でき、matchif let を使えば安全に処理できます。

String 型の内部構造

String 型の変数は、ヒープ領域に確保されることが特徴ですが、後ほど説明する文字列スライスとの違いを明確に理解するために、内部構造を理解しましょう。String 型の変数は、スタック上に以下の内容を含む情報を格納しています。

  • ヒープアドレス上の先頭アドレス(8バイト)
  • ヒープ上で使用中のバイト数(8バイト)
  • ヒープ上で確保済のバイト数(8バイト)

このように String 型は 8 バイト × 3 の 24 バイト で構成される変数としてスタックのメモリ領域を使用します。概要イメージとしては以下のようになっており、実際の文字列はヒープ領域に格納されます。

String 型の内部構造

具体的に以下のコードで内部情報を確認してみます。

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
===========================================

上記例では、s1s2 という 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 の部分文字列への参照を取得することができます。文字列リテラルや Strings があった時に &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 の場合はヒープ領域に格納されます。

&str 型の内部構造

具体的に以下のコードで内部情報を確認してみます。

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
===========================================

上記例では、文字列リテラルである s1String 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 について、基本をしっかり押さえてプログラミングできるようになりましょう。

ABOUT ME
ホッシー
ホッシー
システムエンジニア
はじめまして。当サイトをご覧いただきありがとうございます。
私は製造業のメーカーで、DX推進や業務システムの設計・開発・導入を担当しているシステムエンジニアです。これまでに転職も経験しており、以前は大手電機メーカーでシステム開発に携わっていました。

これまでの業務を通じてさまざまなプログラミング言語や技術に触れてきましたが、その中でもRustの設計思想に惹かれ、この言語についてもっと深く学びたい、そしてその魅力を発信していきたいと思い、このサイトを立ち上げました。

自身の学びを整理しつつ、同じようにRustに興味を持つ方のお役に立てるような情報を発信していければと思っています。どうぞよろしくお願いいたします。

※キャラクターデザイン:ゼイルン様
記事URLをコピーしました