【MacでRust入門】Rust日本語版チュートリアルの要点まとめ ~ 第10回 構造体 struct

2022-09-06
Main Image

目次

こんにちは。

最近流行りのRustを初めてみよう!ということでチュートリアルを読み始めました。

内容を忘れないように & チュートリアルがTL;DRな方向けにブログ記事に要点をまとめていこうと思います。

前回はRustのメモリ管理機能である所有権についてまとめました。

今回は第10回です。Rustの「構造体」について要点をまとめます。

Rustの構造体とは

意味のあるグループについて関連した値を同じ構造でまとめたもので、他の言語で言うところのオブジェクトのデータ属性をまとめたものです。

百聞は一見に如かず。以下に構造体の例を載せます。

Rustの構造体の定義方法

struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

特徴をまとめると以下の通りです。

  • struct 名前 {フィールド名: データ型, ...}で記述する
  • 構造体・フィールドの名前によって値の意味が明確にできる
  • タプル同様、異なるデータ型を含めることができる

Rustの構造体を使用する方法

上記のように定義した構造体は以下のように使うことができます。

インスタンスを生成

まず、構造体からインスタンスと呼ばれる変数を生成します。ここではUser構造体からuser1インスタンスを作ってみます。

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

このように構造体の各フィールドに値をセットし、:の先は型ではなく値にします。いわゆるkey-value構造です。構造体で定義した順番通りにkey-valueをセットしなくてもOKです。

また、mutでインスタンスを可変にすることができます。インスタンス全体(全フィールド)が可変になり、特定のフィールドのみ可変にすることはできません。

インスタンスから特定のフィールドの値を取得する

ドット記法で特定のフィールドの値にアクセスできます。

また、可変(mut)なインスタンスに対しては代入によって値の変更もできます。

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

初期化省略記法

渡された引数から構造体を返す関数の例を見てみます。

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

本来、usernameemailは他のフィールド同様にemail:emailのように書かなければならないはずですが、このようにフィールド名と変数名が同名の場合は:による値のセットを省略できます。

構造体更新記法

すでに生成したuser1インスタンスの一部の値を流用してuser2という新しいインスタンスを作ってみます。

let user2 = User {
    email: String::from("another@example.com"),
    username: String::from("anotherusername567"),
    ..user1
};

emailusername以外のフィールドを流用する場合、本来、active: user1.activeのように書くはずですが、上記のように..user1で他のフィールドはuser1と同じ値を使うようにできます。

タプル構造体

フィールド名がなく型だけを定義するタプル構造体というものもあります。

struct Color(i32, i32, i32);
let black = Color(0, 0, 0);

Colorは明らかに色なので、あえてRGBといったフィールド名をつけずに、簡易的に構造体が表現できます。構造体名がついたタプルというイメージです。

インスタンスはタプルと同じように振る舞うので、分配して値を取り出したり、.と添字で値にアクセスできます。

Rustの構造体を使ったプログラム例とメリット

長方形の面積を求めるプログラム例

例えば「長方形の幅と高さを指定するとその面積を求める」というプログラムを書いてみます。

fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        "長方形の面積は {} 平方ピクセルです",
        area(width1, height1)
    );
}

fn area(width: u32, height: u32) -> u32 {
    width * height
}

上記のように幅と高さをそれぞれ変数を作って求めることもできます。しかし、これだと「幅と高さを持つ長方形がある」「幅と高さは必ずセットに使う」という関係性が曖昧ですね。

そこで、構造体を使って書き換える(リファクタリングする)と、以下のようになります。

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

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!(
        "長方形の面積は {} 平方ピクセルです",
        area(&rect1)
    );
}

fn area(rectangle: &Rectangle) -> u32 {
    rectangle.width * rectangle.height
}

構造体を使うメリット

上記のサンプルコードを読むと、次のことが明らかにわかります。

  • Rectangle(長方形)という構造体があり、その属性には幅と高さがある
  • rect1は幅が30、高さが50のRectangleである
  • area(面積)という関数はある長方形を借用して、その幅と高さを掛け算する

コードの記述だけで「幅と高さを持つ長方形」という関係性が明らかになり、また、「長方形の面積を求める関数」という意図をより明確にすることができました。

このように構造体を使うことによって変数の関係性やプログラムの意図が明らかになり、コードがよりわかりやすく・メンテナンスしやすくなります。

構造体の中身をデバッグで出力する

構造体は今まで出てきた変数のようにprintln!()だけでは中身を出力することができません。

構造体の中身を出力してフィールドの値を確認するのに必要な手続きは2つです。

  1. 構造体定義の直前に#[derive(Debug)]と注釈を書く
  2. print文の{}部分(フォーマット文字列)を{:?}とする

▼ 使用例

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 is {:?}", rect1);
}

出力

rect1 is Rectangle { width: 30, height: 50 }

フォーマット文字列を{:#?}とすると以下のようになります。

rect1 is Rectangle {
    width: 30,
    height: 50
}

今まではデフォルトのフォーマット文字列で出力できていましたが、このように出力する対象によって読みやすくするフォーマットが存在します。

また、このようなデバッグ用の情報を出力する機能を使うには、#[derive(Debug)]といったように明示的に宣言する必要があります。

メソッド記法 ~ Rustで構造体のメソッドを実装する方法

上記の長方形の面積を求める計算はRectangle構造体に対して使うため、このような関数は構造体のメソッドにすることもできます。

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

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

    println!(
        "長方形の面積は {} 平方ピクセルです。",
        rect1.area()
    );
}
  • 構造体Rectangleにメソッドを定義するにはimpl (implement,実装) を使う
  • メソッドの定義はimpl 構造体名 {}内に関数fn メソッド名(){}を書く
  • メソッドとなる関数の引数はselfとする。
  • メソッドに引数を渡すときも所有権を奪うため、ムーブしたくない(普通したくないはず)場合、&selfのように不変借用する
  • メソッドを呼び出すには、インスタンスにドット記法でインスタンス名.メソッド名()とする
  • メソッドの第一引数はselfである必要があるが、selfの後に引数を複数追加できる(関数と同様。)

メソッドを使うことでRectangleという構造体が持つ機能を明確にすることができ、またselfが使えるので繰り返し書くコードが簡潔になります。

関連関数の実装方法

String::newのように::で呼ぶ関数が関連関数であることは第2回で触れましたが、implを使うとこれを構造体に実装することができます。

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

この関連関数squareを呼ぶにはRectangle::square(n)のように使います。

上記のような幅・高さが同じ正方形を作るといったように、新規インスタンスを返すコンストラクタによく使われます。

また、このsquareRectangle構造体の中でしか生まれないので、構造体特有の機能の名前空間分けができる(ディレクトリのような階層構造の整理ができる)と言えます。

まとめ

というわけで、今回はRustの構造体とその関連としてメソッド記法、関連関数の実装方法をまとめました。

  • 構造体とフィールド名の定義によってコードの意図を明確にできる
  • メソッド記法で構造体に関数機能を持たせることができる
  • 関連関数によってインスタンスを使わずに名前空間分けができる

次回はRustのEnum(列挙型)とパターンマッチングについてまとめます。

それでは〜

Rust オススメの参考書籍

▼ 入門者向け。Pythonとの比較あり。Kindleでも読める。

手を動かして考えればよくわかる 高効率言語 Rust 書きかた・作りかた

▼ 中級者向け。オライリーならではの詳解。硬派な方向け。

プログラミングRust 第2版

参考リンク

Rustチュートリアル (日本語版 非公式) https://doc.rust-jp.rs/book-ja/

ads【オススメ】未経験からプログラマーへ転職できる【GEEK JOBキャンプ】
▼ Amazonオススメ商品
ディスプレイライト デスクライト BenQ ScreenBar モニター掛け式
スマートLEDフロアライト 間接照明 Alexa/Google Home対応

Author

Penta

都内で働くITエンジニアもどき。好きなものは音楽・健康・貯金・シンプルでミニマルな暮らし。AWSクラウドやデータサイエンスを勉強中。学んだことや体験談をのんびり書いてます。TypeScript / Next.js / React / Python / AWS / インデックス投資 / 高配当株投資 More profile

Location : Tokyo, JPN

Contact : Twitter@penguinchord

Recommended Posts

Copy Right / Penguin Chord, ペンギンコード (penguinchord.com) 2022 / Twitter@penguinchord