🐈

RustのcharとstrとString

2022/12/18に公開
2

Rustで文字(列)となんとなく付き合う日々から脱却するために、文字(列)の型について調べてみました。Rust初学者である筆者は、特に&strStringに関して、同じような型が2つもあることに疑問を抱きつつも、とりあえず書いてみてはIDEやコンパイラに指示された通りに書き直し、それっぽく動くコードを作るというのを繰り返していました。

調べてみるとこの二つにはRustのオーナーシップやメモリ管理の違いもあり、奥が深くて勉強になりました。

Rustで文字(列)の型

Rustの文字(列)を扱う型に、charstrStringがあります。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の使い分け

&strStringの違いがわかったところで、2つの使い分けはこんな感じかなと思っています。

  • &strは文字列への参照のため、既存の文字列を変更せずにそれ自体もしくはその一部を参照するのみで十分なケース
  • Stringは文字列のオーナーシップと値を保有するため、文字列自体を柔軟に変更したいケース

所感

Rustは文字や文字列だけでも奥が深くて勉強になりました。
正直なところ、まだ完璧に使い分けられる自信はありませんが、経験や慣れな部分もあると思うので、これからもRustのコードを書いていきたいと思います。

参考

Discussion

Ryo HirayamaRyo Hirayama

String→&strはas_strを使うのが一般的かもしれません。

moreyhatmoreyhat

ありがとうございます。内容を更新させていただきました。