Rustの&strとStringについて整理したかった

はじめに

Rustを触っていて今までなぁなぁで&strとStringを使っており、少し調べて違いを整理したかったのでこの記事を書いてみました。

間違った事を書いている可能性もあるので参考にする際は十分にご注意ください。

では、&strとStringを知る上でRustのスライスという概念とメモリのスタック領域とヒープ領域の知識が必要なのでまずはそこから書いていきます。

スライスについて

Rustにはスライスという概念があります。

スライスとはコレクション内の一連の要素を参照したものです1

コレクションとはRustのデータ構造の一つで、複数の値を含むことのできる型です。ヒープに確保され、実行時に伸縮可能なのが特徴です。ベクタ文字列(String)HashMapなどがコレクションに該当します。

スライスは2つのデータからできており、一つは開始地点のポインタ もう一つはそのスライスの長さ(length)です。これらはスタックに積まれます。2

例えば以下のようなコード

let message = String::from("Hello world");

//"world"が取り出される
let world = &message[6..11];

があったとして、メモリモデルをイメージすると以下のようになります。

f:id:matsumaee:20210719193801p:plain
sliceのメモリモデル

今回のスライス worldは先頭が'w'なのでwがあるポインタを指しています。

スタックとヒープ

次はヒープとスタックについてです。

メモリの領域にはヒープ領域とスタック領域という領域があります。他にもいくつか領域はありますが今回はヒープとスタックに絞ります。

ヒープ

ヒープはサイズが可変なデータを保持するためのメモリ領域です。ヒープにデータを置く際、プログラムはOSを訪ねてOSは十分なサイズの空のメモリ領域を見つけ出し、その領域を使用中としてその領域のポインタをプログラムに返します。ポインタはメモリ上のアドレスです。これらの一連の流れをアロケートすると言います。

ポインタ自体は固定サイズなので後述するスタックに積まれます。 ですからヒープのデータが必要になる際は、スタックにあるポインタを追う必要があります。

ヒープへのデータアクセスはスタックに比べ低速です。

スタックはデータ構造がシンプルなのに対し、ヒープはポインタを追う必要があるためです。3

スタック

スタックはLIFO(Last In, First Out)のデータ構造をした実行時に利用できるメモリ領域です。

LIFOとはLast In, First Outの略で、日本語で言うと後入れ先出しと言います。箱の中に本を縦に積み上げていったとしたら取り出す際は上から取り出す(つまり後から積み上げられたやつから取り出す)イメージです。

このデータ構造はとてもシンプルでデータを取りやすいため、スタックのデータアクセスは高速と言われています。

スタックにデータを積み上げる際、データは全て既知で固定なサイズでなければなりません。このこともスタックのデータアクセスが高速だと言われている理由の一つです。4

本当に触りの部分しか触れられていませんが、これらを頭に入れて&strとStringについて書いていきます。

&str

Rustには&str型とString型が用意されており、&strはプリミティブ型でStringは標準ライブラリが提供する型という大まかな違いがあります。

まずはこの&strについて書いていきます。

&strは先程書いた通りプリミティブ型に分類されます。

&strはスライスで一種で、文字列リテラルと呼ばれ、不変でありスタックに積まれます。

不変というのはread onlyでありリサイズが不可という事です。

文字列リテラルが不変なお陰でコンパイル時にどんなデータか判明するので、文字列は最終的なバイナリファイルに直接ハードコードされ非常に高速で効率的です。5

そして&strがRustではどういう風にメモリ管理されてるかイメージする上で大事なメモリモデルは以下の画像の通りです。

f:id:matsumaee:20210719193858p:plain
&strのメモリモデル

非常に簡潔かつ分かりやすかったので画像をYuki ToyodaさんのRustハンズオンのスライドから拝借しました。

画像の通り文字列リテラル&strはスタックにpreallocated read only memoryにある実データへのポインタとその長さlengthなどの文字列リテラルに関するデータを積みます。

&strは不変な文字列なのでCLIアプリの--helpで出力されるような実行時に動的に変わらないような文字列を保持したい時とかに使えそうです。

String

お次はString型です。

String型は端的に言うとヒープにメモリを確保し、コンパイル時にサイズが不明なテキストを保持できる型です。

公式の書き方で書くならば伸長可能、 可変、所有権のあるUTF-8エンコードされた文字列型です。6

Stringのメモリモデルは以下のとおりです。また拝借しました。

f:id:matsumaee:20210719193933p:plain
Stringのメモリモデル

Stringは3つのパーツで構成されており、文字列の中身を保持するメモリ領域(ヒープ)へのポインタその文字列の長さ(length)確保したヒープの許容量(ヒープの再割り当てなしで格納できるUTF-8バイト長)でできています。これらはスタックに積まれます。7

Stringは伸縮可能なのでCLIアプリなどで標準入力を受けつける時に入力された文字列を保持する時などに使えそうです。なぜなら入力される文字列はコンパイル時には分からずユーザーによって変わる動的なものだからです。

最後に

Rustの文字列の扱いは今までJavaScriptぐらいしかやってこなかった自分にとっては難しくてとっつき辛かったです。

この記事で書いた事はRustの文字列周りのほんの上辺だけで本当はもっと複雑かもしれませんが、誰かの参考になれば幸いです。

最後まで読んでくださりありがとうございました。