【4日目】Rust入門者に贈るRust入門者がRustと闘う物語 -構造体-


最近、巷で話題のRust。システムプログラミング言語などの低レイヤー言語は触ったことがない筆者が、裸一貫で闘っていく、そんな物語です。

前回の記事

運用しているサイトがバズってしまいそちらの保守でだいぶやられていました。
前回からだいぶ時間経ってしまいましたが、今日から毎日投稿できるよう頑張ります。

本日は構造体をやっていきたいと思います。URL

構造体は他言語でも頻出の概念なのでいけると信じてダイレクトに読み進めていきたいと思います。

はじめに

※物語共通で使用するので不要な方は飛ばしてください。

開発環境や参考サイト、学習フローチャートなどの概要について話します。

本物語について

私は、今業務で使用している言語や趣味でしようしている言語を含め、使用している全ての言語を独学で勉強してきました(せざるを得なったためですが)。

そして、毎回のことのように何度も壁にぶつかり、やっとの思いで扱えるようになるため、プログラミング言語の取得は、修行のようなものだと考えています。

これは私自身の能力不足なのかもしれませんが、大小はあるにしても誰しもが共通して感じるものなのかなと思いまして、後にRustに入門する方々に参考になればと思い、物語として記録しておきます。

本物語の目的

・初心者としてありがちな凡ミスやくだらないエラーを赤裸々に記録し、後続する方々の参考にして頂くため
・自分用のメモ

本物語での記述について

ファイル名:わかりやすく日本語で書いていますが、自らの環境で真似するのはお勧めできません。

表現の置換:私の最も得意として楽に記述できる言語がJavaScript(特にES6の書き方が好きです)のため、多くの部分でJavaScriptで置換して表現します。多くの人がわかるようにPythonで置換しようと思ったのですが、Pythonの書き方は(個人的に)独特なので、これまた知名度の高いJavaScriptで置換しようと考えました。

エラーやつまづきポイント:敢えて全てのエラーの記録をお伝えしますし、遠回りの手法をお伝えすることがあります。これは私を反面教師にして同じ轍を進まなくて済むためと考えています。

※なお、全体を共通して「厳密性」より「なんとなく」を優先することが多々あるかと思います。その点ご了承ください。

対象読者

・今からRustに入門しようとしている方
・プログラミング自体はある程度できるけど、組み込み系やOS設計などの低レイヤーの部分は触れたことがないという方
・開発自体は好きだが、言語がある程度使えるまでの道のりがしんどい方

Why Rust

・業務でWebAssemblyが必要になりそうだった
・何か1つの言語を1から丁寧に学んでみたかった
・流行っていて今後本格的に低レイヤー言語としての地位を獲得しそうな雰囲気がした
・調べると、難しこと以外でデメリットが見当たらなかった
・名前が良い響き

開発環境とスキルセット

再現性は不明ですが、念のため記しておきます。

開発環境

PC: macOS Catalina 10.15.2
開発エディタ: VSCode
Rust: 1.41.0

スキルセット

基本的にWebフロントエンドとソフトウェア開発(金融系など)がメインですが、、

業務で使用
Javascript(デフォルト)(Node.js, Vue.js, React.js, Jquery, etc)
Python(主にAPIなどのバックエンド)(あまりフレームワークは使わない)
Swift(単純な業務アプリ等)
PHP(Wordpressカスタマイズとか頼まれたら)(比較的苦手)

趣味程度
Golang(速度が必要な処理でAPI作成)
Julia(計量経済学を学習した際に)
R(計量経済学を学習した際に)

主にWebのお仕事をもらいつつ、頼まれたら業務に必要なアプリ作ったりしています。
Swiftが一応システムプログラミング言語ですが、そこまで複雑じゃない業務管理アプリ程度しか作ったことがないので、システムプログラミングエンジニアとしては初心者です。
Golangは、Node.jsで速度処理に困った際に比較で学んだのがきっかけで業務での使用はないです。いつかお仕事できればいいな。

ちなみに、よく聞かれるので、JavaとRubyの使用経験についても記載しておきます。
・Java(Kotlin等も)は無茶振りで超単純なアプリを作ったくらいで、趣味にも入らないくらいです。
・Rubyは、変なセミナーに誘われることが多かったので怖くて触れてません。

学習スケジュール(予定)

そもそも私自身Rustを学ぶ目的というか必要がありまして、実は金融系ソフトウェアの開発にあたりWebAssemblyを使うかもしれない可能性があり、そのため最終的にWebAssembly系で何かできたらと思っています。

恥ずかしながら、こうしてスケジュールを組んで学習というのが初でして、いまいち書き方がわかりませんが、以下のようにしております。

基礎学習

n日目 やること
1日目 Rustと周辺ツールのインストールとセットアップ
2日目 3. 普遍的なプログラミング概念
3日目 4. 所有権を理解する
4日目 5. 構造体を使用して関連のあるデータを構造化する
5日目 6. Enumとパターンマッチング
6日目 7. モジュール
7日目 8. 一般的なコレクション
8日目 9. エラー処理
9日目 10. ジェネリック型、トレイト、ライフタイム
10日目 11. テスト
11日目 13. 関数型言語の機能: イテレータとクロージャ
12日目 14. CargoとCrates.ioについてより詳しく
13日目 15. スマートポインタ
14日目 16. 恐れるな!並行性
15日目 17. Rustのオブジェクト指向プログラミング機能
16日目 18. パターンは値の構造に合致する
17日目 19. 高度な機能

こちらを参考にして、スケジュールを組んでいます。

基本的にセクション毎を学んでいきます。量などにムラがあると思いますが、どうにかやり切る覚悟でいきましょう。

こちらはプロジェクト学習で最終的な復習をする前提で、一旦込み入ったことなどはスルーするかもしれないです。個人的に、80%くらいの精度で学習して、最後に実践的にボロボロになって、20%の総和を回収するのがなんだかんだ効率的だと思いますし、何より前に進んでいる実感からモチベが湧くので(ただ筋というかコアの部分は理解する必要があると思いますが)。

プロジェクト学習

n日目 やること
1つ目 2. 数当てゲームをプログラムする
2つ目 12. 入出力プロジェクト: コマンドラインプログラムを構築する
3つ目 20. 最後のプロジェクト: マルチスレッドのWebサーバを構築する

これらは1日でやり切れたらいいですが、文量なども多いので頑張れたらとしましょう。また、日数ではなくて、わからない点や疑問点をこのプロジェクトを通してゼロにしたいと考えているため、1つに1週間くらいかかることも想定されます。

付録

一旦スルーします。必要に応じて見ます。

応用学習

未定ですが、WebAssemblyで何かしたい。

参考サイト等(仮)

Rustチュートリアルがしっかりしてるサイト

何をやったか(4日目)

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

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

構造体とは、以下のようなものらしいです。

src/main.rs
struct User {
    username: String,
    email: String,
    sign_in_count: u64,
    active: bool,
}

どうやらJSONのような感じで定義するようですね。JavaScriptでいうTypeScriptの型定義と似ています。Swiftでも結構似た表現が出てくるかと思います。

使い方

src/main.rs
let mut user1 = User {
    email: String::from("[email protected]"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};

user1.email = String::from("[email protected]");

こんな感じで使うみたいです。そのまんまですね。

省略記法について

パターン1:フィールド名(keyやname of the fieldとも呼ばれる)と変数名が同一の時

以下のように記述できるようです。key(email, username, active, sign_in_count)が同じものは繰り返さなくていいというわけですね。

src/main.rs
fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}

パターン2:「..」で他インスタンスからコピーする

以下のようにコピーができます。

src/main.rs
let user2 = User {
    email: String::from("[email protected]"),
    username: String::from("anotherusername567"),
    ..user1
};

便利ですね。

注意点

・構造体は、可変にする場合インスタンス全体が可変となる。

・構造体は全データの所有権を持たないといけない(全データの所有権を持つか参照を使う場合は後に学ぶライフタイムという性質を利用するらしいです)。

5.2. 構造体を使ったプログラム例

シンプルな関数 VS タプル VS 構造体

シンプルな関数

こちらは説明いらないですかね。

src/main.rs
fn main() {
    let width1 = 30;
    let height1 = 50;

    println!(
        // 四角形の面積は、{}平方ピクセルです
        "The area of the rectangle is {} square pixels.",
        area(width1, height1)
    );
}

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

タプル

タプルを使用することで引数が一つだけで済みスマートな記述ができる。

src/main.rs
fn main() {
    let rect1 = (30, 50);

    println!(
        "The area of the rectangle is {} square pixels.",
        area(rect1)
    );
}

fn area(dimensions: (u32, u32)) -> u32 {
    dimensions.0 * dimensions.1
}

構造体

構造体を使用することでより簡潔に意味づけができるようになる。

注意点は、所有権があるので、参照を渡しているところくらい(この例ならmoveしても問題はなさそうですが)。

src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!(
        "The area of the rectangle is {} square pixels.",
        area(&rect1)
    );
}

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

トレイトの継承で有用な機能を追加する

構造体のインスタンスの中身を標準出力したい際に使う。

エラー例

src/main.rs
struct Rectangle {
    width: u32,
    height: u32,
}

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

    // rect1は{}です
    println!("rect1 is {}", rect1);
}

成功例

トレイトの継承(derive)(具体的には、#[derive(Debug)])により、出力が可能に。

src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

    println!("rect1 is {:?}", rect1); //rawな感じで出力される
    println!("rect1 is {:#?}", rect1); //改行され綺麗に出力できる
}

5.3. メソッド記法

定義した構造体だけが使えるオリジナル関数のようなもの。割と他の言語でもあるので、コードを見た方が早いかなと思います。僕の中ではimplでstructを拡張しているイメージです。

src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

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

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

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

&selfとは?

&selfがareaのシグニチャの中にありますが、意味は以下のようです。

areaのシグニチャでは、rectangle: &Rectangleの代わりに&selfを使用しています。 というのも、コンパイラは、このメソッドがimpl Rectangleという文脈内に存在するために、 selfの型がRectangleであると把握しているからです。&Rectangleと同様に、 selfの直前に&を使用していることに注意してください。メソッドは、selfの所有権を奪ったり、 ここでしているように不変でselfを借用したり、可変でselfを借用したりできるのです。 他の引数と全く同じですね。

つまり、暗にrectangle: &Rectangleを使用しているから&selfでいいよね的なノリですね。なお、&はなくてもあってもいいオプションのようです。

implの存在意味

わざわざimplを使う必要があるかどうかについても記載があります。

関数の代替としてメソッドを使う主な利点は、メソッド記法を使用して全メソッドのシグニチャでselfの型を繰り返す必要がなくなる以外だと、 体系化です。コードの将来的な利用者にRectangleの機能を提供しているライブラリ内の各所でその機能を探させるのではなく、 この型のインスタンスでできることを一つのimplブロックにまとめあげています。

つまり可読性と保守性のためですね。

引数を渡したいとき

src/main.rs
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
    }
}

こうすればいいらしいです。なお、複数の引数が渡せるかも実験してみます。

引数が複数の時
src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    // rect1にrect2ははまり込む?
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2, 3));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3, 199));
}
impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

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

結果は成功しました。普通のメソッドと同じ感じで扱えるのは便利ですね。試してはないですがタプル、別構造体でもうまく行くでしょう。

関連関数

&selfがシグニチャにない場合を考えます。以下のようなimplがあるとしています。

src/main.rs
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}

定義できるようですが、注意点として呼び出し方法が変わるようです。

これまで通り(エラーとなる)

let sq = Rectangle.square(3);は失敗します。

「::」記法を使う(成功する)

let sq = Rectangle::square(3);は成功します。

つまり、「.」は暗黙で自分自身を引数に渡しているようですね。

ざっくり要約

型が限定的なJSON

よくわからなかったこと、問題点など

なぜインスタンスに所有権を持たせる必要があるのか(もう少し勉強すれば知れるみたい)

気をつけた方が良いこと

特になし。

まとめ

今回の章は、準備感がすごい感じでした。覚えるだけなので、特に障害はないかと思います。

あまりにも手応えがないので、次章もやってしまいます。

次の記事

こちらです。