2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rustの基本をPythonと比較しつつ学んでみる #3

Posted at

普段Pythonなどをメインにお仕事をしていますが、Rustのごく基本的な文法や挙動などをPythonと比較しつつ学んでいってみます(前も入門書消化したタイミングで少し記事にしたりしていますが復習も兼ねて書いておきます)。

※Rust初心者なため誤解している点などがあるかもしれません。その辺はご容赦ください。

※長くなりそうなので記事を分割しています。本記事は3記事目となります(過去の記事で触れた点はスキップします)。

1記事目:

2記事目:

構造体(struct)

Pythonのクラスに近いものとしてはRustでは構造体(struct)があります。

例えば以下のようなPythonのクラスで考えてみます。

class Cat:

    name: str
    age: int

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def increment_age(self) -> None:
        self.age += 1

Rustで上記のクラスに近いものを作るにはstructキーワードを使って以下のように書きます(まずは属性定義だけしています)。

struct Cat {
    name: String,
    age: i32,
}

メソッドを追加したい場合には以下のような記述が必要になります。

impl クラス名 {
    fn メソッド名(...) -> ...
}

コンストラクタの場合はPythonだと__init__というdunder methodsで定義しますが、これはRustではnewが該当するようです。ただし慣習的なもの(書籍などでは大体newで設定されている)のようで別にnewではなくともコンパイル等は通るようです。Pythonだとコンストラクタでは返却値の型アノテーションはNoneで問題無い形でしたがRustでは対象のクラス(今回の例で言うとCat)を型として指定します。

また、structのインスタンスを作る際には以下のような記述になります。

struct名 {
    フィールド名: フィールドの値,
    ...
}

前記のPythonのコードのコンストラクタで言うとRustでは以下のようになります。

fn main() {
}

struct Cat {
    name: String,
    age: i32,
}

impl Cat {
    fn new(name: &str, age: i32) -> Cat {
        return Cat {
            name: String::from(name),
            age: age,
        }
    }
}

インスタンス化はPythonだとcat: Cat = Cat(name="ミケ", age=5)といったような記述になりますがRustだと以下のような感じになります。

fn main() {
    let cat: Cat = Cat::new("ミケ", 5);
}

属性に関してはドットで繋げる形でアクセスできます(例 : cat.age)。

fn main() {
    let cat: Cat = Cat::new("ミケ", 5);
    println!("{}", cat.age);
}

struct Cat {
    name: String,
    age: i32,
}

impl Cat {
    fn new(name: &str, age: i32) -> Cat {
        return Cat {
            name: String::from(name),
            age: age,
        }
    }
}
5

前記のPythonコードにあるincrement_ageメソッドの追加を考えてみます。Pythonだとメソッドにはselfという引数が必要になりますが、Rustでは&selfという引数を指定する形になります(コンストラクタの場合にはPythonと異なり引数への指定は要らないようです)。

※例外的にCopyトレイトを持っている場合などに&selfではなくselfと指定するケースもあるようですが基本的には&selfとなるようです。

また、メソッド内で属性の更新を行いたい場合には引数部分を& mut selfといったようにします。インスタンス化の際にも変数にmutの指定が必要になります。

fn main() {
    let mut cat: Cat = Cat::new("ミケ", 5);
    cat.increment_age();
    println!("{}", cat.age);
}

struct Cat {
    name: String,
    age: i32,
}

impl Cat {
    fn new(name: &str, age: i32) -> Cat {
        return Cat {
            name: String::from(name),
            age: age,
        }
    }

    fn increment_age(&mut self) {
        self.age += 1;
    }
}
6

トレイトの利用

Rustにはトレイトという機能があります。これはPythonで言うところの抽象クラスによるインターフェイスと似たような挙動をします。つまりメソッド内部の処理の実装は含まないもののメソッドの引数や返却値の構造などを事前に定義しておくことができます。

Pythonだとabc(abstract base class)パッケージのABCabstractmethodを使って以下のように実装することができます。インターフェイス用のクラスを継承したら@abstractmethodのデコレーターが付いているメソッドをオーバーライドする形でメソッド内部の実装を書く必要があります。

from abc import ABC, abstractmethod


class AgeInterface(ABC):
    @abstractmethod
    def increment_age(self) -> None:
        ...


class Cat(AgeInterface):

    name: str
    age: int

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    def increment_age(self) -> None:
        self.age += 1


cat: Cat = Cat(name="ミケ", age=5)

Rustのトレイトではまず定義として以下のような記述を行います。ここではメソッドの中身は記述しません。

trait AgeTrait {
    fn increment_age(&mut self);
}

その後にimpl 対象トレイト名 for 構造体名といった記述を行い、その中にメソッドの実装の中身を書きます。

impl AgeTrait for Cat {
    fn increment_age(&mut self) {
        self.age += 1;
    }
}

コード全体としては以下のようになります。

fn main() {
    let mut cat: Cat = Cat::new("ミケ", 5);
    cat.increment_age();
    println!("{}", cat.age);
}

struct Cat {
    name: String,
    age: i32,
}

trait AgeTrait {
    fn increment_age(&mut self);
}

impl Cat {
    fn new(name: &str, age: i32) -> Cat {
        return Cat {
            name: String::from(name),
            age: age,
        }
    }
}

impl AgeTrait for Cat {
    fn increment_age(&mut self) {
        self.age += 1;
    }
}
6

特定の関数やメソッドの引数や返却の型の指定でこのトレイトを指定することもできます。こうすることによって対象のトレイトを持っている構造体であればなんでも指定できるようになります。例えば以下のような関数を定義することができます(引数の型の部分にimplなどの記述が必要なようです)。

fn increment_age_10(animal: &mut impl AgeTrait) {
    for _ in 0..10 {
        animal.increment_age();
    }
}

呼び出し元では以下のような記述になります。

fn main() {
    let mut cat: Cat = Cat::new("ミケ", 5);
    increment_age_10(&mut cat);
    println!("{}", cat.age);
}
15

Rustとマクロ

今までもRustの記事を書く際にもマクロを使ってきました(末尾に!が付くものですね)。特にprintln!のマクロは多く利用してきました。

少々他の言語ではRustのマクロ風な機能に慣れていなかったので少々とっつきにくいとは感じていますが軽くだけ触れておきます。

今までもマクロを使ってきてとりあえずprintln!マクロのように関数のように使えるということは感じていましたが、まずは関数とマクロの違いについて触れていきます。

関数はお馴染みで引数を受け取って必要な場合は結果の返却値を返します。一方でマクロはRustの構文を受け取ってそしてRustの構文を返却します。また、プログラム実行中に呼ばれるのではなくコンパイル時に展開される(関数のように残らず、マクロの内容が該当の処理の箇所に展開される)ようです。そのため内容によっては関数などよりもコンパイル後のサイズが大きくなるケースもあるようです(マクロによって生成されるRustの構文が多い場合など)。

また、各所の記事を読んだ感じでは基本的にはマクロは必要なければ使わなくて良さそう・・・ただし向いているケースもあるのでそういった際には使っていくと色々便利そう・・・という印象を受けました。

基本的な書き方としては以下のような感じになります。

macro_rules! マクロ名 {
    (引数として指定するRustの構文) => {
        展開する構文内容
    }
}

例えば雑ですが引数に指定された構文を参照して内容を出力するprint_valueというマクロを考えてみます。以下のような記述になります。

macro_rules! print_value {
    ($x: expr) => {
        println!("value is: {}", $x);
    }
}

fn main() {
    let int_value: i32 = 42;
    print_value!(int_value);
}

expr部分は型の指定のようなもので、構文解析に使われるようです。exprは式のリテラルや変数、関数呼び出しなどの表現の構文が該当するようです。他にも識別子のidentとかステートメントのstmtなど様々なものが存在するようです。

前述のコードのマクロではprint_value!(int_value);部分がコンパイル時にマクロ側で定義されている構文が使われてprintln!("value is: {}", int_value);という構文に展開される・・・という具合に動作します。

結果として関数を呼び出すのと同じような感覚で結果が出力されます。

value is: 42

また、関数などは呼び出し元よりも後に書いても動作しますがマクロでは前に書かないと弾かれる?ようです。これは構文展開の都合でしょうか?

例えば以下のようなコードではコンパイルエラーとなります。

fn main() {
    let int_value: i32 = 42;
    print_value!(int_value);
}

macro_rules! print_value {
    ($x: expr) => {
        println!("value is: {}", $x);
    }
}
error: cannot find macro `print_value` in this scope
 --> src\main.rs:3:5
  |
3 |     print_value!(int_value);
  |     ^^^^^^^^^^^
  |
  = help: have you added the `#[macro_use]` on the module/import?

その他本記事では詳しくは触れませんがマクロではPythonのデコレーターのような使い方もできるようです。こちらは属性マクロなどといった呼ばれ方をしている?ようです。

例えば整数などの数値はCopyトレイトを持っているので別の変数をアサインするとコピーが走ってそれぞれの変数の更新などは所有権をあまり意識せずに操作することができるのは前記事などで触れましたが、一方てstructなどの構造体はCopyトレイトを持っておらず記述によってはエラーになります。

エラーになる例
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut point_1: Point = Point { x: 100, y: 200 };
    let mut point_2: Point = point_1;
    point_1.x = 300;
}
error[E0382]: assign to part of moved value: `point_1`
 --> src\main.rs:9:5
  |
7 |     let mut point_1: Point = Point { x: 100, y: 200 };
  |         ----------- move occurs because `point_1` has type `Point`, which does not implement the `Copy` trait
8 |     let mut point_2: Point = point_1;
  |                              ------- value moved here
9 |     point_1.x = 300;
  |     ^^^^^^^^^^^^^^^ value partially assigned here after move

こういったケースに#[derive(Clone, Copy)]という記述をデコレーターのようにstructに追加することでCopyトレイトを持つ形となってエラー無く扱うことができます。

#[derive(Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let mut point_1: Point = Point { x: 100, y: 200 };
    let mut point_2: Point = point_1;
    point_1.x = 300;
    println!("{}", point_1.x);
    println!("{}", point_2.x);
}
300
100

deriveマクロはRustの標準ライブラリのCopyなどのトレイトの実装を自動生成する属性マクロ・・・といった挙動をするようです。Pythonのデコレーターも最初とっつきにくかったですが慣れたら非常に便利だったため、この辺のマクロも将来必要になってきたら深堀りして勉強していこうと思います。

参考文献・参考サイトなど

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?