RustのcharとstrとString
Rustで文字(列)となんとなく付き合う日々から脱却するために、文字(列)の型について調べてみました。Rust初学者である筆者は、特に&str
とString
に関して、同じような型が2つもあることに疑問を抱きつつも、とりあえず書いてみてはIDEやコンパイラに指示された通りに書き直し、それっぽく動くコードを作るというのを繰り返していました。
調べてみるとこの二つにはRustのオーナーシップやメモリ管理の違いもあり、奥が深くて勉強になりました。
Rustで文字(列)の型
Rustの文字(列)を扱う型に、char
、str
、String
があります。str
については通常は参照型の&str
として扱われます。共通点として、Rustは文字をUnicode/UTF-8として扱います。つまり、日本語や絵文字などの文字も標準で扱うことが可能です。
char
Rustのプリミティブ型で、32ビット(4バイト)のUnicode/UTF-8文字を保持します。
Unicode/UTF-8を扱えるため、下記のように日本語や絵文字も扱うことが可能です。
let japanese = '禅'; // 日本語を扱える
let heart = '💓'; // 絵文字も扱える
なお宣言時に注意が必要なのが、文字をシングルクォーテーションで囲んで定義したものはchar
型に、ダブルクォーテーションで囲んで定義したものは&str
型に推論される点です。
let a = 'a' // char型
let b = "b" // &str型
str
Rustの文字列を扱うプリミティブ型です。string slice
(文字列スライス)とも呼ばれます。
通常は参照型&str
として利用され、実態はstring slice
の最初のバイトへのポインタとその長さから構成される&[u8]
型のスライスです。既存の文字列の参照型となっているため、文字列自体のオーナーシップは持ちません。
利用方法によって、ポイント先となっている文字列の保存場所はアプリケーションバイナリ、スタック領域、ヒープ領域など異なるようです。例えば、文字列リテラルとして定義した場合は、コンパイル時にポイント先の文字列が確定しているため、アプリケーションバイナリに直接組み込まれます。
let slice = "Hello, World!" // &str型、文字列はアプリケーションバイナリに組み込み
その他、どのようなケースでスタック領域やヒープ領域に保存されるのかは調べきれていません。
また既存の文字列の参照型のため、後述するString
型の文字列の参照して定義することができます。この時、String
型をそのまま参照すると&String
型になってしまう点に注意が必要です。
let s = String::from("string") // String型
let string_slice = &s[..]; // &str型
let ref_s = &s; // &String型 (そのまま参照すると&str型にならない)
String
Rustの標準ライブラリで提供される型で、これまでのchar
型やstr
型のようなプリミティブ型とは異なります。ソースコードで確認できるとおり、実態はVec<u8>
のラッパーとなっています。
pub struct String {
vec: Vec<u8>,
}
&str
との違いとして、String
型は文字列自体のオーナーシップを持ちます。また文字列はヒープ領域に保存され、動的な文字列サイズに対応して、後から柔軟に文字列の変更が可能です。
宣言はライブラリの関数を利用して行うことができます。
let empty = String::new() // 空のString
let hello = String::from("hello") // String型の"Hello"
それぞれの型間の変換
各文字(列)の型間での変換についてみてみたいと思います。記載の変換例は一部の例で他にも方法がいろいろとあると思います。
&str → char
&str
型からchar
型への変換は、下記のようにイテレーターを利用して&str
型の文字列からchar
型の文字を取り出すことが可能です。
let s = "Hello, world!💓";
for c in s.chars() { // charを取り出すイテレーター
println!("{}", c);
}
実行すると文字が1文字ずつ改行されて出力され、文字列から文字を一つずつ取り出せているのが分かります。
H
e
l
l
o
,
w
o
r
l
d
!
💓
また文字が格納された配列(Vec<char>
型)に変換することも可能です。
let s2 = "Hello, Rust!🦀";
let s2_collection: Vec<char> = s2.chars().collect();
println!("{:?}", s2_collection);
実行すると下記のような配列が得られているのが分かります。
['H', 'e', 'l', 'l', 'o', ',', ' ', 'R', 'u', 's', 't', '!', '🦀']
String → char
&str
型と同じように、イテレーターを使って文字を一つずつ取り出したり、Vec<char>
型に変換することが可能です。
let s1 = String::from("Hello, world!💓");
for c in s1.chars() {
println!("{}", c);
}
実行結果。
H
e
l
l
o
,
w
o
r
l
d
!
💓
Vec<char>
へ変換。
let s2 = "Hello, Rust!🦀";
let s2_collection: Vec<char> = s2.chars().collect();
println!("{:?}", s2_collection);
実行結果。
['H', 'e', 'l', 'l', 'o', ',', ' ', 'R', 'u', 's', 't', '!', '🦀']
char → &str
char
から&str
ですが、少し無理やり感もありますが、一度String
型を経由し、その参照をスライスとして取ることで&str
に変換できます。もっと直接的な方法があるかもしれません。 → (2022/12/23 アップデート) as_str()
で直接&str
に変換可能です。
let c = 'c'; // char型
let s1 = &c.to_string()[..]; // &str型
let s2 = c.as_str() // (2022/12/23 アップデート) &str型
char → String
char
からString
は、to_string()
やto_owned()
を利用して変換できます。
let c = 'c'; // char型
let s1 = c.to_string(); // String型
let s2 = c.to_owned(); // String型
String → &str
String
から&str
への変換は、文字列スライスの参照として定義することで変換できます。s3
のように文字列の一部分を文字列スライスとして宣言することも可能です。 → (2022/12/23 アップデート) .as_str()
を使って直接変換することが可能です。
let s1 = String::from("Hello, Rust!"); // String型
let s2 = &s1[..]; //&str型
let s3 = &s1[0..8]; //&str型
let s4 = s1.as_str(); //&str型 2022/12/23 アップデート
println!("{}", s2);
println!("{}", s3);
println!("{}", s4);
実行すると、元のString
型の全体(s2
)と一部(s3
)がそれぞれ出力されます。
Hello, Rust!
Hello,
Hello, Rust!
注意が必要なのが、Rustによる型の自動変換です。例えば、下記の例では引数として&str
型を取るprint
関数をmain関数から呼び出す際に、&String
型の値(s
)を渡しています。一見すると型のミスマッチでコンパイル時にエラーになりそうですが、このコードは正常にコンパイルされて実行できます。その場合は、Rustが引数の型を自動的に&String
型から&str
型に変換してくれるからだそうです。少し紛らわしいですね。
fn print(s: &str){ // &str型を引数として受け取る
println!("{}", s);
}
fn main() {
let s = &String::from("Hello, Rust!"); // &String型
print(s); // &String型を引数として渡すが、Rustが自動的に&str型に変換
}
&str → String
&str
型からString
型への変換は、char
型からString
型への変換と同様にto_string()
やto_owned()
を使うことでできます。
let s1 = "to_string"; //&str型
let s2 = s1.to_string(); //String型
let s3 = "to_owned"; //&str型
let s4 = s2.to_owned(); //String型
こちらももしかすると、関数の引数の型が異なっていてもRustが自動的に型を変換してくれるのかと思い、試してみたのですがこれはダメでした。
fn print(s: &String) { // &String型を引数として受け取る
println!("{}", s);
}
fn main() {
let s = "Hello, Rust!"; // &str型
print(s); // &str型を引数として渡してみるが、ここは自動変換されず。
}
型のミスマッチによるコンパイルエラーになります。
error[E0308]: mismatched types
--> src/main.rs:7:11
|
7 | print(s);
| ^ expected struct `String`, found `str`
|
= note: expected reference `&String`
found reference `&str`
&strとStringの使い分け
&str
とString
の違いがわかったところで、2つの使い分けはこんな感じかなと思っています。
- &strは文字列への参照のため、既存の文字列を変更せずにそれ自体もしくはその一部を参照するのみで十分なケース
- Stringは文字列のオーナーシップと値を保有するため、文字列自体を柔軟に変更したいケース
所感
Rustは文字や文字列だけでも奥が深くて勉強になりました。
正直なところ、まだ完璧に使い分けられる自信はありませんが、経験や慣れな部分もあると思うので、これからもRustのコードを書いていきたいと思います。
Discussion
String→&strは
as_str
を使うのが一般的かもしれません。ありがとうございます。内容を更新させていただきました。