Open9

The Rust Programming Language

derwindderwind

The Rust Programming Language 日本語版

https://doc.rust-jp.rs/book-ja/

Rust Playground

Why Rust?

パフォーマンス

ランタイムやガベージ・コレクタを持たないRustは、パフォーマンスを重視するサービスや組込みデバイスで動作し、他の言語と簡単に統合できる。

信頼性

Rustの豊富な型システムと所有権モデルは、メモリ安全性とスレッド安全性を保証しており、コンパイル時に多くのバグを排除することができる。

生産性

Rustには、優れたドキュメント、便利なエラーメッセージを備えたフレンドリーなコンパイラー、そして統合パッケージ・マネージャーとビルド・ツール、オートコンプリートや型検査を備えたスマートなマルチエディター・サポート、オートフォーマッターなど、一流のツールがある。

生産現場でのRust

世界中の何百もの企業が、高速、低リソース、クロスプラットフォームのソリューションとしてRustを利用しています。FirefoxDropboxCloudflareなど、皆さんがよくご存知のソフトウェアがRustを使用しています。スタートアップから大企業まで、組み込み機器からスケーラブルなウェブサービスまで、Rustは最適です。

derwindderwind

1. 事はじめ

インストール

Using rustup (Recommended)

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

アップデート

rustup update

...
stable-x86_64-unknown-linux-gnu updated - rustc 1.78.0 (9b00956e5 2024-04-29) (from rustc 1.77.2 (25ef9e3d8 2024-04-09))

Hello, World!

  • gccclang <==> rustc
fn main() {
    // 世界よ、こんにちは
    println!("Hello, world!");
}

Hello, world!

メモ: println! はマクロ。A half-hour to learn Rustより、

println!("{}", "Hello there!");

は以下のような感じに対応:

use std::io::{self, Write};
io::stdout().lock().write_all(b"Hello there!\n").unwrap();

Hello, Cargo!

Cargo: Rustのビルドシステム兼パッケージマネージャ

cargo new hello_cargo
cargo build --release

Cargo.toml & Cargo.lock

ls -1 target/debug/deps/ | head -n 8
adler-a6cfdbbac183b7a9.d
aho_corasick-a48502399b2eb1c0.d
approx-1f569209f47200b0.d
arrayref-c41e094af6470e80.d
arrayvec-830bf82a18c17c77.d
autocfg-a665a4b03b8e7f1d.d
az-4da15f065a63042b.d
base64-ecde3d67aae72aa0.d
derwindderwind

2. 数当てゲームのプログラミング

$ cargo new guessing_game
$ ls -lR guessing_game/
guessing_game/:
total 8
-rw-r--r-- 1 xxx xxx   84 May 29 22:38 Cargo.toml
drwxr-xr-x 2 xxx xxx 4096 May 29 22:38 src

guessing_game/src:
total 4
-rw-r--r-- 1 xxx xxx 45 May 29 22:38 main.rs
$ cat guessing_game/Cargo.toml
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

[dependencies]

依存クレートを追加してビルド:

$ echo 'rand = "0.8.5"' >> Cargo.toml
$ cargo build
   Compiling libc v0.2.155
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.17
   Compiling getrandom v0.2.15
   Compiling rand_core v0.6.4
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (/home/xxx/git/guessing_game)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.47s

「数当てゲームの完全なコード」は「リスト2-6」に掲載。二分探索で \log_2 100 = 2 / 0.30 = 7 回程度で正解が当たるはず。

derwindderwind

3. 一般的なプログラミングの概念

変数と可変性

データ型

  • スカラー型
  • 整数型: i8, u8, ..., u64
    • isizeusize 型は、プログラムが動作しているコンピュータの種類に依存
  • 整数リテラル: 98_222, 0x.., 0o.., 0b1111_0000, b'A'
  • 浮動小数点型: f32, f64
  • 数値演算
    • let floored = 2 / 3; // 0
  • 論理値型
    • bool: true, false
  • 文字型
    • let c = 'z';
    • let z = 'ℤ';
  • タプル型
    • let tup: (i32, f64, u8) = (500, 6.4, 1);
  • 配列型
    • let a = [1, 2, 3, 4, 5];
    • let a: [i32; 5] = [1, 2, 3, 4, 5];
    • let a = [3; 5]; // let a = [3, 3, 3, 3, 3];

The tuple without any values has a special name, unit. This value and its corresponding type are both written () and represent an empty value or an empty return type. Expressions implicitly return the unit value if they don’t return any other value.
(値を持たないタプルには、unitという特別な名前があります。この値と対応する型はどちらも () と書かれ、空の値または空の戻り値を表します。式は、他の値を返さない場合、暗黙的にユニット値を返します。)

関数

  • スネークケースを使うのが慣例
fn another_function() {
    println!("Another function.");  // 別の関数
}
  • 関数の戻り値は、関数本体ブロックの最後の式の値と同義
fn five() -> i32 {
    5
}

コメント

// hello, world
// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

制御フロー

    if number < 5 {
        println!("condition was true");       // 条件は真でした
    } else {
        println!("condition was false");      // 条件は偽でした
    }
    if number % 4 == 0 {
        // 数値は4で割り切れます
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        // 数値は3で割り切れます
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        // 数値は2で割り切れます
        println!("number is divisible by 2");
    } else {
        // 数値は4、3、2で割り切れません
        println!("number is not divisible by 4, 3, or 2");
    }
    loop {
        println!("again!");   // また
    }
    while number != 0 {
        println!("{}!", number);

        number -= 1;
    }
  • 範囲を逆順にしたカウントダウン
    for number in (1..4).rev() {
        println!("{}!", number);
    }

let文内でif式を使う

let condition = true;
let number = if condition { 5 } else { 6 };
derwindderwind

4. 所有権を理解する

  • ガベージコレクタなしで安全性担保を行うことができる機能

C++ だと・・・。

int main(void) {
    new char{3};  // メモリリーク
    return 0;
}

4.1. 所有権とは?

メモリの使用方法

  1. ガベージコレクション
  2. プログラマが明示的にメモリを確保したり、解放
  3. メモリは、コンパイラがコンパイル時にチェックする一定の規則とともに所有権システムを通じて管理 (Rust)

所有権規則

  • Rustの各値は、所有者と呼ばれる変数と対応している。
  • いかなる時も所有者は一つである。
  • 所有者がスコープから外れたら、値は破棄される。
fn type_of<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}

fn main() {
    {
        let s = "hello";
        println!("{} {}", s, type_of(&s)); // hello &str
    }
    println!("{} {}", s, type_of(&s));  // error[E0425]: cannot find value `s` in this scope
}
  • 文字列の値はプログラムのテキストとしてハードコードされている

2種類目の文字列型、String

  • 文字列リテラルは不変
  • コードを書く際に、全ての文字列値が判明するわけではない

ということで文字列リテラルだけでは文字列の扱いがしにくい。

String型。この型はヒープにメモリを確保。可変かつ伸長可能なテキスト破片をサポートするために、コンパイル時には不明な量のメモリを ヒープに確保して内容を保持

余談

pub struct String {
    vec: Vec<u8>,
}

らしい。(&str&[u8] らしい)

fn main() {
    {
        let mut s = String::from("hello");
        s.push_str(", world!");
        println!("{} {}", s, type_of(&s)); // hello, world! alloc::string::String
    }
    println!("{} {}", s, type_of(&s)); // error[E0425]: cannot find value `s` in this scope
}
fn main() {
    let s = String::from("hello");
}

に対して rustc --emit asm main.rs && cat main.s すると以下のようになっており、設定次第だが、.rodataセクションにいることが見える。

.L__unnamed_14:
    .ascii   "hello"
    .size    .L__unnamed_14, 5

    .type    .L__unnamed_22,@object
    .section    .rodata..L__unnamed_22,"a",@progbits

Rust playground のアセンブラはもっとあっさり目に見える。

playground::main:
	subq	$24, %rsp(スタックポインタ%rsp24バイト分減らす; スタック確保)
	movq	%rsp, %rdi(スタックポインタの値%rspを第1引数レジスタ%rdiにコピー; 引数の一部としてスタックポインタを設定)
	leaq	.L__unnamed_12(%rip), %rsi(リテラル文字列(.L__unnamed_12)のアドレスを第2引数レジスタ%rsiにロード)
	movl	$5, %edx(定数値5(「Hello」の長さ)を第3引数レジスタ%edxに移動)
	callq	<alloc::string::String as core::convert::From<&str>>::from(上記の引数を使ってString::from関数を呼び出し、スタックにStringを作成)
	movq	%rsp, %rdi(スタックポインタ%rspを再び第1引数レジスタ%rdiにコピー; drop_in_place関数にスタック上のStringオブジェクトのアドレスを渡す準備)
	callq	core::ptr::drop_in_place<alloc::string::String>(drop_in_place関数を呼び出し)
	addq	$24, %rsp(スタックポインタ%rsp24バイト分増加; スタック解放)
	retq(リターン命令)

.L__unnamed_12:(ラベル。アセンブラコード内でメモリ上の特定のアドレスを指し示す名前)
	.ascii	"hello"(ASCII文字列 "hello" をメモリに直接格納するアセンブラ命令)

以下のようなことが書いてあるが、drop_in_place というのがそれなのだろう。

String型が必要とするメモリをOSに返還することが自然な地点があります: s変数がスコープを抜ける時です。 変数がスコープを抜ける時、Rustは特別な関数を呼んでくれます。この関数は、dropと呼ばれ、 ここにString型の書き手はメモリを返還するコードを配置することができます。Rustは、閉じ波括弧で自動的にdrop関数を呼び出します。

15.3 Dropトレイトで片付け時にコードを走らせるメモリ解放

Dropトレイトにはメソッドが一つだけしかありません。dropです。これは、オブジェクトがスコープから抜けた時に自動で呼ばれます。Dropトレイトの主な使用目的は、インスタンスが所有する資源を開放することです。

変数とデータの相互作用法: ムーブ

let x = 5;
let y = x;

これは

playground::main:
    movl    $5, -8(%rsp)(即値(リテラル)5を、スタックポインタ(%rsp)から8バイト下のメモリアドレスに保存)
    movl    $5, -4(%rsp)(即値5を、スタックポインタ%rspから4バイト下のメモリアドレスに保存)
    retq

つまり、スタックに対して2つの32ビット整数5を保存

println!("{} {}", x, y);

も OK。i32型(整数型)はCopyトレイトが実装されているため、変数xの値は変数yにコピーされる。型がCopyトレイトに適合していれば、代入後も古い変数が使用可能。

一方、以下は似ているが String 型は Copyトレイトを実装していないため状況が異なる。

let s1 = String::from("hello");
let s2 = s1;

これは、

playground::main:
    subq    $56, %rsp

    ;; let s1 = String::from("hello");
    leaq    8(%rsp), %rdi(leaqはロードエフェクティブアドレス(Load Effective Address)を行う命令。ここでは、%rspから8バイトオフセットしたアドレスを計算し、その結果を第1引数レジスタ%rdiにロード i.e., %rdi = %rsp+8)
    leaq    .L__unnamed_12(%rip), %rsi(リテラル文字列のアドレスを計算し、それを第2引数レジスタ%rsiにロード)
    movl    $5, %edx
    callq    <alloc::string::String as core::convert::From<&str>>::from

    ;; let s2 = s1(Stringの内部構造をコピーしている操作)
    movq    8(%rsp), %rax(生成されたStringの内部データの一部を別のスタック領域に保存)
    movq    %rax, 32(%rsp)
    movq    16(%rsp), %rax(これも、Stringオブジェクトの別の内部データ(例えば、バイト配列のポインタやキャパシティ)が別のスタック領域にコピー)
    movq    %rax, 40(%rsp)
    movq    24(%rsp), %rax(Stringオブジェクトのもう一つの内部データが別のスタック領域にコピー)
    movq    %rax, 48(%rsp)

    ;; drop(s2);
    leaq    32(%rsp), %rdi
    callq    core::ptr::drop_in_place<alloc::string::String>(資源解放)

    addq    $56, %rsp
    retq

となる。movq の連打で、Stringの内部データ 3 つ(たぶん ptr, len, capacity)をスタックの別の場所(つまり s2)にコピー。

そして、s2 に対応するスタックポインタを第1引数レジスタ %rdi にコピーして drop_in_place を呼び出して資源解放。s1 に対応するスタックポインタには何もしておらず、Rust コンパイラは、s1 が最早有効ではないと考えている。(なのでメモリの二重解放エラーは起きない。コンパイラが面倒を見ているので)

よって、

println!("{} {}", s1, s2); // NG. error[E0382]: borrow of moved value: `s1`

は NG。「s1s2ムーブされた」と表現される。勿論複製すれば OK。

let s1 = String::from("hello");
let s2 = s1.clone();

println!("{} {}", s1, s2);

Rustでは、 自動的にデータの"deep copy"が行われることは絶対にない。複製時の動きをアセンブラで見ると以下。
clone メソッドの呼び出しを見かけたら、何らかの任意のコードが実行され、その実行コストは高い

playground::main:
    subq    $72, %rsp

    ;; let s = String::from("hello");
    leaq    .L__unnamed_12(%rip), %rsi(文字列リテラルのアドレスを計算)
    leaq    8(%rsp), %rdi
    movq    %rdi, (%rsp)
    movl    $5, %edx
    callq    <alloc::string::String as core::convert::From<&str>>::from

    ;; let s_clone = s.clone();
    movq    (%rsp), %rsi(スタックに格納されたStringのアドレスを%rsiに読み込み)
    movq    <alloc::string::String as core::clone::Clone>::clone@GOTPCREL(%rip), %rax(Stringのcloneメソッドへのアドレスを%raxにセット)
    leaq    32(%rsp), %rdi%rdi = %rsp + 32; クローンされたStringの保存先)
    callq    *%rax(先ほど%raxに格納されたclone関数を呼び出し)
    jmp    .LBB31_3

.LBB31_3:
    leaq    32(%rsp), %rdi
    callq    core::ptr::drop_in_place<alloc::string::String>(資源解放)
    jmp    .LBB31_4

Copyの型の一部

  • あらゆる整数型。u32など。
  • 論理値型であるbooltruefalseという値がある。
  • あらゆる浮動小数点型、f64など。
  • 文字型であるchar
  • タプル。ただ、Copyの型だけを含む場合。例えば、(i32, i32)Copyだが、 (i32, String)は違う。

所有権と関数

上記までを踏まえると:

fn takes_ownership(some_string: String) {}
fn makes_copy(some_integer: i32) {}

fn main() {
    let s = String::from("hello");

    takes_ownership(s); // 所有権の関数の引数へのムーブ
    //println!("{}", s); // error[E0382]: borrow of moved value: `s`

    let x = 5;

    makes_copy(x); // Copy トレイトにより関数の引数にコピー
    println!("{}", x); // 参照OK
}

戻り値とスコープ

以下のように関数が戻り値を返さない時は関数スコープを抜ける時に drop して String のリソースを解放。

fn gives_ownership() {
    let some_string = String::from("hello");
}

fn main() {
    gives_ownership();
}
playground::gives_ownership:
    subq    $24, %rsp
    movq    %rsp, %rdi
    leaq    .L__unnamed_12(%rip), %rsi
    movl    $5, %edx
    callq    <alloc::string::String as core::convert::From<&str>>::from
    movq    %rsp, %rdi
    callq    core::ptr::drop_in_place<alloc::string::String>
    addq    $24, %rsp(資源解放)
    retq

playground::main:
    pushq    %rax
    callq    playground::gives_ownership
    popq    %rax
    retq

一方、関数が戻り値を返す時は戻り値にムーブするので、関数スコープを抜ける時に drop しない。

fn gives_ownership() -> String {
    let some_string = String::from("hello");
    some_string
}

fn main() {
    let s1 = gives_ownership();
}

gives_ownership から main に返って、main のスコープを抜ける時に drop する。

playground::gives_ownership:
    pushq    %rax
    movq    %rdi, %rax
    movq    %rax, (%rsp)
    leaq    .L__unnamed_12(%rip), %rsi
    movl    $5, %edx
    callq    <alloc::string::String as core::convert::From<&str>>::from
    movq    (%rsp), %rax
    popq    %rcx
    retq

playground::main:
    subq    $24, %rsp
    movq    %rsp, %rdi
    callq    playground::gives_ownership
    movq    %rsp, %rdi
    callq    core::ptr::drop_in_place<alloc::string::String>(資源解放)
    addq    $24, %rsp
    retq
derwindderwind

4.2. 参照と借用

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();
    (s, length)
}

だと、所有権を関数呼び出しもとに返せるが大袈裟すぎる。

playground::calculate_length:
    subq    $88, %rsp(スタックポインタ%rsp88バイト分減らし、スタック領域を確保)
    movq    %rsi, 8(%rsp)(第2引数%rsiの値をスタックの%rsp + 8バイトの位置に保存)
    movq    %rdi, %rax(第1引数%rdiの値(Stringオブジェクトのポインタ)を%raxレジスタにコピー)
    movq    8(%rsp), %rdi(スタックに保存された%rsiの値を%rdiにロード)
    movq    %rax, 16(%rsp)(%rdiから%raxにコピーした値をスタックの%rsp + 16バイトの位置に保存)
    movq    %rax, 24(%rsp)(%raxを再びスタックの%rsp + 24バイトの位置にも保存)
    callq    alloc::string::String::len(Stringの長さを計算して、結果を%raxに格納)
    movq    %rax, 32(%rsp)(文字列の長さが格納された%raxをスタックの%rsp + 32バイトの位置に保存)
    jmp    .LBB33_3

.LBB33_3:
    movq    24(%rsp), %rax(スタックの%rsp + 24バイトの位置に保存されていた値を%raxにロード。Stringのポインタ)
    movq    16(%rsp), %rcx
    movq    32(%rsp), %rdx
    movq    8(%rsp), %rsi
    movq    %rdx, 80(%rsp)
    movq    (%rsi), %rdirsi(文字列のポインタ)が指すメモリアドレスにある値をrdiに)
    movq    %rdi, 40(%rsp)
    movq    8(%rsi), %rdi
    movq    %rdi, 48(%rsp)
    movq    16(%rsi), %rsi
    movq    %rsi, 56(%rsp)
    movq    40(%rsp), %rsi
    movq    %rsi, (%rcx)
    movq    48(%rsp), %rsi
    movq    %rsi, 8(%rcx)
    movq    56(%rsp), %rsi
    movq    %rsi, 16(%rcx)(rsiの値をrcx + 16バイトの位置に格納)
    movq    %rdx, 24(%rcx)(文字列の長さ(%rdx)をrcx + 24バイトの位置に格納)
    addq    $88, %rsp
    retq
    movq    core::panicking::panic_in_cleanup@GOTPCREL(%rip), %rax
    callq    *%rax

参照を使うと:

fn calculate_length(s: &String) -> usize {
    s.len()
}

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    // '{}'の長さは、{}です
    println!("The length of '{}' is {}.", s1, len);
}
playground::calculate_length:
    pushq    %rax%raxの値をスタックにプッシュ)
    movq    %rdi, (%rsp)(関数の第1引数として%rdiに渡されたStringのポインタを、スタックポインタ%rspが指す位置に保存)
    callq    alloc::string::String::len(Stringの長さが計算され、結果は%raxに格納)
    popq    %rcx(pushq %raxで保存した値をスタックからポップして%rcxにロードして取り出す形)
    retq

playground::main:
    ;; let s1 = String::from("hello");
    subq    $264, %rsp
    leaq    .L__unnamed_14(%rip), %rsi(リテラル文字列.L__unnamed_14 ("hello") のアドレスを%rsiにロード)
    leaq    16(%rsp), %rdi%rsp + 16の位置にあるメモリアドレスを%rdiにロード)
    movq    %rdi, (%rsp)(Stringオブジェクトのポインタをスタックに保存)
    movl    $5, %edx"hello" の長さ5%edxにセット)
    callq    <alloc::string::String as core::convert::From<&str>>::from(&strからString型に変換する関数呼び出し。結果としてStringオブジェクトが%rdiに作成)

    ;; let len = calculate_length(&s1);
    movq    (%rsp), %rdi(スタックの先頭に保存されていたStringオブジェクトのポインタを%rdiにロード)
    callq    playground::calculate_length
    movq    %rax, 8(%rsp)

    jmp    .LBB34_3

所有してないということは、指している値は、参照がスコープを抜けてもドロップされないということです。

と書いているように、上記では drop が実行されていないが、以下の非参照版では drop が呼ばれる。

参照を使わないと:

fn calculate_length(s: String) -> usize {
    s.len()
}

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(s1);

    // '{}'の長さは、{}です
    println!("The length is {}.", len);
}
playground::calculate_length:
    subq    $40, %rsp
    movq    %rdi, 8(%rsp)
    callq    alloc::string::String::len
    movq    %rax, 16(%rsp)(String::lenの結果として返された長さ(%rax)をスタック上の%rsp + 16の位置に保存)
    jmp    .LBB31_3(参照版とは異なり drop_in_place に繋げている)

.LBB31_1:
    movq    8(%rsp), %rdi
    callq    core::ptr::drop_in_place<alloc::string::String>
    jmp    .LBB31_5
    movq    %rax, %rcx
    movl    %edx, %eax
    movq    %rcx, 24(%rsp)
    movl    %eax, 32(%rsp)
    jmp    .LBB31_1

参照外し: *(第15章)

関数の引数に参照を取ることを借用と呼びます。

変数が標準で不変なのと全く同様に、参照も不変なのです。

&mut にすれば変更可能な参照になる。

注意

特定のスコープで、ある特定のデータに対しては、 一つしか可変な参照を持てない

不変な参照なら同一スコープ内で複数持てるが、一方

不変な参照をしている間は、可変な参照をすることはできない

「コンパイラがコンパイル時にデータ競合を防ぐ」ために制約をもうけているらしい。

宙に浮いた参照 (Dangling References)

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

コンパイルエラーで阻止

drop する変数の参照を返せない。以下のようにローカル変数の所有権を呼び出し元にムーブすれば良い。

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

参照の規則

  • 任意のタイミングで、一つの可変参照か不変な参照いくつでものどちらかを行える。
  • 参照は常に有効でなければならない。
derwindderwind

4.3. スライス型

お題: 文字列の最初の単語をかえす関数: fn first_word(s: &String) -> ? の「?」の型は?

  • スライス: 所有権のない別のデータ型
  • スライスにより、コレクション全体ではなく、 その内の一連の要素を参照
    • Module std::slice
      • スライスはメモリの塊へのポインタと長さで表現されるビュー

文字列を受け取って、その文字列中の最初の単語を返す関数 first_word

  • やっていることは最初の空白のインデックスを返して間接的に最初の単語(の長さ)を返している。
  • が、文字列本体とは切り離した値なので、文字列を変更した場合に変化しない。
  • 文字列を変更した時にバグの温床になる。→ “文字列の一部分” に対応する参照を返したい。→ 参照の形にすると、その参照を所有している変数がある状態での文字列の変更は、これをコンパイラが検出してエラーにできる。

文字列スライス

fn type_of<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}

fn main() {
    let s = String::from("hello world");

    let hello = &s[0..5]; // or &s[..5];
    let world = &s[6..11]; // or &s[6..];
    let whole_s = &s[..];

    println!("The types of s, hello, world and whole_s are {}, {}, {}, {}.",
        type_of(&s), type_of(&hello), type_of(&world), type_of(&whole_s));
}

The types of s, hello, world and whole_s are alloc::string::String, &str, &str, &str.

Python 的には

s = "hello world"
hello = s[0:5]
world = s[6:]
whole_s = s[:]

Note
Perplexity:

Rustにおいて、&strと&Stringは異なる型であり、&strは&alloc::string::Stringのエイリアスではありません。しかし、&Stringから&strへの型強制(coercion)が可能です。これらの違いと関係について説明します。
&strと&Stringの違い
&str (文字列スライス)

  • 固定長のUTF-8エンコードされた文字列への参照
  • 不変(イミュータブル)
  • スタックまたはヒープ上のデータを参照可能

&String

  • ヒープ上に確保された可変長文字列(String)への参照
  • Stringは内部的にVec<u8>として実装されている

冒頭に戻ると、「fn first_word(s: &String) -> ? の「?」の型は?」の答えは &str i.e.,

  • 文字列スライスを意味する型は、(上記でも見た) &str で、本来実装したかった first_word は以下:
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..] // 実は &s や s でもコンパイルできる。c.f. オマケ
}

fn main() {
    let s = "Hello world";

    println!("The first word of s is {}", first_word(&String::from(s)));
}

The first word of s is Hello

文字列スライスを所有している変数がある時に、文字列を変更できない:

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // error! (エラー!)

文字列リテラルはスライス

fn type_of<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}

fn main() {
    let s = "Hello, world!";
    println!("The type of s is {}", type_of(&s));
}

The type of s is &str

文字列リテラルは、実行形式のバイナリに埋め込まれるので、実行形式のバイナリのその特定の位置を指すスライス(という解釈らしい)。実行形式のバイナリがプログラムの実行時に書き換わることはないので、文字列リテラルが不変である理由にもなる。ということで &str は不変な参照となる。

引数としての文字列スライス

シグネチャを変更すると、同じ関数を&String値と&str値両方に使えるようになる:

fn first_word(s: &str) -> &str {
    ...
}

ところで、オマケで触れているように StringDeref トレイトを実装しているお陰で、暗黙のうちに &str への変換ができるため、first_word(&my_string[..]) のようなことをしなくても、first_word(&my_string) で行けるっぽい。

他のスライス

fn type_of<T>(_: &T) -> &'static str {
    std::any::type_name::<T>()
}

fn main() {
    let a = [1, 2, 3, 4, 5];

    let slice = &a[1..3];

    println!("The type of a is {} and that of slice is {}", type_of(&a), type_of(&slice));
}

The type of a is [i32; 5] and that of slice is &[i32]

まとめ

所有権、借用、スライスの概念は、Rustプログラムにおいて、コンパイル時にメモリ安全性を保証します。

オマケ

fn first_word(s: &String) -> &str {
    ...

    s
}

でもコンパイルが通ったので ChatGPT に訊いた。StringDeref トレイトを実装しているかららしい:

Rust において、&s[..] と &s と s のいずれもコンパイルが通る理由は、それぞれが等価な表現として> 扱われるためです。以下でそれぞれのケースを詳しく説明します。

  1. &s[..]
    [..] はスライスシンタックスで、s 全体をスライスとして取得することを意味します。
    &s[..] は s の全体を参照するスライス型 &str を返します。
    String 型のインスタンスは、メモリ上にバイト列として保存されており、[..] で全体をスライスして得た &str は、このバイト列のスライスです。
  2. &s
    String 型のインスタンスに対して、参照(&s)を取ると、String は Deref トレイトを実装しているため、自動的に &str に型が変換されます。
    &s は &String ですが、&String は実際には &str に自動変換されるため、実行上は &s[..] と同じ意味を持ちます。
  3. s
    関数が &String を受け取る場合、s 自体もまた Deref によって &str に自動的に変換されるため、参照としてそのまま &str として扱われます。
    この動作はRustの**Deref coercion(デレフ強制)**と呼ばれる機能です。String は Deref トレイトを持つため、&String から &str への変換が自動で行われます。
    結論
    &s[..] は明示的にスライスを取っているが、&s や s でも同様の結果となるのは、Rustが Deref トレイトを使用して String から &str への変換を自動的に行うためです。
derwindderwind

5. 構造体を使用して関係のあるデータを構造化する

5.1. 構造体を定義し、インスタンス化する

  • 構造体は第3章で議論したタプルと似ている。
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

使い方:

let mut user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("anotheremail@example.com");

参考

Python の namedtuple が雰囲気的に「構造体はタプルと似ている」という感覚と近い気がする。

>>> from collections import namedtuple
>>> User = namedtuple('User', ['username', 'email', 'sign_in_count', 'active'])
>>> user = User('someusername123', 'someone@example.com', 1, True)
>>> user
User(username='someusername123', email='someone@example.com', sign_in_count=1, active=True)

フィールドと変数が同名の時にフィールド初期化省略記法を使う

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

は煩わしく、以下のように名前を揃えると省略できる。

fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

参考

TypeScript/JavaScript と似た感覚。c.f. 【TypeScript/JavaScript】オブジェクト省略記法を理解しよう【初心者向け】

const name: string = "Taro";
const age: number = 21;

const myProfile: { name: string; age: number } = {
    name: name,
    age: age
};

は以下のように書ける。

... // 同上
const myProfile: { name: string; age: number } = {
    name,
    age
};

異なる型を生成する名前付きフィールドのないタプル構造体を使用する

  • フィールドに紐づけられた名前がなく、むしろフィールドの型だけのタプル構造体と呼ばれる
fn main() {
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);
    
    let color = Color(200, 190, 32);
    let origin = Point(3, 4, 5);
    let any_tuple = ("Go", 9, 6, "Sun");
    // let white = Color(255, "255", 255); // error[E0308]: mismatched types
    
    println!("{} {} {}", color.0, origin.1, any_tuple.3)
}

200 4 Sun

フィールドのないユニット様(よう)構造体

※ 英語では “Unit-Like Structs”

  • 一切フィールドのない構造体
  • ユニット () に似ているのでこういう名前
  • ユニット様構造体は、ある型にトレイトを実装するけれども、 型自体に保持させるデータは一切ない場面に有効になる(らしい)

日本語版では抜けているが、英語版を翻訳すると以下のような内容がタプルの解説の箇所に存在する:

値を持たないタプルには、unitという特別な名前があります。この値と対応する型はどちらも () と書かれ、空の値または空の戻り値を表します。式は、他の値を返さない場合、暗黙的にユニット値を返します。

オマケ

最後のコードを手直しすると・・・

struct User<'a> {
    username: &'a str,
    email: &'a str,
    sign_in_count: u64,
    active: bool,
}

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
    
    println!("User: {}, active: {}", user1.username, user1.active);
}

User: someusername123, active: true