はじめに
メソッド構文へようこそ。この実験は、Rust Bookの一部です。LabEx で Rust のスキルを練習できます。
この実験では、メソッドはfnキーワードと名前で宣言され、パラメータと戻り値を持つことができ、構造体のコンテキスト内で定義されます。最初のパラメータは常にselfで、呼び出されている構造体のインスタンスを表します。
メソッド構文
メソッドは関数に似ています。fnキーワードと名前で宣言し、パラメータと戻り値を持ち、他の場所からメソッドが呼び出されたときに実行されるコードを含んでいます。関数とは異なり、メソッドは構造体(または列挙体またはトレイトオブジェクト、それぞれ第 6 章と第 17 章で扱います)のコンテキスト内で定義され、最初のパラメータは常にselfで、メソッドが呼び出されている構造体のインスタンスを表します。
メソッドの定義
Rectangleインスタンスをパラメータとして持つarea関数を変更し、代わりにRectangle構造体に定義されたareaメソッドにしましょう。リスト 5-13 に示すようになります。
ファイル名:src/main.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
1 impl Rectangle {
2 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.",
3 rect1.area()
);
}
リスト 5-13: Rectangle構造体にareaメソッドを定義する
Rectangleのコンテキスト内で関数を定義するには、Rectangle用のimpl(実装)ブロックを開始します[1]。このimplブロック内のすべてのものはRectangle型に関連付けられます。次に、area関数をimplの波括弧内に移動し[2]、シグネチャと本体のどこでも最初の(この場合は唯一の)パラメータをselfに変更します。mainでは、area関数を呼び出してrect1を引数として渡していましたが、代わりにメソッド構文を使用してRectangleインスタンスのareaメソッドを呼び出すことができます[3]。メソッド構文はインスタンスの後に続きます。ドットを追加し、その後にメソッド名、丸括弧、および任意の引数を追加します。
areaのシグネチャでは、&selfを使用してrectangle: &Rectangleの代わりにします。&selfは実際にはself: &Selfの省略形です。implブロック内では、型Selfはimplブロックが対象とする型のエイリアスです。メソッドは最初のパラメータとしてSelf型のselfという名前のパラメータを持たなければなりません。そのため、Rust は最初のパラメータの場所でこれをselfという名前だけで省略することができます。ただし、selfの省略形の前に&を使用する必要があります。これは、このメソッドがSelfインスタンスを借用することを示すためで、rectangle: &Rectangleの場合と同じです。メソッドはselfの所有権を取得することができ、ここで行ったようにselfを不変に借用することも、selfを可変に借用することもできます。他のパラメータと同じようにです。
ここで&selfを選んだ理由は、関数バージョンで&Rectangleを使用した理由と同じです。所有権を取得したくなく、構造体のデータを読み取るだけで書き込みたくないからです。メソッドの一部としてメソッドを呼び出したインスタンスを変更したい場合、最初のパラメータとして&mut selfを使用します。最初のパラメータとして単にselfを使用してインスタンスの所有権を取得するメソッドはまれです。この技術は、通常、メソッドがselfを別のものに変換し、変換後に呼び出し元が元のインスタンスを使用できなくなるようにするために使用されます。
関数の代わりにメソッドを使用する主な理由は、メソッド構文を提供し、各メソッドのシグネチャでselfの型を繰り返さなくて済むことに加えて、組織化のためです。型のインスタンスでできることすべてを 1 つのimplブロックにまとめており、提供するライブラリのさまざまな場所でRectangleの機能を探す必要がなくなります。
構造体のフィールドと同じ名前のメソッドを与えることができることに注意してください。たとえば、Rectangleにもwidthという名前のメソッドを定義できます。
ファイル名:src/main.rs
impl Rectangle {
fn width(&self) -> bool {
self.width > 0
}
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
if rect1.width() {
println!(
"The rectangle has a nonzero width; it is {}",
rect1.width
);
}
}
ここでは、インスタンスのwidthフィールドの値が 0 より大きい場合、widthメソッドがtrueを返し、値が 0 の場合にfalseを返すように選択しています。同じ名前のメソッド内のフィールドを任意の目的に使用できます。mainでは、rect1.widthの後に丸括弧を付けると、Rust はwidthメソッドを意味することを知ります。丸括弧を使用しない場合、Rust はwidthフィールドを意味することを知ります。
多くの場合、フィールドと同じ名前のメソッドを与えるとき、それがフィールドの値だけを返して何もしないようにします。このようなメソッドは「ゲッター」と呼ばれ、Rust は他の言語のように構造体フィールドに対して自動的に実装しません。ゲッターは便利です。フィールドを非公開にすることができますが、メソッドを公開にすることができるため、型の公開 API の一部としてそのフィールドへの読み取り専用アクセスを可能にします。第 7 章では、公開と非公開とは何か、およびフィールドまたはメソッドを公開または非公開に指定する方法について説明します。
->演算子はどこにありますか?
C と C++ では、メソッドを呼び出すために 2 つの異なる演算子が使用されます。オブジェクト自体にメソッドを呼び出す場合は
.を使用し、オブジェクトへのポインタにメソッドを呼び出して最初にポインタを解引用する必要がある場合は->を使用します。言い換えると、objectがポインタの場合、object->something()は(*object).something()に似ています。Rust には
->演算子に相当するものはありません。代わりに、Rust には「自動参照と解引用」と呼ばれる機能があります。メソッドを呼び出すことは、この動作を持つ Rust の少数の場所の 1 つです。動作の仕方は次のとおりです。
object.something()でメソッドを呼び出すと、Rust は自動的に&、&mut、または*を追加して、objectがメソッドのシグネチャに一致するようにします。言い換えると、次の 2 つは同じです。p1.distance(&p2); (&p1).distance(&p2);最初の方がはるかにクリーンに見えます。この自動参照動作が機能するのは、メソッドには明確なレシーバー(
selfの型)があるからです。メソッドのレシーバーと名前が与えられると、Rust はメソッドが読み取り(&self)、変更(&mut self)、または消費(self)しているかどうかを確定的に判断できます。Rust がメソッドレシーバーに対する借用を暗黙的に行うことは、所有権を実践的に使いやすくするための大きな要素の 1 つです。
より多くのパラメータを持つメソッド
Rectangle構造体に 2 番目のメソッドを実装することで、メソッドの使用方法を練習しましょう。今回は、Rectangleのインスタンスに別のRectangleのインスタンスを渡し、2 番目のRectangleがself(最初のRectangle)の中に完全に収まる場合にtrueを返します。そうでない場合はfalseを返します。つまり、can_holdメソッドを定義した後は、リスト 5-14 に示すプログラムを書けるようになります。
ファイル名:src/main.rs
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
リスト 5-14: まだ書かれていないcan_holdメソッドを使用する
期待される出力は次のようになります。rect2の両方の寸法がrect1の寸法より小さいためですが、rect3はrect1よりも幅が広いです。
Can rect1 hold rect2? true
Can rect1 hold rect3? false
メソッドを定義することがわかっているので、impl Rectangleブロック内にあるはずです。メソッド名はcan_holdになり、別のRectangleの不変借用をパラメータとして取ります。メソッドを呼び出すコードを見ることで、パラメータの型を判断できます。rect1.can_hold(&rect2)は&rect2を渡しています。これはrect2(Rectangleのインスタンス)への不変借用です。これは理にかなっています。なぜなら、rect2を読み取るだけで済む(書き込む場合は可変借用が必要になります)ため、mainがrect2の所有権を保持して、can_holdメソッドを呼び出した後も再利用できるようにするためです。can_holdの戻り値はブール値になり、実装ではselfの幅と高さがそれぞれ他のRectangleの幅と高さより大きいかどうかを確認します。リスト 5-13 のimplブロックに新しいcan_holdメソッドを追加しましょう。リスト 5-15 に示します。
ファイル名: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
}
}
リスト 5-15: 別のRectangleインスタンスをパラメータとして持つRectangleにcan_holdメソッドを実装する
リスト 5-14 のmain関数でこのコードを実行すると、期待される出力が得られます。メソッドはselfパラメータの後にシグネチャに追加する複数のパラメータを持つことができ、それらのパラメータは関数のパラメータと同じように機能します。
関連付けられた関数
implブロック内で定義されるすべての関数は、implの後に名前付けされた型と関連付けられているため、関連付けられた関数と呼ばれます。selfを最初のパラメータとして持たない(したがってメソッドではない)関連付けられた関数を定義することができます。なぜなら、それらは型のインスタンスを使用して動作する必要がないからです。このような関数を既に 1 つ使用しています。String型に定義されたString::from関数です。
メソッドではない関連付けられた関数は、構造体の新しいインスタンスを返すコンストラクタによく使用されます。これらはしばしばnewと呼ばれますが、newは特別な名前ではなく、言語に組み込まれていません。たとえば、1 つの寸法パラメータを持ち、それを幅と高さの両方として使用することで、正方形のRectangleを作成するのが簡単になるように、squareという名前の関連付けられた関数を提供することができます。そうすることで、同じ値を 2 回指定する必要がなくなります。
ファイル名:src/main.rs
impl Rectangle {
fn square(size: u32) -> 1 Self {
2 Self {
width: size,
height: size,
}
}
}
戻り値の型[1]と関数の本体[2]にあるSelfキーワードは、implキーワードの後に現れる型のエイリアスです。この場合、それはRectangleです。
この関連付けられた関数を呼び出すには、構造体名とともに::構文を使用します。let sq = Rectangle::square(3);がその例です。この関数は構造体によって名前空間化されます。::構文は、関連付けられた関数とモジュールによって作成される名前空間の両方に使用されます。第 7 章でモジュールについて説明します。
複数の impl ブロック
各構造体は複数のimplブロックを持つことができます。たとえば、リスト 5-15 は、それぞれのメソッドが独自のimplブロックにあるリスト 5-16 に示すコードと同等です。
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
リスト 5-16: 複数のimplブロックを使用してリスト 5-15 を書き直す
ここでは、これらのメソッドを複数のimplブロックに分ける理由はありませんが、これは有効な構文です。第 10 章でジェネリック型とトレイトについて説明する際に、複数のimplブロックが役に立つケースを見ます。
まとめ
おめでとうございます!あなたはメソッド構文の実験を完了しました。あなたのスキルを向上させるために、LabEx でさらに実験を行って練習することができます。