23
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

Rustを始めて最初に混乱するのが、文字列型の多さ

  • String
  • &str
  • &String
  • Box<str>
  • Cow<str>
  • &'static str

なんでこんなに種類があるんだ...

この記事で、それぞれの違いと使い分けを完全に理解しましょう。

目次

  1. 基本の2つ:String と &str
  2. メモリ上の違い
  3. 変換
  4. その他の文字列型
  5. 関数のシグネチャ
  6. まとめ

基本の2つ:String と &str

String

所有権を持つ文字列。ヒープに確保される。

let s: String = String::from("hello");
let s: String = "hello".to_string();
let s: String = "hello".to_owned();
  • 変更可能(可変)
  • ヒープに確保
  • サイズが動的に変わる
  • 所有権を持つ

&str

参照としての文字列。文字列スライス。

let s: &str = "hello";  // 文字列リテラル
let string = String::from("hello");
let s: &str = &string;  // String への参照
let s: &str = &string[0..3];  // 部分文字列
  • 不変
  • 所有権を持たない
  • 既存の文字列の一部を指す

簡単な覚え方

所有権 可変性 例え
String あり 可変 自分の家
&str なし 不変 借りている部屋

メモリ上の違い

String の構造

String
┌────────┬────────┬────────┐
│  ptr   │  len   │  cap   │
│(8byte) │(8byte) │(8byte) │
└───┬────┴────────┴────────┘
    │
    ▼
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ h │ e │ l │ l │ o │   │   │   │ (heap)
└───┴───┴───┴───┴───┴───┴───┴───┘
  • ptr: ヒープ上のデータへのポインタ
  • len: 現在の長さ
  • cap: 確保済みの容量

&str の構造

&str
┌────────┬────────┐
│  ptr   │  len   │
│(8byte) │(8byte) │
└───┬────┴────────┘
    │
    ▼
┌───┬───┬───┬───┬───┐
│ h │ e │ l │ l │ o │ (どこかのメモリ)
└───┴───┴───┴───┴───┘
  • ptr: 文字列データへのポインタ
  • len: 長さ

capacity がないので、サイズを変更できない。

サイズの確認

use std::mem::size_of;

fn main() {
    println!("String: {} bytes", size_of::<String>());      // 24 bytes
    println!("&str:   {} bytes", size_of::<&str>());        // 16 bytes
    println!("&String: {} bytes", size_of::<&String>());    // 8 bytes(ポインタ)
}

変換

&str → String

let s: &str = "hello";

// 方法1: to_string()
let string: String = s.to_string();

// 方法2: String::from()
let string: String = String::from(s);

// 方法3: to_owned()
let string: String = s.to_owned();

// 方法4: into()
let string: String = s.into();

どれを使うべき?
to_string()String::from() がおすすめ(わかりやすい)

String → &str

let string: String = String::from("hello");

// 方法1: 参照を取る
let s: &str = &string;

// 方法2: as_str()
let s: &str = string.as_str();

// 方法3: スライス
let s: &str = &string[..];

暗黙の変換(Deref coercion)も使える

fn print_str(s: &str) {
    println!("{}", s);
}

let string = String::from("hello");
print_str(&string);  // &String → &str に自動変換

その他の文字列型

&'static str

プログラム全体で有効な文字列参照。文字列リテラルはこれ。

let s: &'static str = "hello";  // バイナリに埋め込まれる

Box

ヒープ上の文字列を所有するが、サイズは固定。

let s: Box<str> = "hello".into();
let s: Box<str> = String::from("hello").into_boxed_str();

String との違い

サイズ 容量変更 メモリ効率
String 24 bytes できる 普通
Box<str> 16 bytes できない 良い

Box<str> は変更しない文字列を大量に持つ場合に有効。

Cow<'a, str>

Clone on Write。必要なときだけコピーする賢い型。

use std::borrow::Cow;

fn process(s: Cow<str>) -> Cow<str> {
    if s.contains("bad") {
        // 変更が必要なときだけ所有権を持つ
        Cow::Owned(s.replace("bad", "good"))
    } else {
        // 変更不要なら参照のまま
        s
    }
}

fn main() {
    let borrowed: Cow<str> = Cow::Borrowed("hello");
    let owned: Cow<str> = Cow::Owned(String::from("hello"));
    
    // 変更がなければコピーされない
    let result = process(Cow::Borrowed("hello world"));
    
    // 変更があればコピーされる
    let result = process(Cow::Borrowed("bad world"));
}

使いどころ

  • 条件によって変更するかしないか決まる場合
  • パフォーマンスが重要な場面

&String

正直、あまり使わない。

let string = String::from("hello");
let ref_string: &String = &string;  // String への参照

// でも、&str で十分なことがほとんど
fn print(s: &str) {}  // こっちのほうが汎用的
fn print(s: &String) {}  // 制限的

&String より &str を使おう(後述)

関数のシグネチャ

引数:&str を受け取る

// Good: &str を受け取る
fn greet(name: &str) {
    println!("Hello, {}!", name);
}

// Bad: &String を受け取る
fn greet_bad(name: &String) {
    println!("Hello, {}!", name);
}

fn main() {
    let string = String::from("Alice");
    let literal = "Bob";
    
    // &str を受け取る関数は両方OK
    greet(&string);  // String → &str に自動変換
    greet(literal);  // &str そのまま
    
    // &String を受け取る関数はStringのみ
    greet_bad(&string);
    // greet_bad(literal);  // エラー!
}

引数には &str を使おう!

戻り値

新しい文字列を返す:String

fn create_greeting(name: &str) -> String {
    format!("Hello, {}!", name)
}

入力の一部を返す:&str(ライフタイムが必要)

fn first_word(s: &str) -> &str {
    s.split_whitespace().next().unwrap_or("")
}

場合による:Cow

use std::borrow::Cow;

fn maybe_modify(s: &str) -> Cow<str> {
    if s.is_empty() {
        Cow::Owned(String::from("default"))
    } else {
        Cow::Borrowed(s)
    }
}

構造体のフィールド

所有したい:String

struct User {
    name: String,  // User が name を所有
}

impl User {
    fn new(name: &str) -> User {
        User { name: name.to_string() }
    }
}

参照を持ちたい:&str(ライフタイムが必要)

struct User<'a> {
    name: &'a str,  // どこか別の場所の文字列を参照
}

impl<'a> User<'a> {
    fn new(name: &'a str) -> User<'a> {
        User { name }
    }
}

迷ったら String を使おう(ライフタイムを考えなくて済む)

よくある疑問

Q: なぜ "hello"&str なの?

文字列リテラルはバイナリに埋め込まれ、プログラム実行中ずっと存在します。
その領域を指す参照が &'static str です。

// これは &'static str
let s = "hello";

// バイナリ上のどこかにこれがある
// ┌───┬───┬───┬───┬───┐
// │ h │ e │ l │ l │ o │
// └───┴───┴───┴───┴───┘

Q: + 演算子の挙動

let s1 = String::from("Hello, ");
let s2 = String::from("world!");

// String + &str → String
let s3 = s1 + &s2;  // s1 は move される
// println!("{}", s1);  // エラー!s1 はもう使えない

// 複数連結
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;
// または
let s = format!("{}-{}-{}", s1, s2, s3);  // こっちが読みやすい

Q: UTF-8 とインデックス

Rustの文字列は UTF-8 エンコーディング。

let s = "こんにちは";

// バイト数
println!("{}", s.len());  // 15 (3bytes × 5文字)

// 文字数
println!("{}", s.chars().count());  // 5

// インデックスアクセスはできない
// let c = s[0];  // エラー!

// イテレーションは可能
for c in s.chars() {
    println!("{}", c);
}

// バイト単位でのスライスは可能(ただし注意)
let hello = &s[0..3];  // "こ"
// let invalid = &s[0..1];  // パニック!UTF-8 の途中で切れる

まとめ

文字列型まとめ

所有権 サイズ 用途
String あり 24B 所有したい・変更したい
&str なし 16B 参照・読み取りのみ
&'static str なし 16B 文字列リテラル
Box<str> あり 16B 固定長で省メモリ
Cow<str> 状況次第 24B 条件付き変更
&String なし 8B ほぼ使わない

ベストプラクティス

  1. 引数には &str を使う
  2. 戻り値は String が基本(新しい文字列を返す場合)
  3. 構造体のフィールドは String(ライフタイムを避けたいなら)
  4. &String は使わない
  5. パフォーマンスが重要なら Cow<str> を検討

判断フローチャート

文字列を使いたい
    │
    ├─ 所有したい?
    │   ├─ Yes → String
    │   └─ No → &str
    │
    ├─ 関数の引数?
    │   └─ &str を使え
    │
    ├─ 関数の戻り値?
    │   ├─ 新しい文字列 → String
    │   └─ 入力の一部 → &str(ライフタイム)
    │
    └─ 構造体のフィールド?
        ├─ シンプルに → String
        └─ 参照で十分 → &'a str

文字列型が多いのは、メモリ効率と安全性のトレードオフを細かく制御できるようにするため。最初は String&str だけ覚えておけば大丈夫です!

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

23
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?