Rust プログラミング 入門

Rust完全ガイド 第3回 データ構造入門

構造体(Structs)

Rustでは、データを整理して扱いやすくするために 「構造体(Struct)」 という仕組みが用意されています。
構造体は、異なる型の値をひとまとめにし、一つの単位として管理できるデータ構造です。

たとえば、ユーザー情報や座標データなどを一つの構造体としてまとめる ことで、データの扱いが容易になります。
また、構造体はメソッドを持つこともでき、オブジェクト指向プログラミングのような使い方も可能です。

この章では、Rustの 構造体の定義方法や使い方、タプル構造体・ユニット構造体、メソッドの定義 について学んでいきます。

構造体の定義と利用方法

Rustの構造体は struct キーワード を使って定義します。
以下の例では、「ユーザー情報を表す構造体」 を定義し、それを使ってデータを格納する方法を示します。

struct User {
    name: String,
    age: u32,
    email: String,
}

fn main() {
    let user1 = User {
        name: String::from("Alice"),
        age: 25,
        email: String::from("alice@example.com"),
    };

    println!("名前: {}", user1.name);
    println!("年齢: {}", user1.age);
    println!("メール: {}", user1.email);
}

構造体の基本構造

  1. struct を使って構造体を定義する。
  2. フィールド(name, age, email など)にデータ型を指定する。
  3. 構造体のインスタンス(user1)を作成し、フィールドに値を設定する。
  4. user1.name のように、ドット記法でフィールドにアクセスできる。

構造体の特徴

  • 各フィールドは異なる型を持つことができる。
  • String のような所有権を持つ型は、String::from() で値をセットする必要がある。
  • デフォルトでは、構造体のフィールドはイミュータブル(変更不可) なので、変更する場合は mut を指定する必要がある。
fn main() {
    let mut user2 = User {
        name: String::from("Bob"),
        age: 30,
        email: String::from("bob@example.com"),
    };

    user2.age = 31; // フィールドの変更
    println!("変更後の年齢: {}", user2.age);
}

タプル構造体とユニット構造体

Rustには、通常の構造体 (struct) 以外にも タプル構造体(Tuple Struct)とユニット構造体(Unit Struct) があります。

タプル構造体(Tuple Struct)

タプル構造体は、通常のタプルと似ていますが、型に名前をつけて扱うことができます。

struct Color(i32, i32, i32); // RGBカラーを表す構造体
struct Point(f64, f64); // 座標を表す構造体

fn main() {
    let red = Color(255, 0, 0);
    let point = Point(3.0, 4.0);

    println!("Red: ({}, {}, {})", red.0, red.1, red.2);
    println!("Point: ({}, {})", point.0, point.1);
}

タプル構造体の特徴

  • フィールドに名前がないため、red.0 のようにインデックスでアクセスする。
  • 通常の構造体よりも軽量で、シンプルなデータのグループ化に適している。
  • 座標 (Point) や RGBカラー (Color) など、単純なデータのまとまりに使われることが多い。

ユニット構造体(Unit Struct)

ユニット構造体は、フィールドを持たない構造体 で、主に トレイト(Trait)と組み合わせて使う ことが多いです。

struct UnitStruct;

impl UnitStruct {
    fn say_hello() {
        println!("Hello from UnitStruct!");
    }
}

fn main() {
    UnitStruct::say_hello();
}

ユニット構造体の特徴

  • フィールドを持たないため、単なる型のシグナルとして利用される。
  • トレイトの実装(impl)に使われることが多い。
  • 空のデータを表す際に便利。

メソッドの定義

Rustでは、構造体に メソッド(関数) を定義することができます。
メソッドを定義するには、impl(Implementation)ブロック を使います。

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 10, height: 20 };

    println!("面積: {}平方ピクセル", rect1.area());
    println!("rect1はrect2を含むか? {}", rect1.can_hold(&rect2));
}

メソッドの特徴

  • impl ブロックの中で fn を使い、メソッドを定義する。
  • &self を指定すると、インスタンスのフィールドにアクセスできる。
  • can_hold のように、他の構造体を引数に取ることも可能。

関連関数(コンストラクタ)

Rustでは 関連関数(Associated Function) を使い、構造体のインスタンスを生成するメソッド(コンストラクタ) を作ることができます。

impl Rectangle {
    fn new(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    let rect = Rectangle::new(40, 60);
    println!("新しい長方形: {}x{}", rect.width, rect.height);
}
  • new() 関数は Rectangle のインスタンスを作成する。
  • Rectangle::new(40, 60) のように、構造体名を使って呼び出す。

まとめ

Rustの構造体は、データを整理し、プログラムを分かりやすくするための強力な機能 です。
特に、通常の構造体・タプル構造体・ユニット構造体の違い を理解して使い分けることが重要です。

  • 通常の構造体 (struct) は、複数のフィールドを持つ一般的なデータ構造
  • タプル構造体 (tuple struct) は、シンプルなデータのグループ化に適している
  • ユニット構造体 (unit struct) は、フィールドを持たず、主にトレイトの実装で使われる

また、メソッドを定義することで、構造体をオブジェクト指向のように扱うことも可能 です。
引き続き、Rustのデータ構造について詳しく学んでいきましょう。

列挙型(Enums)

Rustでは、「列挙型(Enum)」 を使うことで、複数の異なるデータの選択肢を一つの型としてまとめることができます。
列挙型は、プログラムの可読性を向上させ、明確なデータ構造を持つコードを記述するのに役立ちます。

また、Rustでは Option<T>Result<T, E> などの エラーハンドリング にも列挙型が活用されており、安全なプログラム設計を可能にする重要な要素 となっています。

この章では、列挙型の定義方法と活用方法、Option<T>Result<T, E> の使い方、match を用いたパターンマッチング について学んでいきます。

列挙型とは?

Rustの列挙型は enum キーワード を使って定義します。
列挙型は、関連する値をまとめるために使用されるデータ構造 です。

たとえば、信号機の状態を列挙型として表すことができます。

enum TrafficLight {
    Red,
    Yellow,
    Green,
}

fn main() {
    let light = TrafficLight::Red;
    match light {
        TrafficLight::Red => println!("止まれ"),
        TrafficLight::Yellow => println!("注意"),
        TrafficLight::Green => println!("進め"),
    }
}

列挙型の特徴

  1. enum を使って定義する。
  2. 各バリアント(Red, Yellow, Green など)は :: を使ってアクセスする。
  3. match を使うと、列挙型の値に応じた処理を簡単に記述できる。
  4. 列挙型は、異なる種類のデータを1つの型として扱うのに適している。

列挙型にデータを持たせる

列挙型の各バリアントは、データを保持することも可能 です。
例えば、IPアドレスの種類とその値を持つ IpAddr 型を定義できます。

enum IpAddr {
    V4(String),
    V6(String),
}

fn main() {
    let home = IpAddr::V4(String::from("127.0.0.1"));
    let loopback = IpAddr::V6(String::from("::1"));

    println!("IPv4: {:?}", home);
    println!("IPv6: {:?}", loopback);
}

このように、各バリアントにデータを持たせることで、異なる情報を一つの型にまとめることができます。

Option<T>Result<T, E>の活用

Rustでは null を使わずに安全に値の有無を表現するために Option<T> を、エラーハンドリングのために Result<T, E> を提供しています。
どちらも 列挙型として定義されており、Rustの安全性を支える重要な仕組み です。

Option<T> の活用

Option<T> は、値があるか(Some(T))、ないか(None)を表す列挙型 です。
例えば、整数値を返す関数が値を持たない可能性がある場合、Option<T> を使います。

fn divide(a: i32, b: i32) -> Option {
    if b == 0 {
        None // 0での割り算はできないので None を返す
    } else {
        Some(a / b)
    }
}

fn main() {
    let result = divide(10, 2);
    match result {
        Some(value) => println!("結果: {}", value),
        None => println!("エラー: 0で割ることはできません"),
    }
}

Result<T, E> の活用

Result<T, E> は、正常な結果(Ok(T))とエラー(Err(E))を表す列挙型 です。
ファイルの読み込みなど、成功・失敗の可能性がある処理 に利用されます。

use std::fs::File;

fn main() {
    let file = File::open("hello.txt");

    match file {
        Ok(_) => println!("ファイルを開きました"),
        Err(e) => println!("ファイルを開けませんでした: {}", e),
    }
}
  • Ok(T) は成功した場合の値を持つ。
  • Err(E) はエラーの情報を保持する。
  • エラーを適切に処理できるため、プログラムの安全性が向上する。

matchによるパターンマッチング

Rustの match を使うことで、列挙型の値に応じた処理を簡潔に記述できます。
また、Option<T>Result<T, E> の処理にもよく使われます。

基本的な match の使い方

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn coin_value(coin: Coin) -> u32 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

fn main() {
    let value = coin_value(Coin::Quarter);
    println!("コインの価値: {} セント", value);
}
  • match は、列挙型のすべてのパターンを網羅する必要がある(未処理のパターンがあるとコンパイルエラー)。
  • Coin::Quarter の場合は 25 が返される。

Option<T>match で処理

Option<T>match を使って処理すると、値の有無に応じた動作を記述できます。

fn get_length(text: Option<&str>) -> usize {
    match text {
        Some(s) => s.len(),
        None => 0, // None の場合は 0 を返す
    }
}

fn main() {
    let word = Some("Rust");
    let empty: Option<&str> = None;

    println!("文字数: {}", get_length(word)); // 4
    println!("空の文字数: {}", get_length(empty)); // 0
}
  • Some(s) なら文字数を返す。
  • None なら 0 を返す。

まとめ

Rustの列挙型は、データの状態を明示的に表現し、安全なプログラム設計を可能にする重要な機能です。

  • 列挙型(enum)を使うことで、関連する値を一つの型にまとめることができる。
  • Option<T>null の代わりに値の有無を表現し、安全性を向上させる。
  • Result<T, E> はエラーハンドリングに利用され、プログラムの堅牢性を向上させる。
  • match を活用することで、列挙型の値ごとに適切な処理を分岐できる。

引き続き、Rustのデータ構造について学び、より実用的なプログラムを作成できるようにしていきましょう。

ベクタ (Vec<T>) と文字列 (String)

Rustでは、データを効率的に扱うために 「ベクタ(Vec<T>)」と「文字列(String)」 という2つのデータ構造が提供されています。
ベクタは 可変長のリスト であり、配列のように使うことができますが、サイズを動的に変更できる という特徴があります。
一方、文字列は String 型と &str(文字列スライス) という2種類があり、それぞれ用途に応じて使い分ける必要があります。

この章では、Vec<T> の基本操作、String&str の違い、文字列の操作方法 について学んでいきます。

Vec<T> の基本操作(追加・削除・イテレーション)

Vec<T> とは?

Vec<T>可変長のリスト を表すデータ構造で、動的にサイズを変更できます。
固定長の配列 [T; N] とは異なり、要素の追加・削除が可能です。

ベクタの作成と要素の追加

Vec<T>Vec::new() または vec![] を使って作成 できます。

fn main() {
    let mut numbers: Vec = Vec::new(); // 空のベクタを作成
    numbers.push(10);
    numbers.push(20);
    numbers.push(30);

    println!("{:?}", numbers); // [10, 20, 30]
}
  • Vec::new() は空のベクタを作成する。
  • push() を使うと、要素を追加できる。

要素の取得

ベクタの要素を取得する方法は2つあります。

fn main() {
    let numbers = vec![10, 20, 30];

    let first = numbers[0]; // インデックスで取得
    println!("最初の要素: {}", first);

    match numbers.get(2) {
        Some(value) => println!("3番目の要素: {}", value),
        None => println!("要素が存在しません"),
    }
}
  • numbers[0] のようにインデックスでアクセス可能 だが、範囲外アクセスでパニックする。
  • get() を使うと、安全に要素を取得でき、Option<T> を返す。

要素の削除

pop() を使うと 最後の要素を削除 できます。
remove(index) を使うと 指定したインデックスの要素を削除 できます。

fn main() {
    let mut numbers = vec![10, 20, 30, 40];

    numbers.pop(); // 最後の要素 (40) を削除
    println!("{:?}", numbers); // [10, 20, 30]

    numbers.remove(1); // インデックス1の要素 (20) を削除
    println!("{:?}", numbers); // [10, 30]
}

ベクタのイテレーション(繰り返し処理)

for ループを使って、ベクタの各要素を繰り返し処理できます。

fn main() {
    let numbers = vec![10, 20, 30];

    for num in &numbers {
        println!("要素: {}", num);
    }
}

String型と&strの違い

Rustの文字列には String 型と &str(文字列スライス)の2種類 があります。
どちらを使うべきかは、用途によって異なります。

説明主な用途
String可変な文字列(ヒープに格納)動的な文字列操作(結合・変更)
&str不変の文字列スライス(スタックや文字列の一部を指す)文字列の一部を借用する場合

String の作成

StringString::from() または .to_string() で作成できます。

fn main() {
    let s1 = String::from("Hello");
    let s2 = "Rust".to_string();

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

&str の特徴

  • 文字列リテラル("Hello")は &str 型。
  • String の一部を &str として参照可能。
fn main() {
    let s = String::from("Hello Rust");
    let part: &str = &s[0..5]; // "Hello" の部分スライス
    println!("{}", part);
}

文字列操作(結合・スライス・反復)

文字列の結合

Rustでは + 演算子や format!() を使って文字列を結合できます。

fn main() {
    let s1 = String::from("Hello, ");
    let s2 = String::from("Rust!");
    let result = s1 + &s2; // `s1` の所有権が移動する
    println!("{}", result);
}
  • + を使う場合、右側の &str を借用する必要がある。
  • s1 の所有権が result に移動するため、s1 は以降使用できない。

所有権を移動せずに結合したい場合は format!() を使うのが便利です。

fn main() {
    let s1 = String::from("Hello");
    let s2 = String::from("Rust");
    let result = format!("{} {}", s1, s2); // すべての変数がそのまま使える
    println!("{}", result);
}

文字列のスライス

&str を使うことで、文字列の一部を参照できます。
ただし、UTF-8の境界を超えたスライスを取得しようとするとエラー になるため注意が必要です。

fn main() {
    let s = String::from("こんにちは");
    let slice = &s[0..3]; // エラー! UTF-8の境界を超えている
    println!("{}", slice);
}
  • Rustでは、UTF-8の文字の境界を考慮しないスライスはコンパイルエラーになる。
  • 正しく文字単位でスライスしたい場合は chars() を使うのが安全。
fn main() {
    let s = "こんにちは";
    let slice: String = s.chars().take(2).collect(); // 最初の2文字を取得
    println!("{}", slice); // "こん"
}

文字列の反復

chars() を使うと、文字列を1文字ずつ処理できます。

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

    for c in s.chars() {
        println!("{}", c);
    }
}

まとめ

  • Vec<T> は可変長のリストであり、要素の追加・削除・イテレーションが可能。
  • String は可変な文字列、&str は不変な文字列スライス。
  • 文字列の結合には +format!() を使い、スライスではUTF-8の境界に注意が必要。

これで 第3回「データ構造入門」 は終了です。
次回の 第4回では「エラー処理と型」 について学びます。
Rustの強力なエラー処理機構や型システムを理解し、安全で堅牢なプログラムを作成するための知識を深めていきましょう!

  • この記事を書いた人

ふくまる

機械設計業をしていたが25歳でエンジニアになると決意して行動開始→ 26歳でエンジニアに転職→ 28歳でフリーランスエンジニアに→ 現在、34歳でフリーランス7年目 Go案件を受注中 Go,GCPが得意分野

-Rust, プログラミング, 入門