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に相当)
関連関数(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);
メソッドは、構造体のインスタンスに対して操作を行う関数と言えるだろう。
メソッドを定義する際は、第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のクロージャーの参照を保持することで一応作れるが、ライフタイムの問題がある。
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を使う方法。
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のクロージャーは型としてはトレイトオブジェクトという一種類らしいので、色々なクロージャーを指定 しても、生成される実行ファイルのサイズは変わらない。
ジェネリクスの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クロージャーの型をジェネリクスに指定する方法は、色々なクロージャーを指定すると生成される実行ファイルのサイズが増えてしまう。
トレイトを構造体のフィールドに直接保持することは出来ない。[2025-07-05]
(Javaなら、クラスだろうがインターフェースだろうか区別なくフィールドに保持できるので、Rustでもついそのようなイメージで考えてしまうのだが…)
trait MyTrait {
fn println(&self);
}
struct MyStruct {
value: MyTrait, ←コンパイルエラー
}
これが許されないのは、コンパイル時にトレイトのサイズが決められないという理由らしい。
実行時に構造体のインスタンスを作るとき、その構造体のサイズのメモリーが確保される。
そのサイズはコンパイル時に決めるので、コンパイル時に確定する必要がある。
トレイトをフィールドに保持するというのは、トレイトを実装した構造体を保持したいという意図だろうが、
それらの構造体のサイズはまちまちだろうし、コンパイル時点でどの構造体を保持するのかは決められないから、サイズは確定できない。
Javaの場合、クラスのフィールドに別のクラスを持つのは、内部的には、そのクラスの参照(ポインター)を持つということである。
クラスだろうがインターフェースだろうが、ポインターのサイズに変わりは無い。
しかしRustの場合、構造体のフィールドに別の構造体を持つということは、その構造体を直接持つということのようだ。
struct A {
b: B,
c: C,
}
概念的には、構造体Bが16バイト、構造体Cが32バイトなら、構造体Aのサイズは16+32バイトで、
Aの先頭16バイトをBとして扱い、後ろ32バイトをCとして扱うといったところか。
(実際にはパディングが入ったりするので、この通りとは限らない)
だから、構造体の各フィールドのサイズがコンパイル時に確定できなければならない。
したがって、トレイトのポインター(アドレス)であれば、構造体のフィールドに保持することが出来る。
trait MyTrait {
fn println(&self);
}
struct MyTraitStruct {}
impl MyTrait for MyTraitStruct {
fn println(&self) {
println!("MyTraitStruct");
}
}
struct MyStruct {
value: Box<dyn MyTrait>,
}
impl MyStruct {
fn new(value: Box<dyn MyTrait>) -> MyStruct {
MyStruct { value }
}
fn println(&self){
self.value.println();
}
}
fn main() {
let t = MyTraitStruct {};
let s = MyStruct::new(Box::new(t));
s.println();
}
Boxは、構造体をヒープ上に作ってそのアドレスを扱うというものなので、Box自身のサイズはコンパイル時に確定できるということなのだろう。
なお、以下のようにBoxを使わなくてもトレイトのフィールドを定義することは出来る。(コンパイルエラーにはならない)
struct MyStruct {
value: dyn MyTrait,
}
しかし、このフィールドに代入するメソッドを作ろうとすると、非常に苦労することになるだろう…。