🎃

【基本的な型】プログラミングRust 第3章

2024/03/20に公開

はじめに

本記事はプログラミングRust 第2版を読んで要点をもとに、情報を補完し、まとめた記事です。この書籍はとてもタメになりますがなかなかに難しいので、本記事と合わせて読むと少しは分かりやすいかな...?と思って書いています。諦める人が1人でも減りますように。

出典は本記事の最後に記載しています。
詳しく知りたい方は、書籍を購入して読むことをおすすめします。

固定長数値

整数型

符号なし整数型は以下のように表現します。

    const a: u8 = 1; // 8ビット符号なし整数型
    const b: u16 = 2; // 16ビット符号なし整数型
    const c: u32 = 3; // 32ビット符号なし整数型
    const d: u64 = 4; // 64ビット符号なし整数型
    const e: u128 = 5; // 128ビット符号なし整数型
    const f: usize = 6; // 計算機のワード長

符号付き整数型は以下のように表現します。

    const a: i8 = 1; // 8ビット符号付き整数型
    const b: i16 = 2; // 16ビット符号付き整数型
    const c: i32 = 3; // 32ビット符号付き整数型
    const d: i64 = 4; // 64ビット符号付き整数型
    const e: i128 = 5; // 128ビット符号付き整数型
    const f: isize = 6; // 計算機のワード長

バイト値に対してはu8型を用います。
※golangではbyteを使用しますが、これもuint8のエイリアスです。

また、整数の後ろに数値型をつけることで、型を指定できます。
型を省略した場合は、デフォルトではi32が使用されます。

    let a = 100u8;

数値の間にアンダースコア(_)を入れることもでき、可読性向上に役立ちます。

    let a = 1_000_000;

チェック付き演算

結果をOptionとして返します。
※以下のコードはどちらも正しい(一致する)ものです。

    assert_eq!(10_u8.checked_add(20), Some(30));
    assert_eq!(100_u8.checked_add(200), None);

飽和演算

オーバーフローした場合は、その型の最大(最小)値で返します。

    assert_eq!(100_u8.saturating_mul(3), 255); // 100 * 3 = 300 -> 255

浮動小数点型

少数を表す浮動小数点型は以下の2つで表現できます。

    const a: f32 = 1.1;
    const b: f64 = 2.2;

真偽値型

true, falseで表現できます。

文字

Unicodeの1文字(A,Bなど)を表現するにはchar型を使用します。

タプル

Rustのタプル型は、複数の値を1つの複合型にグループ化するために使用されます。タプルは、異なる型の値を含むことができ、固定された長さを持ちます。

タプルを作成するには、値をカンマで区切って丸括弧で囲みます。

let tuple: (i32, f64, u8) = (500, 6.4, 1);

名前を割り当てることもでき、以下の例ではx,y,zのそれぞれの変数にタプルの値を束縛しています。

    let (x, y, z): (i32, f64, &str) = (1, 2.0, "3");
    println!("x = {}, y = {}, z = {}", x, y, z);

ユニット型

タプルでもう一つよく使われるタプルの型は0要素のタプル () で、ユニット型と呼ばれています。
値を何も返さない関数の戻り値の型はユニット型()となります。

fn main() {
    let empty = do_something();
    receive_unit(empty);
}

// ユニット型の引数を受け取る
fn receive_unit(_: ()) {
    println!("receive_unit");
}

// ユニット型の戻り値
fn do_something() -> () {
    println!("do_something");
}

Resultでよく使われます。

fn do_something() -> Result<(), std::io::Error> {
    Ok(())
}

ポインタ型

参照

まずは、参照をRustの基本的なポインタ型だと考えるとよいだろう。

変数xへの参照は&xと表現でき、Rustでは「xへの参照を借用する」といいます。
そして参照には次の2種類があります。

1. 共有参照

&Tで表現され、変更不能な参照です。ある値に対して一度に複数の共有参照が存在することができるが、その値を変更することはできません。

2. 可変参照

&mut Tで表現され、値を変更可能な参照です。ある値に対して一度に複数存在することができず、可変参照が存在する間は共有参照も存在することができません。

配列・ベクタ・スライス

配列

配列のサイズはコンパイル時に確定し、新しい要素を追加したりサイズを減らしたりすることはできません。

fn main() {
    let arr = [1, 2, 3, 4, 5];
    println!("配列: {:?}", arr);
}

ベクタ

Vec<T>で表現され、ヒープ上に確保される可変長のTの値の列をベクタと呼びます。配列と異なり、サイズを自由に変更することができます。

fn main() {
    let mut vec = Vec::new();
    vec.push(1);
    vec.push(2);
    vec.push(3);
    println!("ベクタ: {:?}", vec); // ベクタ: [1, 2, 3]
}

このようにVec::new()でベクタを作成できますが、あらかじめ要素の数が分かっていればVec::with_capacity(N)を使用します。

fn main() {
    let mut vec = Vec::with_capacity(3);
    vec.push(1);
    vec.push(2);
    vec.push(3);
    println!("ベクタ: {:?}", vec); // ベクタ: [1, 2, 3]
}

スライス

スライスは、配列やベクタの一部を参照するための型です。スライスは、参照と長さを持ち、元の配列やベクタのデータを所有しません。

Rustのスライス(slice)は、ファットポインタ(fat pointer)として表現されます。ファットポインタは、通常のポインタよりも多くの情報を持つポインタです。

スライスのファットポインタは、以下の2つの情報を含んでいます。

  • スライスの開始位置を指すポインタ
  • スライスの長さ(要素数)

通常のポインタは、メモリ上の単一のアドレスを指します。一方、ファットポインタは、メモリ上の開始位置と長さの情報を持っているため、スライスが参照する範囲全体を表現することができます。

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let slice = &arr[1..4];
    println!("スライス: {:?}", slice); // スライス: [2, 3, 4]
}

スライスの型は&[T]または&mut [T]で表現され、&[T]はTの共有スライス、&mut [T]はTの可変スライスと呼ばれます。

// スライスを引数に持つ
fn sum(values: &[i32]) -> i32 {
    values.iter().sum()
}

fn main() {
    let arr = [1, 2, 3, 4, 5];
    let result = sum(&arr[1..4]);
    println!("合計: {}", result); // 合計: 9
}

文字列(Stringと&str)

RustにおけるStringと&strの主な違いは、所有権とメモリ割り当てです。
以下はサンプルコードで、明示的に型を示しています。

またRustは文字列が有効なUTF-8であることを保証します。

fn main() {
    let s1: String = String::from("hello");
    let s2: &str = "world";

    println!("s1: {}, s2: {}", s1, s2); // s1: hello, s2: world

    let s3: String = s1 + " " + s2;
    println!("s3: {}", s3); // s3: hello world
}

String

Stringはヒープに格納され、所有権を持ちます。文字列を変更することができます。

Stringを作るにはいくつかの方法があります。

1. .to_string()

&strStringに変換します。このメソッドは文字列をコピーします。

    let x: String = "Hello, World!".to_string();

2. format!マクロ

format!マクロは、文字列をフォーマットするために使用されます。指定されたフォーマット文字列と引数を使用して、新しいStringを作成します。

    let x: String = format!("Hello, {}", "world!");
    println!("{}", x); // Hello, world!

&str

文字列スライスと呼ばれ、別のものが所有しているテキストへの参照でテキストを借用します。所有権を持たないため変更ができません。

fn main() {
    let s1: String = String::from("hello");
    let s2: &str = "world";

    println!("s1: {}, s2: {}", s1, s2); // s1: hello, s2: world

    let s3: String = s1 + " " + s2;
    println!("s3: {}", s3); // s3: hello world
}

mutで定義すれば変更できるのでは?

このコードでは、s4は最初に文字列スライス"hello"を参照しています。その後、s4に別の文字列スライス"world"を再割り当てしています。これは、s4が参照する文字列スライスを変更しているだけであり、元の文字列データ自体を変更しているわけではありません。

    let mut s4: &str = "hello";
    println!("s4: {}", s4); // s4: hello
    s4 = "world";
    println!("s4: {}", s4); // s4: world

文字列リテラルはどこに格納されるのか?

文字列リテラルは常にデータセグメントに格納されます。

データセグメントは、プログラムの実行可能ファイル内に存在する特別なセクションであり、初期化された静的なデータを格納するために使用されます。データセグメントに格納されるデータは、プログラムの実行中に変更されることはありません。

データセグメントには、以下のようなデータが格納されます:

  • 文字列リテラル
  • グローバル変数の初期値
  • 静的配列の初期値
  • 定数データ

プログラムが実行されると、データセグメントは仮想メモリ内の読み取り専用メモリ領域にマッピングされます。これにより、プログラムはデータセグメント内のデータにアクセスできますが、変更することはできません。

データセグメントに格納されるデータは、コンパイル時に決定され、プログラムのバイナリファイル内に直接埋め込まれます。これにより、データセグメント内のデータへのアクセスは非常に高速になります。

出典

プログラミングRust 第2版
Jim Blandy、Jason Orendorff、Leonora F. S. Tindall 著、中田 秀基 訳
原書: Programming Rust, 2nd Edition
O'Reilly
https://www.oreilly.co.jp/books/9784873119786/

Discussion