メソッド記法

メソッドは関数に似ています: fnキーワードと名前で宣言されるし、引数と返り値があるし、 どこか別の場所で呼び出された時に実行されるコードを含みます。ところが、 メソッドは構造体の文脈(あるいはenumかトレイトオブジェクトの。これらについては各々第6章と17章で解説します)で定義されるという点で、 関数とは異なり、最初の引数は必ずselfになり、これはメソッドが呼び出されている構造体インスタンスを表します。

メソッドを定義する

Rectangleインスタンスを引数に取るarea関数を変え、代わりにRectangle構造体上にareaメソッドを作りましょう。 リスト5-13に示した通りですね。

ファイル名: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    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.",
        rect1.area()
    );
}

リスト5-13: Rectangle構造体上にareaメソッドを定義する

Rectangleの文脈内で関数を定義するには、impl(implementation; 実装)ブロックを始めます。 それからarea関数をimplの波かっこ内に移動させ、最初の(今回は唯一の)引数をシグニチャ内と本体内全てでselfに変えます。 area関数を呼び出し、rect1を引数として渡すmainでは、代替としてメソッド記法を使用して、 Rectangleインスタンスのareaメソッドを呼び出せます。メソッド記法は、インスタンスの後に続きます: ドット、メソッド名、かっこ、そして引数と続くわけです。

areaのシグニチャでは、rectangle: &Rectangleの代わりに&selfを使用しています。 というのも、コンパイラは、このメソッドがimpl Rectangleという文脈内に存在するために、 selfの型がRectangleであると把握しているからです。&Rectangleと同様に、 selfの直前に&を使用していることに注意してください。メソッドは、selfの所有権を奪ったり、 ここでしているように不変でselfを借用したり、可変でselfを借用したりできるのです。 他の引数と全く同じですね。

ここで&selfを選んでいるのは、関数バージョンで&Rectangleを使用していたのと同様の理由です: 所有権はいらず、構造体のデータを読み込みたいだけで、書き込む必要はないわけです。 メソッドの一部でメソッドを呼び出したインスタンスを変更したかったら、第1引数に&mut selfを使用するでしょう。 selfだけを第1引数にしてインスタンスの所有権を奪うメソッドを定義することは稀です; このテクニックは通常、 メソッドがselfを何か別のものに変形し、変形後に呼び出し元が元のインスタンスを使用できないようにしたい場合に使用されます。

関数の代替としてメソッドを使う主な利点は、メソッド記法を使用して全メソッドのシグニチャでselfの型を繰り返す必要がなくなる以外だと、 体系化です。コードの将来的な利用者にRectangleの機能を提供しているライブラリ内の各所でその機能を探させるのではなく、 この型のインスタンスでできることを一つのimplブロックにまとめあげています。

->演算子はどこに行ったの?

CとC++では、メソッド呼び出しには2種類の異なる演算子が使用されます: オブジェクトに対して直接メソッドを呼び出すのなら、.を使用するし、オブジェクトのポインタに対してメソッドを呼び出し、 先にポインタを参照外しする必要があるなら、->を使用するわけです。 言い換えると、objectがポインタなら、object->something()は、(*object).something()と同等なのです。

Rustには->演算子の代わりとなるようなものはありません; その代わり、Rustには、 自動参照および参照外しという機能があります。Rustにおいてメソッド呼び出しは、 この動作が行われる数少ない箇所なのです。

動作方法はこうです: object.something()とメソッドを呼び出すと、 コンパイラはobjectがメソッドのシグニチャと合致するように、自動で&&mut*を付与するのです。 要するに、以下のコードは同じものです:


# #![allow(unused_variables)]
#fn main() {
# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
#
# impl Point {
#    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
#
#        f64::sqrt(x_squared + y_squared)
#    }
# }
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
#}

前者の方がずっと明確です。メソッドには自明な受け手(selfの型)がいるので、この自動参照機能は動作するのです。 受け手とメソッド名が与えられれば、コンパイラは確実にメソッドが読み込み専用(&self)か、書き込みもする(&mut self)のか、 所有権を奪う(self)のか判断できるわけです。Rustにおいて、メソッドの受け手に関して借用が明示されないという事実は、 所有権を現実世界でエルゴノミックにさせる大部分を占めているのです。

より引数の多いメソッド

Rectangle構造体に2番目のメソッドを実装して、メソッドを使う鍛錬をしましょう。今回は、Rectangleのインスタンスに、 別のRectangleのインスタンスを取らせ、2番目のRectangleselfに完全にはめ込まれたら、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 };

    // rect1にrect2ははまり込む?
    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

リスト5-14: 未完成のcan_holdを使用する

そして、予期される出力は以下のようになります。なぜなら、rect2の各次元はrect1よりも小さいものの、 rect3rect1より幅が広いからです:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

メソッドを定義したいことはわかっているので、impl Rectangleブロック内での話になります。 メソッド名は、can_holdになり、引数として別のRectangleを不変借用で取るでしょう。 メソッドを呼び出すコードを見れば、引数の型が何になるかわかります: rect1.can_hold(&rect2)は、 &rect2Rectangleのインスタンスであるrect2への不変借用を渡しています。 これは道理が通っています。なぜなら、rect2を読み込む(書き込みではなく。この場合、可変借用が必要になります)だけでよく、 can_holdメソッドを呼び出した後にもrect2が使えるよう、所有権をmainに残したままにしたいからです。 can_holdの返り値は、booleanになり、メソッドの中身は、selfの幅と高さがもう一つのRectangleの幅と高さよりも、 それぞれ大きいことを確認します。リスト5-13のimplブロックに新しいcan_holdメソッドを追記しましょう。 リスト5-15に示した通りです。

ファイル名: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# #[derive(Debug)]
# struct Rectangle {
#     width: u32,
#     height: u32,
# }
#
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のインスタンスを引数として取るcan_holdメソッドを、 Rectangleに実装する

このコードをリスト5-14のmain関数と合わせて実行すると、望み通りの出力が得られます。 メソッドは、self引数の後にシグニチャに追加した引数を複数取ることができ、 その引数は、関数の引数と同様に動作するのです。

関連関数

implブロックの別の有益な機能は、implブロック内にselfを引数に取らない関数を定義できることです。 これは、構造体に関連付けられているので、関連関数と呼ばれます。それでも、関連関数は関数であり、メソッドではありません。 というのも、対象となる構造体のインスタンスが存在しないからです。もうString::fromという関連関数を使用したことがありますね。

関連関数は、構造体の新規インスタンスを返すコンストラクタによく使用されます。例えば、一次元の引数を取り、 長さと幅両方に使用する関連関数を提供することができ、その結果、同じ値を2回指定する必要なく、 正方形のRectangleを生成しやすくすることができます。

ファイル名: src/main.rs


# #![allow(unused_variables)]
#fn main() {
# #[derive(Debug)]
# struct Rectangle {
#     width: u32,
#     height: u32,
# }
#
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { width: size, height: size }
    }
}
#}

この関連関数を呼び出すために、構造体名と一緒に::記法を使用します; 一例はlet sq = Rectangle::square(3);です。 この関数は、構造体によって名前空間分けされています: ::という記法は、関連関数とモジュールによって作り出される名前空間両方に使用されます。 モジュールについては第7章で議論します。

複数のimplブロック

各構造体には、複数のimplブロックを存在させることができます。例えば、リスト5-15はリスト5-16に示したコードと等価で、 リスト5-16では、各メソッドごとにimplブロックを用意しています。


# #![allow(unused_variables)]
#fn main() {
# #[derive(Debug)]
# struct Rectangle {
#     width: u32,
#     height: u32,
# }
#
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ブロックに分ける理由はないのですが、合法な書き方です。 複数のimplブロックが有用になるケースは第10章で見ますが、そこではジェネリック型と、トレイトについて議論します。

まとめ

構造体により、自分の領域で意味のある独自の型を作成することができます。構造体を使用することで、 関連のあるデータ片を相互に結合させたままにし、各部品に名前を付け、コードを明確にすることができます。 メソッドにより、構造体のインスタンスが行う動作を指定することができ、関連関数により、 構造体に特有の機能をインスタンスを利用することなく、名前空間分けすることができます。

しかし、構造体だけが独自の型を作成する手段ではありません: Rustのenum機能に目を向けて、 別の道具を道具箱に追加しましょう。