Rust のデータ型の探索

RustRustBeginner
オンラインで実践に進む

This tutorial is from open-source community. Access the source code

💡 このチュートリアルは英語版からAIによって翻訳されています。原文を確認するには、 ここをクリックしてください

はじめに

データ型へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。

この実験では、Rust におけるデータ型の概念を探ります。すべての値には、それがどのように扱われるかを決定するための特定の型が割り当てられており、複数の型が考えられる場合には、コンパイラに必要な情報を提供するために型アノテーションを追加する必要があります。

これは Guided Lab です。学習と実践を支援するためのステップバイステップの指示を提供します。各ステップを完了し、実践的な経験を積むために、指示に注意深く従ってください。過去のデータによると、この 初級 レベルの実験の完了率は 83%です。学習者から 100% の好評価を得ています。

データ型

Rust におけるすべての値は、特定のデータ型に属しており、それがどの種類のデータであるかを Rust に伝えることで、そのデータとどのようにやりとりするかを知ることができます。ここでは、2 つのデータ型のサブセット:スカラー型と複合型を見ていきます。

Rust は静的型付け言語であることを忘れないでください。これは、コンパイル時にすべての変数の型を知る必要があることを意味します。コンパイラは通常、値とその使い方に基づいて、どの型を使用したいかを推論することができます。たとえば、「推測値と秘密の数字を比較する」でparseを使ってStringを数値型に変換した場合のように、複数の型が考えられる場合には、次のように型アノテーションを追加する必要があります。

let guess: u32 = "42".parse().expect("Not a number!");

前のコードで示した: u32の型アノテーションを追加しない場合、Rust は次のエラーを表示します。これは、コンパイラがどの型を使用したいかを知るために、私たちからさらに情報を必要としていることを意味します。

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^ consider giving `guess` a type

他のデータ型については、異なる型アノテーションが見られます。

スカラー型

スカラー型は単一の値を表します。Rust には 4 つの主なスカラー型があります。整数型、浮動小数点数型、ブール型、文字型。他のプログラミング言語からこれらを知っている人もいるでしょう。Rust におけるこれらの型の使い方を見ていきましょう。

整数型

整数とは、小数部分を持たない数値のことです。第 2 章では、u32型という整数型を使用しました。この型宣言は、関連付けられた値が符号なし整数(符号付き整数型はuではなくiで始まります)であり、32 ビットの空間を占める必要があることを示しています。表 3-1 は、Rust の組み込み整数型を示しています。これらのバリアントのいずれかを使用して、整数値の型を宣言できます。

表 3-1:Rust の整数型

長さ 符号付き 符号なし


8 ビット i8 u8
16 ビット i16 u16
32 ビット i32 u32
64 ビット i64 u64
128 ビット i128 u128
arch isize usize

各バリアントは、符号付きまたは符号なしのいずれかであり、明示的なサイズを持っています。符号付き符号なしは、数値が負になる可能性があるかどうか、つまり、数値に符号が必要かどうか(符号付き)または常に正であり、符号なしで表現できるかどうか(符号なし)を指します。これは、紙に数字を書くようなものです。符号が重要な場合は、数字はプラス記号またはマイナス記号で示されます。ただし、数字が正であると安全に仮定できる場合は、符号なしで表示されます。符号付き数値は、2 の補数表現を使用して格納されます。

各符号付きバリアントは、-(2^(n-1)) から 2^(n-1) - 1(両端を含む)までの数値を格納できます。ここで、nは、そのバリアントが使用するビット数です。したがって、i8は-(2^7) から 2^7 - 1、つまり -128 から 127 までの数値を格納できます。符号なしバリアントは、0 から 2^n - 1 までの数値を格納できます。したがって、u8は 0 から 2^8 - 1、つまり 0 から 255 までの数値を格納できます。

さらに、isize型とusize型は、プログラムが実行されているコンピューターのアーキテクチャに依存します。これは、表で「arch」として示されています。64 ビットアーキテクチャの場合は 64 ビット、32 ビットアーキテクチャの場合は 32 ビットです。

整数リテラルは、表 3-2 に示す形式のいずれかで記述できます。複数の数値型が可能な数値リテラルは、型を指定するための57u8のような型サフィックスを使用できることに注意してください。数値リテラルは、1_000のように、数値を読みやすくするために視覚的な区切り文字として_を使用することもできます。これは、1000を指定した場合と同じ値になります。

表 3-2:Rust の整数リテラル

数値リテラル 例


10 進数 98_222
16 進数 0xff
8 進数 0o77
2 進数 0b1111_0000
バイト(u8のみ) b'A'

では、どの整数型を使用すればよいのでしょうか?わからない場合は、Rust のデフォルトが一般的に良い出発点です。整数型はデフォルトでi32になります。isizeまたはusizeを使用する主な状況は、何らかのコレクションにインデックスを付ける場合です。

整数のオーバーフロー

u8型の変数があり、0 から 255 までの値を保持できるとします。変数をその範囲外の値(256 など)に変更しようとすると、整数のオーバーフローが発生し、2 つの動作のいずれかが発生する可能性があります。デバッグモードでコンパイルする場合、Rust は整数のオーバーフローのチェックを含み、この動作が発生した場合にプログラムが実行時にパニックを起こす原因となります。Rust は、プログラムがエラーで終了する場合にパニックという用語を使用します。「Unrecoverable Errors with panic!」でパニックについて詳しく説明します。

--releaseフラグを使用してリリースモードでコンパイルする場合、Rust はパニックを引き起こす整数のオーバーフローのチェックを含みません。代わりに、オーバーフローが発生した場合、Rust は2 の補数ラップを実行します。簡単に言うと、型が保持できる最大値を超える値は、型が保持できる最小値に「ラップアラウンド」します。u8の場合、値 256 は 0 になり、値 257 は 1 になります。プログラムはパニックを起こしませんが、変数は、おそらく期待していたものではない値を持つことになります。整数のオーバーフローのラップ動作に依存することは、エラーと見なされます。

オーバーフローの可能性を明示的に処理するには、プリミティブ数値型に対して標準ライブラリが提供するこれらのメソッドファミリーを使用できます。

  • wrapping_*メソッド(wrapping_addなど)を使用して、すべてのモードでラップします。
  • checked_*メソッドを使用して、オーバーフローがある場合はNone値を返します。
  • overflowing_*メソッドを使用して、値とオーバーフローがあったかどうかを示すブール値を返します。
  • saturating_*メソッドを使用して、値の最小値または最大値で飽和させます。

浮動小数点数型

Rust には、小数点を持つ数である「浮動小数点数」用の 2 つのプリミティブ型もあります。Rust の浮動小数点数型は、それぞれ 32 ビットのf32と 64 ビットのf64です。デフォルトの型はf64です。なぜなら、現代の CPU では、f32とほぼ同じ速度で動作しますが、より高い精度を持つことができるからです。すべての浮動小数点数型は符号付きです。

data-typesという名前の新しいプロジェクトを作成しましょう。

cargo new data-types
cd data-types

以下は、浮動小数点数を使ったサンプルです。

ファイル名:src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

浮動小数点数は IEEE-754 標準に従って表されます。f32型は単精度浮動小数点数で、f64は倍精度浮動小数点数です。

数値演算

Rust は、すべての数値型に対して期待される基本的な数学演算をサポートしています。加算、減算、乗算、除算、余り。整数の除算は、0 に向かって最も近い整数に切り捨てます。次のコードは、let文で各数値演算をどのように使用するかを示しています。

ファイル名:src/main.rs

fn main() {
    // 加算
    let sum = 5 + 10;

    // 減算
    let difference = 95.5 - 4.3;

    // 乗算
    let product = 4 * 30;

    // 除算
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // 結果は -1 になります

    // 余り
    let remainder = 43 % 5;
}

これらの文の各式は、数学演算子を使用しており、単一の値に評価され、その後変数に束縛されます。付録 B には、Rust が提供するすべての演算子の一覧があります。

ブール型

他の多くのプログラミング言語と同様に、Rust のブール型は 2 つの値のみを持ちます。truefalseです。ブール型は 1 バイトのサイズです。Rust のブール型はboolを使用して指定します。たとえば:

ファイル名:src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // 明示的な型注釈付き
}

ブール値を使用する主な方法は、if式などの条件分岐を通じることです。「制御フロー」では、Rust におけるif式の動作方法について説明します。

文字型

Rust のchar型は、その言語の最も基本的なアルファベット型です。以下は、char値を宣言するいくつかの例です。

ファイル名:src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // 明示的な型注釈付き
    let heart_eyed_cat = '😻';
}

文字列リテラルではダブルクォートを使用するのに対し、charリテラルはシングルクォートで指定することに注意してください。Rust のchar型は 4 バイトのサイズで、Unicode スカラ値を表します。これは、ASCII だけでなくはるかに多くのものを表すことができることを意味します。アクセント付き文字、中国語、日本語、韓国語の文字、絵文字、ゼロ幅スペースはすべて、Rust では有効なchar値です。Unicode スカラ値は、U+0000からU+D7FFおよびU+E000からU+10FFFFの範囲(両端を含む)です。ただし、「文字」は Unicode においては実際には概念ではないため、「文字」が何であるかに関する人間の直感が、Rust におけるcharと一致しない場合があります。「文字列で UTF-8 エンコードされた文字列を格納する」でこのトピックについて詳細に説明します。

複合型

「複合型」は、複数の値を 1 つの型にまとめることができます。Rust には 2 つのプリミティブな複合型があります。タプルと配列です。

タプル型

タプルは、さまざまな型の複数の値を 1 つの複合型にまとめる一般的な方法です。タプルは固定長です。宣言すると、サイズが増減しません。

丸括弧の中にカンマ区切りの値のリストを書くことでタプルを作成します。タプルの各位置には型があり、タプル内の異なる値の型は必ずしも同じでなくてもよいです。この例ではオプショナルな型注釈を追加しています。

ファイル名:src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

変数tupは、タプル全体に束縛されます。なぜなら、タプルは単一の複合要素と見なされるからです。タプルから個々の値を取り出すには、パターンマッチングを使ってタプル値を分解することができます。例えば:

ファイル名:src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

このプログラムはまずタプルを作成して変数tupに束縛します。その後、letを使ったパターンを使ってtupを取り、3 つの別々の変数xyzに変換します。これは「分解」と呼ばれ、単一のタプルを 3 つの部分に分割するためです。最後に、プログラムはyの値を出力します。それは6.4です。

また、アクセスしたい値のインデックスに続けてピリオド(.)を使うことで、直接タプル要素にアクセスすることもできます。例えば:

ファイル名:src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

このプログラムはタプルxを作成し、その後、それぞれのインデックスを使ってタプルの各要素にアクセスします。ほとんどのプログラミング言語と同様に、タプルの最初のインデックスは 0 です。

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

配列型

複数の値のコレクションを持つ別の方法は、「配列」を使うことです。タプルとは異なり、配列の各要素は必ず同じ型でなければなりません。他の一部の言語の配列とは異なり、Rust の配列は固定長です。

配列の値は、角括弧の中にカンマ区切りのリストとして書きます。

ファイル名:src/main.rs

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

データをスタックに割り当てたい場合や、常に固定数の要素を持つことを保証したい場合、配列は便利です。ただし、配列はベクター型ほど柔軟ではありません。ベクターは、標準ライブラリによって提供される似たようなコレクション型で、サイズの増減が可能です。配列とベクターのどちらを使うか迷った場合は、おそらくベクターを使うべきです。第 8 章ではベクターについて詳しく説明します。

ただし、要素数が変更される必要がないことがわかっている場合、配列の方が便利です。たとえば、プログラムで月の名前を使う場合、12 個の要素が常に含まれることがわかっているため、おそらく配列を使う方が適切でしょう。

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

配列の型は、各要素の型、セミコロン、そして配列の要素数を角括弧で囲んで書きます。例えば:

let a: [i32; 5] = [1, 2, 3, 4, 5];

ここで、i32は各要素の型です。セミコロンの後の数字5は、配列が 5 つの要素を含むことを示しています。

また、各要素に同じ値を含む配列を初期化することもできます。方法は、初期値を指定してからセミコロン、そして角括弧で囲んだ配列の長さを指定することです。例えば:

let a = [3; 5];

aと名付けられた配列は、最初にすべての要素が値3に設定された5つの要素を含みます。これはlet a = [3, 3, 3, 3, 3];と書くのと同じですが、もっと簡潔な書き方です。

配列要素のアクセス

配列は、スタックに割り当てることができる既知の固定サイズの単一のメモリチャンクです。インデックスを使って配列の要素にアクセスすることができます。例えば:

ファイル名:src/main.rs

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

    let first = a[0];
    let second = a[1];
}

この例では、firstと名付けられた変数は値1を取得します。なぜなら、それは配列のインデックス[0]にある値だからです。secondと名付けられた変数は、配列のインデックス[1]から値2を取得します。

無効な配列要素のアクセス

配列の末尾を超えた要素にアクセスしようとするとどうなるか見てみましょう。第 2 章の当て推測ゲームに似たコードを実行して、ユーザーから配列インデックスを取得するとします。

ファイル名:src/main.rs

use std::io;

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

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
     .read_line(&mut index)
     .expect("Failed to read line");

    let index: usize = index
     .trim()
     .parse()
     .expect("Index entered was not a number");

    let element = a[index];

    println!(
        "The value of the element at index {index} is: {element}"
    );
}

このコードは正常にコンパイルされます。cargo runを使ってこのコードを実行し、0123、または4を入力すると、プログラムは配列のそのインデックスに対応する値を表示します。代わりに配列の末尾を超えた数値を入力すると、たとえば10を入力すると、次のような出力が表示されます。

thread 'main' panicked at 'index out of bounds: the len is 5 but the index is
10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

このプログラムは、インデックス操作で無効な値を使用した時点で「実行時」エラーを起こしました。プログラムはエラーメッセージとともに終了し、最後のprintln!文を実行しませんでした。インデックスを使って要素にアクセスしようとするとき、Rust は指定したインデックスが配列の長さ未満であることをチェックします。インデックスが長さ以上の場合、Rust はパニックになります。このチェックは実行時に行わなければなりません。特にこの場合、コンパイラは後でコードを実行するときにユーザーが何を入力するかを予測することはできません。

これは、Rust のメモリセーフティ原則が機能している例です。多くの低レベル言語では、この種のチェックは行われず、間違ったインデックスを指定すると、無効なメモリにアクセスできてしまいます。Rust は、メモリアクセスを許可せずに続行せずに即座に終了することで、この種のエラーから保護します。第 9 章では、Rust のエラーハンドリングと、パニックにならず、無効なメモリアクセスを許さない、読みやすく安全なコードを書く方法についてさらに説明します。

まとめ

おめでとうございます!あなたはデータ型の実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに多くの実験を行うことができます。