文字列
Rustにおける文字列はかなり特殊です。
通常のプログラミング言語であれば「とりあえず""で囲んで宣言(文字列リテラル)しておけばStringとして使えるでしょ」となりますが、Rustの場合は異なります。
文字列スライス
Rustにおいて文字列リテラルは、文字列スライスと呼ばれる型になります。
型表記は&strです。
use std::any::type_name;
// 'staticは後ほど説明します。&strが型表記である点だけご理解ください
fn type_of<T>(_: &T) -> &'static str {
type_name::<T>()
}
fn main() {
let x = "hoge";
println!("x の型: {}", type_of(&x));
}
文字列リテラルはヒープメモリではなく、静的領域に格納されます。
その静的領域に格納された、連続したデータ(文字)に対する参照になるため、スライスとして扱うことができます(故に、メモリ消費が抑えられます)。
なので、文字列リテラルは、Rustでは文字列スライスと呼ばれる表現になるのです。
String型
RustにおけるString型は文字列スライスと異なり、ヒープメモリを確保し、動的なサイズ変更が可能です。
JavaやC#の経験者であれば、StringBuilderの位置付けがRustにおけるString型と理解してもらうとわかりやすいのかなと思います(実際キャパシティなども設定可能です)。
文字列スライスと異なり、String型を変数で扱う際は所有権が発生します。
fn main() {
let mut str = String::from("Hello, world!"); // 所有権発生
str.push_str("!!"); // 動的なサイズ変更が可能
println!("{}", str);
}
文字列スライスと文字列は別物なので変換が必要
Rustにおいて文字列スライスと文字列は型自体が異なります。
そのため、相互に代入等はできません。
fn main() {
let x = "hoge";
let y: String = x; // これはエラー
println!("{}", y);
}
それぞれ変換処理を持つため、適宜変換していく必要があります。
面倒ではありますが、スライスの特性を活かすことでメモリを効率よく扱えるので、ここはトレードオフになる部分です。
スライスは元のデータを所有せず、データへのポインタと要素数という情報だけを持ちます。そのため、メモリをコピーせず効率的に扱えるというのが大きなポイントです。
fn main() {
let x = "hoge";
let y: String = x.to_string(); // to_string()メソッドでString型に変換
println!("{}", y);
}
fn main() {
let x = "hoge".to_string();
let y: &str = &x; // この場合、静的領域ではなくヒープメモリに格納されたデータに対する文字列スライスになります
println!("{}", y);
}
生文字列
通常の文字列リテラルは\をエスケープシーケンスとして認識します。
しかしファイルパス等、\をただの文字列として扱いたい場合にいちいちエスケープ処理を施すのは面倒臭いです。
そういった場合、先頭にrをつけることで生文字列として扱えるようになります。
// 通常の文字列
let path = "C:\\Users\\name\\file.txt"; // エスケープ必要
// 生文字列
let r_path = r"C:\Users\name\file.txt"; // エスケープ不要
また、さらに#を追加すると、改行も含めて取り扱うことができるようになります。
let html = r#"
<div class="container">
<p>こんにちは世界!</p>
</div>
"#;
String型の実態と注意点
String型は動的サイズ変更が可能な集合、つまりベクタです。
また、文字はUTF-8のUnicodeで管理されており、8byteの集合として表すことができます。
つまり、String型は実態としてはVec<char>=Vec<u8>になります(型エイリアスによりStringという表記でも扱える、という風に捉えていただくとわかりやすいかと思います)。
fn main() {
let x = String::from("デスティニー");
let y: Vec<u8> = x.into_bytes();
println!("{}", y.len());
}
ここで注意していただきたいのは、String型に対してlenメソッドを使った場合も、文字数ではなくバイト数が返ってくるということです(実態がVec<u8>なので……)。
fn main() {
let x = String::from("デスティニー");
println!("{}", x.len());
}
文字数を取得したい場合、.chars().count()を使いましょう。
fn main() {
let x = String::from("デスティニー");
println!("{}", x.chars().count());
}
ライフタイムパラメータ
ライフタイムの明示が必要なケース①
以前ライフタイムの説明の際に下記のように説明しました。
生存期間は原則として「変数束縛〜変数が最後に利用されるまで」になります。
基本的にはこのルールに則る形で問題ありません。
ただ、構造体の中に共有参照・可変参照のフィールドを持たせたい場合など、構造体本体とフィールドのライフタイムが異なるケースが出てきます。
具体的に例を見てみましょう。
struct Person {
name: &String
}
fn main() {
let person;
{ // Rustでは{}でブロックスコープを定義できます
let name = String::from("Alice");
person = Person { name: &name };
} // ここでブロックスコープを抜けて変数nameのメモリが解放されます
println!("{}", person.name); // nameを参照したいけど、既に解放されているので……?
}
もし上記処理が許容されてしまうと、どうなるでしょうか?
そう、いわゆるnull参照問題を引き起こしてしまうことになります。
Rustの設計思想としては許せることではありませんね!
ですが、下記のような処理だったらどうでしょうか?
struct Person {
name: &String
}
fn main() {
let person;
{
let name = String::from("Alice");
person = Person { name: &name };
println!("{}", person.name);
}
}
これならライフタイムの範囲内での参照なので問題なしですね。
しかしこのレベルのライフタイムをコンパイラが全てチェックするのはかなり厳しいです(ただでさえ長いビルド時間がさらに長くなってしまいます)。
そのため、共有参照・可変参照のフィールドを持たせたい場合、Rustではライフタイムパラメータを使ってライフタイムを明示する必要があります。
ライフタイムパラメータによる明示
ライフタイムパラメータを付与することで、コンパイラが処理の安全性をチェックできるようになります。
struct Person<'a> { // 構造体はaというライフタイムを参照することを宣言
name: &'a String // nameフィールドはaというライフタイムを持つことを明示
}
fn main() {
let person;
{
let name = String::from("Alice");
person = Person { name: &name }; // ②nameのライフタイムがperson.nameと噛み合わないのでエラー
}
println!("{}", person.name); // ①person.nameはここで参照されるので、少なくともここまではライフタイムが存在しないといけないことがわかる
}
ライフタイムが明示されたことにより、上記処理はborrowed value does not live long enoughというエラーになります。
反対に下記であれば問題なく通ることも確認してみましょう。
struct Person<'a> { // 構造体はaというライフタイムを参照することを宣言
name: &'a String // nameフィールドはaというライフタイムを持つことを明示
}
fn main() {
let person;
{
let name = String::from("Alice");
person = Person { name: &name }; // ②nameのライフタイムがperson.nameと噛み合うのでOK
println!("{}", person.name); // ①person.nameはここで参照されるので、少なくともここまではライフタイムが存在しないといけないことがわかる
}
}
ライフタイムの明示が必要なケース②
下記のケースではコンパイルエラーになります。
fn main() {
let string1 = "xyz";
let result;
{
let string2 = String::from("abcd");
result = longest(string1, &string2);
}
println!("The longest string is {}", result);
}
fn longest(x: &str, y: &str) -> &str {
if x.len() > y.len() {
x
} else {
y
}
}
関数longestの引数xとyのライフタイムが異なっているため、返される値によってnull参照問題を引き起こしてしまう可能性があるからです。
とにかくコンパイラがライフタイムを推論できず安全性を担保できない場合、ライフタイムの明示が必要になります。
fn main() {
let string1 = "xyz";
let result;
{
let string2 = String::from("abcd");
result = longest(string1, &string2); // ②引数のライフタイム不一致のためエラー
}
println!("The longest string is {}", result);
}
// ①ライフタイムパラメータでライフタイムを明示
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
関数が明示するライフタイムと合うように処理を修正してあげれば、問題なく通るようになります。
fn main() {
let string1 = "xyz";
let string2 = String::from("abcd");
let result = longest(string1, &string2); // ②引数のライフタイムが一致するためOK
println!("The longest string is {}", result);
}
// ①ライフタイムパラメータでライフタイムを明示
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
マクロ
概要
Rustでは処理を一定の法則に従って読み替える、マクロと呼ばれる機能があります。
詳細を書こうとするとひとつの記事が書けてしまうので、今回は定義されたマクロの利用方法だけ覚えてください。
マクロの利用方法
末尾に!をつけると、指定マクロを利用するという宣言になります。
実はこれまでご紹介した処理の中にも、!があったのを覚えていますでしょうか?
そう、ベクタの簡易定義や標準出力で実は使っていたんですね。
fn main() {
// vec!の実態はvecというマクロの利用宣言
let mut v = vec![1, 2, 3];
// ループ処理
for i in v {
println!("{}", i); // println!の実態はprintlnというマクロの利用宣言
}
}
ベクタを例にすると、本来であれば、ベクタを利用するには下記のような手続きが必要になります。
// 本来なら下記のようにインスタンス生成→値設定が必要
fn main() {
let mut v = Vec::new();
v.push(1);
v.push(2);
v.push(3);
// ループ処理
for i in v {
println!("{}", i);
}
}
しかし毎回こんな記述をしていたら面倒なので、より簡潔に記述できるよう、vec!というマクロが用意されているんですね。
vec!は後ろに記述した配列をもとに、下記のように処理を読み替えるマクロになっています。
// let mut v = vec![1, 2, 3];は、マクロルールに則って下記のように読み替えられ実行される
let mut v = Vec::new();
for item in [1, 2, 3] {
v.push(item);
}
処理の中で!を見つけたら、ここはマクロによって別の処理に読み替えられるんだな、という風に理解してもらえればOKです。
お疲れ様でした!
お疲れ様でした。
次回は、Rustのクレート作成・モジュール分割・テスト実装・ワークスペースについて学びます。
Rust入門講座⑥クレート・モジュール・テスト・ワークスペース
Hope you enjoy it!