はじめに
Rustを始めて最初に混乱するのが、文字列型の多さ。
String&str&StringBox<str>Cow<str>&'static str
なんでこんなに種類があるんだ...
この記事で、それぞれの違いと使い分けを完全に理解しましょう。
目次
基本の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 | ほぼ使わない |
ベストプラクティス
-
引数には
&strを使う -
戻り値は
Stringが基本(新しい文字列を返す場合) -
構造体のフィールドは
String(ライフタイムを避けたいなら) &Stringは使わない-
パフォーマンスが重要なら
Cow<str>を検討
判断フローチャート
文字列を使いたい
│
├─ 所有したい?
│ ├─ Yes → String
│ └─ No → &str
│
├─ 関数の引数?
│ └─ &str を使え
│
├─ 関数の戻り値?
│ ├─ 新しい文字列 → String
│ └─ 入力の一部 → &str(ライフタイム)
│
└─ 構造体のフィールド?
├─ シンプルに → String
└─ 参照で十分 → &'a str
文字列型が多いのは、メモリ効率と安全性のトレードオフを細かく制御できるようにするため。最初は String と &str だけ覚えておけば大丈夫です!
この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!