fake_coder2's tech blog

偽物のほうが本物よりも本物だ

プログラミングほぼ初心者のためのRust入門

Rust

はじめに

Rustってなんだか難しいなー。と思いながら勉強したときの備忘録です。

要旨

本記事ではRustでプログラミングを始めるうえで知っておくべき基本的な言語機能を紹介します。
機能についての説明は対象読者を想定したレベルに留めます。そのため正確ではない部分もあるかと思いますが、記事の内容が冗長にならないようにするため婉曲な表現は極力避けます。
かく云う私自信がRustを勉強中なので誤りがあれば指摘してもらえると喜びます。

対象読者

本記事の対象読者は下記に当てはまる方を想定しています。

Rustの基本的な言語機能

Rustに関して最低限知っておきたい機能を取り上げる。

  • 基本型(スカラー型)
    • 数値型
    • 文字列型
    • 論理値型
  • 複合型
    • 配列型
    • タプル型
  • ユーザ定義の型
    • 構造体
    • 列挙型
  • 標準ライブラリの型
    • Option
    • Result
    • Vec
    • Box
  • 関数
    • 通常の関数
    • 関連関数
    • メソッド
  • 制御構文
  • トレイト
  • マクロ

基本型(スカラー型)

数値型

  • 符号付き整数

    // i8 範囲:-127~127
    let figure_i8: i8 = 8;
    // i16 範囲:-32768~32767
    let figure_i16: i16 = 16;
    
    // 他にもi32, i64, i128, isizeなどがある
    


  • 符号無し整数

    // u8 範囲:0~255
    let figure_u8: u8 = 8;
    // u16 範囲:0~65535
    let figure_u16: u16 = 16;
    
    // 他にもu32, u64, u128, usizeなどがある
    


  • 浮動小数点数

    // f32 範囲:-3.4028235e38~3.4028235e38
    let figure_f32: f32 = 32.3232;
    // f64 範囲:-1.7976931348623157e308~1.7976931348623157e308
    let figure_u16: u16 = 64.6464;
    


文字列型

  • char
    正確には文字列型ではなく文字型である。

    // Unicodeの1文字を扱う
    let moji: char = 'a';
    println!("{}", moji);
    


  • &str
    Rustの最も基本的な文字列型である。固定長の文字列を定義できる。

    // String(UTF-8文字列)のスライスを扱う
    let mojiretsu_str: &str = "hello";
    println!("{}", mojiretsu_str);
    


  • String
    正確には標準ライブラリで定義されている文字列型であるが、話を簡単にするために基本型として紹介する。可変長の文字列を定義できる

    // UTF-8文字列を扱う
    let mojiretsu_string: String = String::from("hello");
    println!("{}", mojiretsu_string);
    


論理値型

真偽値を表現するための型。

let this_is_false: bool = false;
let this_is_true: bool = true;
println!("{}", this_is_false);
println!("{}", this_is_true);

複合型

配列型

同一の型の複数の値を収めることができる型。サイズ(要素数)は固定。

// i32型の要素数(サイズ)が5の配列
let hairetsu: [i32; 5] = [3, 6, 8, 10, 1];
println!("要素の1つ目: {}", hairetsu[0]);
println!("要素の2つ目: {}", hairetsu[1]);

タプル型

異なる型の複数の値を収めることができる型(※言い回しが微妙)。内部に格納された型の全てを含めて1つの型を構成するため、タプル内の一部の型を後から変更することはできない。

let tuple = ["apple", 1, 'b'];
println!("要素の1つ目: {}", tuple[0]);
println!("要素の2つ目: {}", tuple[1]);

ユーザ定義の型

構造体

意味のあるグループを形成する複数の関連した値をまとめ、名前付けできる独自の型。
構造体の定義はstructキーワードを用いる。定義した構造体を使用するにはインスタンスを生成する。

fn main() {

    // 構造体Personを定義
    struct Person {
        name: String,
        age: u32
    }

    // 構造体Personのインスタンスp
    let p = Person {
        name: String::from("John"),
        age: 8
    };

    println!("名前:{}", p.name);
    println!("年齢:{}", p.age);

}

列挙型

"(見かけ上は)独自"の複数の型をまとめて管理するための型。列挙型の中で定義した型は要素型あるいは列挙子はたまたヴァリアントと呼ばれる。列挙型はいくつかの異なる要素型の中から1つを選ぶような場合に使用する。文章での表現はやや理解し難いかもれしれないため、ぜひ例を見てほしい。

fn main() {

    // 列挙型を定義
    enum IpAddressType {
        IPv4,
        IPv6
    }

    // インスタンス生成
    let mut ip = IpAddressType::IPv4;

    match ip {
        IpAddressType::IPv4 => println!("IPv4"),
        IpAddressType::IPv6 => println!("IPv6"),
    }

    // インスタンスを上書き
    ip = IpAddressType::IPv6;

    match ip {
        IpAddressType::IPv4 => println!("IPv4"),
        IpAddressType::IPv6 => println!("IPv6"),
    }

}

標準ライブラリの型

Option

データが存在する場合と存在しない場合を表現できる型(列挙型であり、列挙型を具体化した型とも表現できる)。

fn main() {
    
    // 変数valueを定義 値はString型の「値あり」という文字列
    let value = Option::Some(String::from("値あり"));

    match value {
        Option::Some(value) => println!("{}", value),
        Option::None => println!("値なし")
    }

    // 変数valueを再定義(上書きではない) 値はNone Noneは型を明示しないといけないらしい... 
    let value = Option::<String>::None;

    match value {
        Option::Some(value) => println!("{}", value),
        Option::None => println!("値なし")
    }

    // Noneの場合はvalueをprintln!できない

}

Result

処理の結果が成功か失敗かを表現できる型(列挙型であり、列挙型を具体化した型とも表現できる)。※以下に一応の例を記したもののあまり効果的な例ではない。Resultは基本は関数の戻り値として用いるべきである。

fn main() {

    let result = Result::<i32, i32>::Ok(200);

    match result {
        Ok(result) => println!("SuccessCode: {}", result),
        Err(result) => println!("ErrorCode: {}", result)
    }

    let result = Result::<i32, i32>::Err(404);

    match result {
        Ok(result) => println!("SuccessCode: {}", result),
        Err(result) => println!("ErrorCode: {}", result)
    }

}

Vec

同一の型の複数の値を収めることができる型。サイズ(要素数)は可変。配列の可変バージョンである。

fn main() {
    // Vec型の配列vector_hairetsuを定義
    let mut vector_hairetsu = vec![1, 3, 5, 7, 9];
    println!("{:?}", vector_hairetsu);

    // 末尾に要素を追加
    vector_hairetsu.push(11);
    println!("{:?}", vector_hairetsu);

    // 末尾の要素を削除
    vector_hairetsu.pop();
    println!("{:?}", vector_hairetsu);
}

Box

Rustは、通常、値をメモリのスタック領域に確保するが、Boxを使うとヒープ領域に確保する。Boxはヒープ領域に任意の型を格納し、スタック領域にヒープ領域へのポインタを置く。
Boxはコンパイル時にサイズが不明な型を扱う場合などに有用である。例として、要素数(サイズ)が不明な配列型などがある(ここでいう「要素数が不明な配列型」はVecではない。詳しくは例をご覧あれ)。

// 準備中

変数

letとlet mut

変数の定義にはletキーワードを用いる。通常、Rustの変数は後からの変更は不可能だが、mutキーワードを用いることで変更を可能にすることができる。前者をイミュータブルな変数、後者をミュータブルな変数と呼ぶ。
また、変数の型は明示的に指定が無ければコンパイラが型を推測する。動的型付け兼静的型付け言語である。

fn main() {

    // 変更ができない変数(イミュータブルな変数)
    let hensuu1 = 5;
    println!("hensuu1:{}", hensuu1);

    // エラーになる
    //hensuu1 = 10;
    //println!("hensuu1:{}", hensuu1);

    // 変更ができる変数(ミュータブルな変数)
    let mut hensuu2 = 5;
    println!("hensuu2:{}", hensuu2);

    hensuu2 = 10;
    println!("hensuu2:{}", hensuu2);

}

constとstatic

constとstaticは定数を定義するためのキーワードである。constは変更不可でstaticは変更可能にすることができる。

関数

初めに通常の関数、関連関数、メソッドの違いを整理する。

これらの関数とメソッドは簡易的にはどちらも関数という説明で済むが、とりわけ構造体が関係しない関数を「通常の関数」と呼び、構造体に紐付けて定義する関数で引数にselfを取らないものを「関連関数」、引数にselfを取るものを「メソッド」と呼ぶ。
関連関数/メソッドは、呼び出しの際に構造体とセットで扱うという点でも通常の関数と区別できる。

関数およびメソッドはfnキーワードを用いて表現し、構造体に関連関数/メソッドを定義するにはimplキーワードを用いる。

通常の関数

通常の関数は構造体が関係しない(構造体の外で定義する)関数である。

fn main() {

    hello();
    add(1, 2);
    println!("{}", mul(3, 5));
}

// 引数なし関数、戻り値の型不定
fn hello() {
    println!("Hello");
    return;
}

// 引数あり関数、戻り値の型不定
fn add(a: i32, b: i32) {
    println!("{}", a+b);
    return;
}

// 引数あり関数、戻り値の型指定
fn mul(c: i32, d: i32) -> i32 {
    return c * d;
}

関連関数

構造体に紐付けて定義する関数で引数にselfを取らないものである。

fn main() {

    // 構造体Personを定義
    struct Person {
        name: String,
        age: u32,
    }

    // 構造体Personに関連関数hobbyとcalculate_annual_incomeを定義
    impl Person {
        // 引数なし、戻り値の型は不定
        fn hobby() {
            println!("趣味:野球");
        }

        // 引数はu32型のargument、戻り値の型はu32
        fn calculate_annual_income(argument: u32) -> u32 {
            return argument * 12;
        }
    }

    // 関連関数はインスタンスが無くても使える
    // 構造体Personのインスタンスp
    // let p = Person {
    //     name: String::from("John"),
    //     age: 30,
    // };

    Person::hobby();
    println!("年収{}万円", Person::calculate_annual_income(100));

}

メソッド

構造体に紐付けて定義する関数で引数にselfを取るものである。

fn main() {

    // 構造体Personを定義
    struct Person {
        name: String,
        age: u32,
    }

    // 構造体Personにメソッドhobbyとcalculate_annual_incomeを定義
    impl Person {
        // 引数は&self(インスタンスの初期値)のみ,戻り値の型は不定
        fn hobby(&self) {
            println!("{}の趣味:野球", self.name);
        }

        // 引数は&self(インスタンスの初期値)とu32型のargument2、戻り値の型はu32
        fn calculate_annual_income(&self, argument2: u32) -> u32 {
            return argument2 * 12;
        }
    }

    // 構造体Personのインスタンスp
    let p = Person {
        name: String::from("John"),
        age: 30
    };

    p.hobby();
    println!("Johnは{}歳で年収{}万円", p.age, p.calculate_annual_income(100));

}

制御構文

if

多くのプログラミング言語のif文はブロックであるが、Rustにおけるif文は式である。式であるため変数に束縛(格納)することや関数の引数にすることができる。

fn main() {

    let value = 1;

    if value < 1 {
        println!("valueは1より小さいです。");
    } else if value > 1 {
        println!("valueは1より大きいです。");
    } else {
        println!("valueは1です。");
    }
    
}

式として使う場合

fn main() {

    let value = 1;

    // 条件に一致したら任意の値をresultに代入する
    let result = if value < 1 {
                    println!("valueは1より小さいです。");
                    -1000
                } else if value > 1 {
                    println!("valueは1より大きいです。");
                    1000
                } else {
                    println!("valueは1です。");
                    value
                };

    println!("{}", result);

ループ

Rustには、loop、for、whileという3種類のループ文がある。どのループもbreakキーワードを使うことができる。とりわけloopはほぼ必須。

loop{
    処理
    break;
}

while 条件式{
    処理
}

for 任意の変数名 in イテレータ(配列とか){
    処理
}

loop

loopは同じ処理を連続したい場合に便利。ちなみにloopは式。

3回連続で「Hello World!」を出力する例

fn main() {

    let mut count = 0;
    loop {
        println!("Hello World!");
        count += 1;
        if count == 3 {
            break;
        }
    }

}

for

forはイテレータ(配列、タプルとか)を回したいときに便利。

Vector配列をforループする例

fn main() {

    let vector_hairetsu = vec![0, 1, 2, 3, 4];

    for v in vector_hairetsu {
        println!("{}", v);
    }

}

while

whileは特定の条件を満たす間だけ繰り返し処理したい場合に便利。

変数countが10より小さい間、繰り返し処理する例

fn main() {

    let mut count = 0;

    while count < 10 {
        println!("count: {}", count);
        count += 1;
    }
    
}

その他の関連事項

Range

fn main() {

    // 「..」を用いた書き方がRange 1以上10未満をループ
    for number in 1..10 {
        println!("{}", number);
    }
    
}

Iterator

// 準備中

match

fn main() {

    let i: i32 = 2;
    
    match i {
        1 => println!("1"),
        2 => println!("2"),
        3 => println!("3"),
        // その他は「_」キーワードで表現
        _ => println!("その他"),
    }

}

トレイト

関数名とその関数の戻り値の型だけをいくつかまとめて(1つでもいい)定義するもの。通常、トレイトで定義した関数は構造体で利用し具体的な処理内容も構造体で決める。
Javaを学んだことがある人にとってはインターフェースに近いものだ。

fn main() {
    // 構造体Dogのインスタンスを生成
    let dog = Dog{};
    // 構造体Catのインスタンスを生成
    let cat = Cat{};
    show_snimal_data(dog);
    show_snimal_data(cat);
}

trait Animal {
    // 動物の寿命を返すメソッドを定義
    fn lifespan(&self) -> u32;
    // 動物の学術名を返すメソッドを定義
    fn scientific_name(&self) -> String;
}

// 構造体Dogを定義
struct Dog;

// 構造体DogにAnimalトレイトを実装かつlifespan関数とscientific_name関数を定義
impl Animal for Dog {
    fn lifespan(&self) -> u32 {
        return 13;
    }
    fn scientific_name(&self) -> String {
        return "Canis lupus familiaris".to_string();
    }
}

// 構造体catを定義
struct Cat;

// 構造体CatにAnimalトレイトを実装かつlifespan関数とscientific_name関数を定義
impl Animal for Cat {
    fn lifespan(&self) -> u32 {
        return 16;
    }
    fn scientific_name(&self) -> String {
        return "Felis catus".to_string();
    }
}

// 動物の寿命と学術名を標準出力する関数を定義
fn show_snimal_data<T: Animal>(animal: T) {
    println!("Lifespan: {} years", animal.lifespan());
    println!("Scientific name: {}", animal.scientific_name());
}

マクロ

Rustにおける「マクロ」とは、ある機能の集合(まとめて定義したもの)を指す。
また、マクロ=メタプログラミングを行うための道具である。これがどういうことかと言うと、例えば、何かを標準出力に出力したいという場合、マクロを利用しないならいくつかのソースコードを書く必要があるが、println!マクロを利用すればたかだか1行で済む。

マクロと関数の違い

  • 関数は引数の数と型を宣言しなければならないが、マクロは可変長の引数を取ることができる。
  • マクロは、コンパイラがコードの意味を解釈する前に展開される。例えば、 与えられた型にトレイトを実装することができる。これは関数ではできない。何故なら、関数は実行時に呼ばれ、 トレイトはコンパイル時に実装される必要があるからである。
  • マクロはファイル内で呼び出す前に定義したりスコープに導入しなければならないが、 関数はどこにでも定義でき、どこでも呼び出せる。

マクロの分類

マクロは宣言的(declarative)マクロと手続き的(procedural)マクロに大別され、とりわけ手続き的マクロは3種類に分けられる。

宣言的マクロ
手続き的マクロ①deriveマクロ

構造体とenumにderive属性を使ったときに追加されるコードを指定する、カスタムの#[derive]マクロ

手続き的マクロ②属性風マクロ

任意の要素に使えるカスタムの属性を定義する、属性風のマクロ

手続き的マクロ③関数風マクロ

関数のように見えるが、引数として指定されたトークンに対して作用する関数風のマクロ

補足

  • 「型」と表現されるものは総称して「データ型」と呼ばれる。プログラミング言語に共通する一般的な概念である。
  • 関数の定義でセミコロンを付けない行はreturnとして扱われる。
  • 関連関数は構造体の新規インスタンスを返すコンストラクタによく使用される。
  • メタプログラミングとは、ある機能を実現するためのソースコードにラベル付け(あるいは単になんらかの名称に関連付け)し、コーディングの際はそれを用いることである機能を利用することができるというものである。コード量を減らすのに有用である。なお、通常は実行時に本来のソースコードが展開される。
  • 真のプログラミング初心者がRustから入るのは向かないと思う。

参考文献

以上