S-JIS[2024-09-16/2024-09-19] 変更履歴

Rust構造体メモ

Rustの構造体のメモ。


概要

Rustで複数の値をまとめて保持するデータ構造が構造体。

Rustの構造体はフィールド(データ)とメソッド(関数)および関連関数(associated function)を持つ。
(構造体は、継承が出来ないことを除いてJavaのクラスと似ている。RustのメソッドはJavaのインスタンスメソッドに相当し、関連関数はstaticメソッドに相当する)

構造体の名前の命名ルールはCamelCase。


構造体の定義方法および使用方法

構造体のフィールドはstructブロックで定義し、メソッドおよび関連関数はimplブロックで定義する。
(structure・implementsの略だと思う)

〔可視性〕struct 構造体名〔<型引数1, 型引数2, …>〕 {
    〔可視性〕フィールド名1 : 型1,
    〔可視性〕フィールド名2 : 型2,
    …
}
impl〔<型引数宣言1, 型引数宣言2, …>〕 構造体名〔<型引数1, 型引数2, …>〕 {
    〔可視性〕fn メソッドまたは関連関数名(引数名1 : 型1, 引数名2 : 型2, …) -> 戻り型 {
        〜
    }

    〜
}

structとimplが分離しているからと言っても、他人の構造体に対して自分のソースコードでimplすることは出来ない。


なお、トレイトを実装する際もトレイト毎にimplを書く。

トレイトの実装方法impl トレイト名 for 構造体名


可視性を省略した場合は、private相当になる。


メソッドを定義する場合、第1引数はselfにする必要がある。(Pythonみたいな感じ)
selfは自分の構造体のインスタンスを表している。(Javaのthisに相当)

selfの具体的な宣言方法

関連関数(Javaのstaticメソッド相当)を定義する場合は、第1引数をselfにしない。


構造体のインスタンスの生成

構造体のインスタンス生成は以下の構文。(コンストラクター呼び出しに相当)

構造体名 {
    フィールド名1 : 値1,
    フィールド名2 : 値2,
    …
}

ただし、「フィールド名」と「値に指定する変数名」が同一の場合は、変数名のみで指定できる。


この構文で別モジュールにある構造体のインスタンスを生成する場合は、フィールドの可視性がpub(public)である必要がある。
普通はフィールドをpubにはしないため、implブロックnew関数を定義し、その中で構造体のインスタンスを作って返すということがよく行われる。
(newという名前にするのは、Rustの慣例)


フィールドアクセス

メソッド内からフィールドにアクセスするのは以下の構文。

self.フィールド名

(Javaだとフィールドにアクセスする際に「this.」を省略することが出来るが、Rustでは「self.」を省略できない)


フィールドの可視性がpublicであれば、「構造体インスタンス.フィールド名」でアクセスできる。


メソッド呼び出し

メソッドを呼び出す場合、引数のselfに当たる部分を書く必要は無い。

構造体インスタンス.メソッド名(引数, …)

メソッドの中から(自分の構造体の中の)他のメソッドを呼び出すのは、「self.メソッド名(引数, …)」とする。
(自分の構造体のメソッドを呼び出す場合でも、「self.」を省略できない)


構造体名や実装しているトレイト名を明示してメソッドを呼び出すことも出来る。[2024-09-19]
関連関数を呼び出すのと同じ形式)

構造体やトレイト名::メソッド名(構造体インスタンス, 引数, …)

実装した複数のトレイトが同名のメソッドを持っているとき、どれを呼び出すかを明示するためにこの構文を使う。


関連関数呼び出し

構造体の関連関数(Javaのstaticメソッド相当)を呼び出すのは以下の構文。

構造体名::関数名(引数, …)

関連関数の中から他の関連関数を呼び出す際にも「構造体名::関数名(引数, …)」のように構造体名を明示する必要がある。
自分の構造体名の代わりにSelfを使うことも出来る。「Self::関数名(引数, …)」。
(自分の構造体の関連関数を呼び出す場合でも、「構造体名::」や「Self::」を省略できない)


構造体の例

要素 Java相当 Scala相当
定義
pub struct MyStruct {
    field1 : i32,
    field2 : i32,
}
impl MyStruct {
    pub fn new(a1 : i32, a2 : i32) -> MyStruct {
        // インスタンス生成
        MyStruct {
            field1: a1,
            field2: a2,
        } // 末尾にセミコロン「;」を付けない
    }

    pub fn get_field1(&self) -> i32 {
        self.field1
    }
}
public class MyStruct {
    private int field1;
    private int field2;

    // コンストラクター
    public MyStruct(int a1, int a2) {
        this.field1 = a1;
        this.field2 = a2;
    }

    // new関数相当
    public static MyStruct create(int a1, int a2) {
        // インスタンス生成
        return new MyStruct(a1, a2);
    }

    public int getField1() {
        return this.field1;
    }
}
class MyStruct(
  var field1: Int,
  var field2: Int
) {
  def getField1(): Int = {
    this.field1
  }
}
object MyStruct {
  // new関数相当
  def apply(a1: Int, a2: Int): MyStruct = {
     // インスタンス生成
     new MyStruct(a1, a2)
  }
}
使用
let s = MyStruct::new(123, 456);
let f1 = s.get_field1();
final var s = MyStruct.create(123, 456):
final var f1 = s.getField1();
val s = MyStruct(123, 456)
val f1 = s.getField1()

Javaの場合、フィールドもインスタンスメソッドもstaticメソッドもclass内に書く。
Scalaの場合、フィールドとインスタンスメソッドはclassに書き、staticメソッド相当のものはobjectに書く。
Rustの場合、フィールドはstructに書き、メソッドと関連関数(staticメソッド相当)はimplに書く。

なお、Rustではトレイトを実装する際もトレイト毎にimplを書く ので、ひとつのstructに対して複数のimplがあり得る。


Javaに慣れた身からすると、structブロックに対してimplブロックが別にあるのは、structを継承してトレイトを実装した新しいものを作っているように見えてしまう。
Rustでstructとimplが分かれているのは、Javaのような継承を意味しているわけではなく、フィールド定義とメソッド定義を別々のブロックに分けて書いているだけである。
(Scalaでも、staticメソッド相当のものはclassとは別のobjectに書いているわけだし、分けて書いているという意味では同じだ)

Rustでは、トレイトの個数だけimplブロックを書くことになり、ちょっと冗長な気もしたが。
Javaの場合、複数のインターフェースを実装すると、それらのインターフェースで宣言されている全てのメソッドをひとつのclass内に実装することになる。なので、どのメソッドがどのインターフェース由来なのかが分かりにくいことがある。
Rustではトレイト毎にimplブロックを書くので、実装しているメソッドがどのトレイト由来なのかは自明であり、分かりやすい。


タプル構造体

構造体は、フィールド名を指定せず(タプルのように)型だけで定義することも出来る。

〔可視性〕struct 構造体名(型1, 型2, …);

タプル構造体のインスタンス生成は以下の構文。

構造体名(値1, 値2, …)

タプル構造体の場合、フィールドに名前が付いていないので、タプルと同様に数値(インデックス)でフィールドを取得する。

self.インデックス

タプル構造体の例

pub struct MyTuple(i32, i32);
    let t = MyTuple(123, 456);
    println!("{} {}", t.0, t.1);

    let MyTuple(f1, f2) = t;
    println!("{} {}", f1, f2);

self

メソッドは、構造体のインスタンスに対して操作を行う関数と言えるだろう。

メソッドを定義する際は、第1引数で必ずself(操作対象のインスタンス)を受け取るようにする。

第1引数selfの宣言方法は、所有権などの関係で、何種類かある。(関数の引数の定義方法と同様)

宣言方法 説明
self
fn f(self, …)
呼び出し側から自分のインスタンスの所有権を奪う。
メソッドを呼び出した後はインスタンスを継続して使わせたくない、という場合に有効かも?(クローズ処理とか)
&self
fn f(&self, …)
呼び出し側から自分のインスタンスを借用する。(参照を受け取る)
所有権は呼び出し側にそのまま残る。
フィールドは読み取り専用。(不変参照)
この使い方が最も多いのではないかと思う。
mut
fn f(mut self, …)
fn f(&mut self, …)
メソッド内でフィールドの値を変更したい場合はmutを付ける。

フィールドで参照を保持する方法

構造体のフィールドで参照を保持したい場合は、ジェネリクスを使ってライフタイムを指定する必要がある。

struct MyStructRef<'a> {
    ref_field: &'a str,
}

impl<'a> MyStructRef<'a> {
    fn new(s: &'a str) -> MyStructRef<'a> {
        MyStructRef { ref_field: s }
    }

    fn print_field(&self) {
        println!("MyStructRef.feild={}", self.ref_field);
    }
}
    let s = MyStructRef::new("abc");	// "abc"の型は&str
    s.print_field();

ライフタイムは(通常の型引数と同様に)ジェネリクスの< >の中で定義する。
ライフタイムの命名ルールとして、先頭にアポストロフィー「'」を付ける必要がある。

そして、参照する型に「& 'ライフタイム」という形でライフタイムを指定する。


フィールドでクロージャーを保持する方法

普通は、関数で受け取るクロージャーimplのクロージャーだが、implのクロージャーはそのままでは構造体のフィールドで保持できない。


フィールドでdynクロージャーを保持する例

dynのクロージャー参照を保持することで一応作れるが、ライフタイムの問題がある。

struct MyStructDyn<'a> {
    closure_field: &'a dyn Fn(i32) -> i32,
}

impl<'a> MyStructDyn<'a> {
    fn new(f: &'a dyn Fn(i32) -> i32) -> MyStructDyn<'a> {
        MyStructDyn { closure_field: f }
    }

    fn apply(&self, n: i32) -> i32 {
        (self.closure_field)(n)
    }
}
    let f = |n: i32| n + 1;	// クロージャー
    let s = MyStructDyn::new(&f);
    let r = s.apply(123);
    println!("r = {}", r);

これで動作するけど、クロージャーの寿命がMyStructDynインスタンスの寿命より短いとコンパイルエラーになる。

    let s;
    {
        let f = |n: i32| n + 1;	// クロージャー
        s = MyStructDyn::new(&f);
    } // スコープが終わるのでfが破棄されるが、fの参照を入れたsがスコープの外にあるので、借用の違反になる
    let r = s.apply(123);
    println!("r = {}", r);

ちなみに以下のコードは通る。

    let s;
    {
        s = MyStructDyn::new(&|n| n + 1);
    }
    let r = s.apply(123);
    println!("r = {}", r);

変数に代入されないクロージャーは一時的な値(temporary value)という扱いになり、一時的な値のライフタイムはそのオブジェクトが使われる範囲に応じて自動的に延長されるらしい。


フィールドでBox<dynクロージャー>を保持する例

struct MyStructBoxDyn {
    closure_field: Box<dyn Fn(i32) -> i32>,
}

impl MyStructBoxDyn {
    fn new(f: Box<dyn Fn(i32) -> i32>) -> MyStructBoxDyn {
        MyStructBoxDyn { closure_field: f }
    }

    fn apply(&self, n: i32) -> i32 {
        (self.closure_field)(n)
    }
}
    let f = |n: i32| n + 1;	// クロージャー
    let s = MyStructBoxDyn::new(Box::new(f));
    let r = s.apply(123);
    println!("r = {}", r);

これなら、クロージャーのスコープを超えることが出来る。

    let s;
    {
        let f = |n: i32| n + 1;	// クロージャー
        s = MyStructBoxDyn::new(Box::new(f));

//      s = MyStructBoxDyn::new(Box::new(|n| n + 1));
    }
    let r = s.apply(123);
    println!("r = {}", r);

Rustではジェネリクスに指定する具象型ごとにバイナリーコードが生成されるらしいが、dynのクロージャーは型としてはトレイトオブジェクトという一種類らしいので、色々なクロージャーを指定 しても、生成される実行ファイルのサイズは変わらない。


ジェネリクスでimplクロージャーを指定する例

ジェネリクスのwhereの構文を使ってimplのクロージャーの型を指定すると、それを使ってフィールドに保持できる。
(クロージャーの型でimplもdynも指定しない場合は、implと同じ)

struct MyStructImpl<F>
where
    F: Fn(i32) -> i32,
{
    closure_field: F,
}

impl<F> MyStructImpl<F>
where
    F: Fn(i32) -> i32,
{
    fn new(f: F) -> MyStructImpl<F> {
        MyStructImpl { closure_field: f }
    }

    fn apply(&self, n: i32) -> i32 {
        (self.closure_field)(n)
    }
}
    let s;
    {
        let f = |n: i32| n + 1;	// クロージャー
        s = MyStructImpl::new(f);

//      s = MyStructImpl::new(|n| n + 1);
    }
    let r = s.apply(123);
    println!("r = {}", r);

Rustではジェネリクスに指定する具象型ごとにバイナリーコードが生成されるらしいので、implクロージャーの型をジェネリクスに指定する方法は、色々なクロージャーを指定すると生成される実行ファイルのサイズが増えてしまう。


Rustへ戻る / 技術メモへ戻る
メールの送信先:ひしだま