Prompt
Rustの内部の文字表現はどうなってる?
さすが情報工学を専攻しているだけあって、核心を突く質問だね!
Rustの文字表現は、一言で言うと**「安全性とモダンな国際化を両立させるために、レイヤーごとに厳格に使い分けている」**んだ。
結論から言うと、Rustには char型(4バイト) と String/str(UTF-8可変長) の2つの顔がある。ここが混乱の元であり、面白さの肝でもあるぜ!
1. char 型:常に「4バイト」の固定長
Rustの char は、C言語の char(1バイト)とは全くの別物。**「Unicodeスカラ値」を一つ保持するために、常に4バイト(32ビット)**のメモリを確保するんだ。
-
理由: 世界中のどんな文字(漢字、絵文字、アラビア語など)も、1つにつき1つの「数値(コードポイント)」として一意に扱えるようにするため。
-
メリット:
Vec<char>にすれば、「3文字目はどれ?」といったランダムアクセスが でできる。
2. String と str:効率的な「UTF-8」
一方で、実際のテキストデータを保持する String や &str は、中身をUTF-8エンコーディングで保持している。
-
UTF-8とは: 1文字を 1〜4バイトの可変長で表現する方式。
-
メリット:
-
省メモリ: 英数字(ASCII)なら1文字1バイトで済む。
-
互換性: Webや他の多くのシステムと親和性が高い。
-
ここで君のさっきの「👨5👦」の謎が解ける。
String は「バイトの列」としてメモリに並んでいるから、文字の境界が1バイト目にあるとは限らないんだ。だから、安易なインデックスアクセス(s[0] など)を禁止して、安全性を守っているんだね。
3. レイヤーの構造(情報工学的な視点)
Rustの文字表現をスタックで表すとこんな感じになるよ:
-
Grapheme Cluster (書記素クラスタ): 人間が見て「1文字」と感じる単位。(例:家族の絵文字)
-
Unicode Scalar Value (char型): Unicodeで定義された論理的な1文字。(例:家族絵文字を構成する各要素)
-
UTF-8 Bytes: メモリ上の生データ。
💡 見落とされがちな視点:OSとのやり取り
君が Windows 11 と Ubuntu を使い分けているなら、さらに「もう一つの表現」を知っておく必要がある。
-
WindowsのAPI: 内部的に UTF-16(2バイト単位)を使っていることが多い。
-
Linux (Ubuntu) のAPI: OSのロケール設定(多くはUTF-8) に依存する。
Rustでファイルパスなどを扱うときに String ではなく OsString や PathBuf を使うのは、この「OSごとの文字表現の差」を吸収するためなんだ。String は「常に正しいUTF-8」であることを保証する型だから、OSからくる「もしかしたら不正なUTF-8かもしれないデータ」を直接入れることはできないんだぜ。
🏁 まとめ
-
charは 4バイト。数学的な「1コードポイント」。 -
Stringは UTF-8。メモリ効率重視の「可変長バイト列」。 -
安全性 のため、バイト数と文字数は明確に区別される!
「一文字」という概念が、メモリ上ではこれほどまでに奥深い……。情報工学徒として、このレイヤー構造はゾクゾクするほど美しいと思わないかい?😊
Prompt
&strはどこかにある固定のデータの実体から切り出していると聞いたけど、それもutf8?
その通り!&str も中身は 100% 厳格な UTF-8 だよ。
情報工学的に言うと、&str は**「どこかにある UTF-8 データの塊を指し示すスライス(Window)」**なんだ。
君の疑問をさらに深掘りして、その「どこか」の正体と、メモリ上での姿を解剖してみよう!
1. 「どこかにある実体」の正体
&str が指している「実体」は、主に以下の3パターンのどれかだよ。
-
バイナリ埋め込み(静的メモリ):
let s = “Hello”; と書いた場合。これはコンパイルされたバイナリの中に直接書き込まれていて、実行時にメモリの「読み取り専用データセクション」にロードされる。
-
ヒープメモリ (String からの切り出し):
let s = String::from(“Hello”); let slice = &s[0..2]; とした場合。ヒープにある String の一部を覗き見している状態だね。
-
スタックメモリ:
スタック上のバイト配列([u8; N])がたまたま正しい UTF-8 だったら、それを &str として借用することもできるよ。
2. メモリ上での &str の構造
ここが情報工学徒として一番面白いところ!
&str は、実は**「2つのデータがセットになった構造体(Fat Pointer)」**として扱われているんだ。
-
ポインタ (Pointer): 実体のデータの先頭アドレス。
-
長さ (Length): そこから何バイト分が有効か。
**「文字数」ではなく「バイト数」**を持っているのがポイント。だから、&str を通じてデータを見るときも、Rust は常に「ここからここまでは正しい UTF-8 ですよ」という保証の上で動いているんだ。
3. なぜ &str も UTF-8 なのか?
Rust の型システムにおいて、str(および &str)という型は**「有効な UTF-8 であること」が言語レベルでの不変条件(Invariant)**になっているからなんだ。
もし無理やり UTF-8 じゃないバイト列を &str に変換しようとすると(from_utf8 とかを使って)、Rust は「それ、壊れてるから無理!」とエラーを出すか、パニックする。
💡 見落とされがちな落とし穴:切り出しの境界
「どこかから切り出す」とき、バイト単位で指定するから、こんな事故が起きる可能性がある:
Rust
let s = "こんにちは"; // 「こ」は3バイト
let slice = &s[0..1]; // ❌ 1バイト目だけで切ろうとするとパニック!
UTF-8 の文字の途中でぶった切るような &str は、Rust が全力で作成を阻止する。これが Rust の「安全性」の正体だね。
🏁 まとめ
-
&strも絶対 UTF-8。 -
実体はバイナリの中だったりヒープだったりする。
-
構造は「アドレス」と「バイト長」のペア(ファットポインタ)。
-
切り出しは文字の境界(マルチバイトの区切り)を守る必要がある。
「切り出した窓」であっても、その中身の純度は 100% 保証されている……。この徹底した管理、美しすぎると思わないかい?😊
次は、この &str を使って「所有権を持たずに文字列を効率よくスライスして、パズルみたいに組み合わせる」技を試してみる?
Would you like to see how to safely slice a string at character boundaries using the char_indices method?