// book chapter
動かして学ぶRustプログラミング問題集第21章 トレイトオブジェクトと関連型.
第20章で、トレイト境界とimpl Traitによって「このトレイトを実装した型なら何でも受け取れる」関数が書けるようになりました。ただし、それらはいずれもコンパイル時に型が1つに決まる仕組みでした。Vec<T>のTは1つの型にしかなれないので、「犬も猫も入ったベクタ」のような、型の混ざったコレクションは作れません。
この章の前半では、その壁を越えるトレイトオブジェクト(dyn トレイト名)を扱います。「このトレイトを実装した何らかの型」を1つの型として扱えるようになり、どのメソッド実装を呼ぶかは実行時に決まります。ジェネリクスとは何が違い、どう使い分けるのかも合わせて確認します。
後半は関連型です。これまでのトレイトはメソッドしか持っていませんでしたが、トレイトは「この型は実装側で決めてよい」という型のプレースホルダも持てます。その応用として、+などの演算子を自作の型で使えるようにする演算子オーバーロードまで進みます。標準ライブラリのAddトレイトが、関連型をどう使っているかを実際に見ることになります。
進め方は第20章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - Vecに異なる型を入れたい
トレイトオブジェクトとベクタ(Vec)とトレイトに関する問題です。
次のコードはコンパイルエラー(E0308)になります。DogとCatの両方を1つのベクタに入れられるよう、animalsの型注釈を書き足して修正してください。
ポチはワンと鳴く
タマはニャーと鳴くtrait Animal {
fn name(&self) -> String;
fn cry(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn name(&self) -> String {
String::from("ポチ")
}
fn cry(&self) -> String {
String::from("ワン")
}
}
impl Animal for Cat {
fn name(&self) -> String {
String::from("タマ")
}
fn cry(&self) -> String {
String::from("ニャー")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
let animals = vec![&dog, &cat];
for animal in &animals {
println!("{}は{}と鳴く", animal.name(), animal.cry());
}
}Playgroundで開く解答例と解説
trait Animal {
fn name(&self) -> String;
fn cry(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn name(&self) -> String {
String::from("ポチ")
}
fn cry(&self) -> String {
String::from("ワン")
}
}
impl Animal for Cat {
fn name(&self) -> String {
String::from("タマ")
}
fn cry(&self) -> String {
String::from("ニャー")
}
}
fn main() {
let dog = Dog;
let cat = Cat;
let animals = vec![&dog, &cat];
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
for animal in &animals {
println!("{}は{}と鳴く", animal.name(), animal.cry());
}
}Playgroundで開くエラーメッセージはmismatched types(E0308)で、expected '&Dog', found '&Cat'と続きます。vec![&dog, &cat]の最初の要素から「これはVec<&Dog>だ」と推論されたところへ、&Catが来たので型が合わない、という指摘です。
第17章で学んだとおり、Vec<T>のTは1つの型に決まります。DogとCatが同じAnimalトレイトを実装していても、型としては別物なので、そのままでは同じベクタに入りません。
ここで使うのがトレイトオブジェクトです。dyn Animalは「Animalを実装している何らかの型」を表す型で、DogでもCatでもこの型として扱えます。要素の型を&dyn Animalにすれば、ベクタのTは&dyn Animalという1つの型に揃い、中身が&Dogでも&Catでも受け入れられます。
dyn Animalが具体的にどの型なのかはコンパイル時には分かりません。そのため値をそのまま置くことはできず、必ず&などのポインタ越しに扱います。この問題では&dog・&catと参照を入れているので&dyn Animalになりました。
forループの中ではanimal.name()やanimal.cry()と、これまでどおりメソッドを呼べています。Animalトレイトのメソッドであることは分かっているので、Dog版とCat版のどちらが動くかは要素ごとに実行時に決まります。この仕組みは次の問題02で詳しく見ます。
02 - &dyn Traitを引数で受け取る
トレイトオブジェクトと参照に関する問題です。
introduce関数を定義して、トレイトオブジェクト経由でDogとCatを紹介してください。
ポチと申します。ワン!
タマと申します。ニャー!trait Animal {
fn name(&self) -> String;
fn cry(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn name(&self) -> String {
String::from("ポチ")
}
fn cry(&self) -> String {
String::from("ワン")
}
}
impl Animal for Cat {
fn name(&self) -> String {
String::from("タマ")
}
fn cry(&self) -> String {
String::from("ニャー")
}
}
// 関数introduceを定義せよ
// 引数: &dyn Animal
// 処理: 「〇〇と申します。△△!」と出力する(〇〇は名前、△△は鳴き声)
fn main() {
let dog = Dog;
let cat = Cat;
introduce(&dog);
introduce(&cat);
}Playgroundで開く解答例と解説
trait Animal {
fn name(&self) -> String;
fn cry(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn name(&self) -> String {
String::from("ポチ")
}
fn cry(&self) -> String {
String::from("ワン")
}
}
impl Animal for Cat {
fn name(&self) -> String {
String::from("タマ")
}
fn cry(&self) -> String {
String::from("ニャー")
}
}
// 関数introduceを定義せよ
// 引数: &dyn Animal
// 処理: 「〇〇と申します。△△!」と出力する(〇〇は名前、△△は鳴き声)
fn introduce(animal: &dyn Animal) {
println!("{}と申します。{}!", animal.name(), animal.cry());
}
fn main() {
let dog = Dog;
let cat = Cat;
introduce(&dog);
introduce(&cat);
}Playgroundで開く&dyn Animalは関数の引数にも書けます。呼び出し側は&dog・&catと普通の参照を渡すだけで、受け取る側では&dyn Animalというトレイトオブジェクトとして扱われます。
introduce関数の中では、animalがDogなのかCatなのかは分かりません。分かっているのは「Animalを実装している何か」ということだけです。それでもanimal.name()と書けるのは、Animalトレイトにnameメソッドがあると宣言されているからです。
では、実際にDog::nameとCat::nameのどちらが動くのかは、いつ決まるのでしょうか。答えは実行時です。&dyn Animalは、値へのポインタに加えて「この値の型におけるAnimalの各メソッドはどこにあるか」という表(vtable)へのポインタも持っています。メソッドを呼ぶたびにこの表を引いて、該当する実装へ飛びます。これを動的ディスパッチと呼びます。
第20章のfn introduce(animal: &impl Animal)やfn introduce<T: Animal>(animal: &T)では、呼び出しごとに型が1つに決まり、コンパイル時にDog用・Cat用の関数がそれぞれ作られていました(静的ディスパッチ)。&dyn Animal版は関数が1つだけで、その中で実行時に振り分けています。書き味はほとんど同じですが、裏側の仕組みが違います。次の問題03で2つを並べて比べます。
03 - ジェネリクスとの使い分け
トレイトオブジェクトとジェネリクスとimpl Traitに関する問題です。
introduce_dynと同じ処理をするintroduce_impl関数を、引数を&impl Animalにして定義してください。
[impl] ポチと申します。ワン!
[impl] タマと申します。ニャー!
[dyn] ポチと申します。ワン!
[dyn] タマと申します。ニャー!trait Animal {
fn name(&self) -> String;
fn cry(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn name(&self) -> String {
String::from("ポチ")
}
fn cry(&self) -> String {
String::from("ワン")
}
}
impl Animal for Cat {
fn name(&self) -> String {
String::from("タマ")
}
fn cry(&self) -> String {
String::from("ニャー")
}
}
// 関数introduce_implを定義せよ
// 引数: &impl Animal
// 処理: 「[impl] 〇〇と申します。△△!」と出力する
fn introduce_dyn(animal: &dyn Animal) {
println!("[dyn] {}と申します。{}!", animal.name(), animal.cry());
}
fn main() {
let dog = Dog;
let cat = Cat;
introduce_impl(&dog);
introduce_impl(&cat);
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
for animal in animals {
introduce_dyn(animal);
}
}Playgroundで開く解答例と解説
trait Animal {
fn name(&self) -> String;
fn cry(&self) -> String;
}
struct Dog;
struct Cat;
impl Animal for Dog {
fn name(&self) -> String {
String::from("ポチ")
}
fn cry(&self) -> String {
String::from("ワン")
}
}
impl Animal for Cat {
fn name(&self) -> String {
String::from("タマ")
}
fn cry(&self) -> String {
String::from("ニャー")
}
}
// 関数introduce_implを定義せよ
// 引数: &impl Animal
// 処理: 「[impl] 〇〇と申します。△△!」と出力する
fn introduce_impl(animal: &impl Animal) {
println!("[impl] {}と申します。{}!", animal.name(), animal.cry());
}
fn introduce_dyn(animal: &dyn Animal) {
println!("[dyn] {}と申します。{}!", animal.name(), animal.cry());
}
fn main() {
let dog = Dog;
let cat = Cat;
introduce_impl(&dog);
introduce_impl(&cat);
let animals: Vec<&dyn Animal> = vec![&dog, &cat];
for animal in animals {
introduce_dyn(animal);
}
}Playgroundで開く2つの関数は、本体の中身がまったく同じです。違いは引数の型が&impl Animalか&dyn Animalかだけで、呼び出し方もどちらも&dog・&catを渡すだけです。それでも、コンパイラがしていることは次のように異なります。
| 観点 | &impl Animal(ジェネリクス) |
&dyn Animal(トレイトオブジェクト) |
|---|---|---|
| 呼ぶ実装が決まるとき | コンパイル時(静的ディスパッチ) | 実行時(動的ディスパッチ) |
| 生成される関数 | Dog用とCat用の2つ |
1つ |
| 実行時のコスト | なし | vtableを引く分だけかかる |
| 異なる型の混在 | 不可 | 可(Vec<&dyn Animal>) |
introduce_implは第20章で学んだとおり、fn introduce_impl<T: Animal>(animal: &T)の糖衣構文です。Dogで呼べばT = Dog、Catで呼べばT = Catの専用関数がコンパイル時に作られるので(単相化)、呼び先は最初から決まっています。
一方、mainの後半のforループではanimalsの要素が&dyn Animalなので、introduce_implに渡すことはできません。TをDogかCatのどちらかに決めようがないからです。ここではintroduce_dynが必要になります。
使い分けの目安はシンプルです。
- 関数の引数として1つの値を受け取るだけなら、ジェネリクス(
impl Trait)が基本です。実行時コストがなく、最適化も効きます - 「異なる型を同じコレクションに入れたい」「実行時にどの型が来るか決まる」なら、トレイトオブジェクトを選びます
04 - 関連型
関連型とトレイトに関する問題です。
SensorトレイトのThermometer向け実装とDoorSensor向け実装を完成させてください。Thermometerは温度をf64で、DoorSensorは開閉状態をboolで返します。
温度: 23.5
ドアが開いている: truetrait Sensor {
type Value; // 測定値の型は実装側で決める
fn read(&self) -> Self::Value;
}
struct Thermometer {
celsius: f64,
}
struct DoorSensor {
is_open: bool,
}
// Thermometer向けにSensorを実装せよ(Valueはf64、readはcelsiusを返す)
// DoorSensor向けにSensorを実装せよ(Valueはbool、readはis_openを返す)
fn main() {
let thermometer = Thermometer { celsius: 23.5 };
let door = DoorSensor { is_open: true };
println!("温度: {}", thermometer.read());
println!("ドアが開いている: {}", door.read());
}Playgroundで開く解答例と解説
trait Sensor {
type Value; // 測定値の型は実装側で決める
fn read(&self) -> Self::Value;
}
struct Thermometer {
celsius: f64,
}
struct DoorSensor {
is_open: bool,
}
// Thermometer向けにSensorを実装せよ(Valueはf64、readはcelsiusを返す)
impl Sensor for Thermometer {
type Value = f64;
fn read(&self) -> f64 {
self.celsius
}
}
// DoorSensor向けにSensorを実装せよ(Valueはbool、readはis_openを返す)
impl Sensor for DoorSensor {
type Value = bool;
fn read(&self) -> bool {
self.is_open
}
}
fn main() {
let thermometer = Thermometer { celsius: 23.5 };
let door = DoorSensor { is_open: true };
println!("温度: {}", thermometer.read());
println!("ドアが開いている: {}", door.read());
}Playgroundで開くSensorトレイトの中のtype Value;が関連型です。「readは何かの値を返す。ただし、その型はここでは決めない」という宣言で、Self::Valueはその型を指す書き方です。
実装側ではtype Value = f64;のように、具体的な型を1つ決めます。すると、そのimplブロックの中ではSelf::Valueがf64になるので、readの戻り値はf64として書けます。解答例のように-> f64と直接書いても、-> Self::Valueと書いても同じ意味です。
温度計は「数値を返すセンサー」、ドアセンサーは「真偽値を返すセンサー」です。同じSensorトレイトを実装していながら、readの戻り値の型が実装ごとに違います。これが関連型の役割で、メソッドの中身だけでなく、型まで実装側に委ねることができます。
mainでの呼び出し側はthermometer.read()・door.read()と書くだけで、戻り値の型は実装から自動的に決まります。println!の{}に渡せているのは、f64もboolもDisplayを実装しているためです。
05 - 関連型は実装ごとに1つ
関連型とトレイトに関する問題です。
次のコードはコンパイルエラー(E0119)になります。なぜエラーになるかを考え、キロメートルへの変換だけを残す形に修正してください。
1500メートル = 1.5キロメートルtrait Convert {
type Output;
fn convert(&self) -> Self::Output;
}
struct Meters(f64);
// センチメートル(整数)へ変換
impl Convert for Meters {
type Output = i32;
fn convert(&self) -> i32 {
(self.0 * 100.0) as i32
}
}
// キロメートルへ変換
impl Convert for Meters {
type Output = f64;
fn convert(&self) -> f64 {
self.0 / 1000.0
}
}
fn main() {
let distance = Meters(1500.0);
println!("{}メートル = {}キロメートル", distance.0, distance.convert());
}Playgroundで開く解答例と解説
trait Convert {
type Output;
fn convert(&self) -> Self::Output;
}
struct Meters(f64);
// センチメートル(整数)へ変換
impl Convert for Meters {
type Output = i32;
fn convert(&self) -> i32 {
(self.0 * 100.0) as i32
}
}
// キロメートルへ変換
impl Convert for Meters {
type Output = f64;
fn convert(&self) -> f64 {
self.0 / 1000.0
}
}
fn main() {
let distance = Meters(1500.0);
println!("{}メートル = {}キロメートル", distance.0, distance.convert());
}Playgroundで開くエラーメッセージはconflicting implementations of trait 'Convert' for type 'Meters'(E0119)です。「Metersに対するConvertの実装が衝突している」と言われています。
type Output = i32;とtype Output = f64;で中身が違うのだから別の実装として扱ってほしい、と思うかもしれません。しかしRustでは、1つの型に対して同じトレイトは1回しか実装できません。関連型はその実装の中で決めるものなので、MetersのOutputは必然的に1つに定まります。2つ目のimpl Convert for Metersは、関連型が何であれ、単純に「2回目の実装」として弾かれます。
これは関連型の制約というより、関連型の設計意図そのものです。Metersのconvert()を呼んだとき、戻り値の型が1通りに決まるからこそ、呼び出す側はConvertとだけ書けばよく、「どのConvertか」を指定せずに済みます。
ジェネリックな型引数との違い
「同じ型に複数の変換先を持たせたい」のであれば、トレイト自体に型引数を持たせます。
trait Convert<T> {
fn convert(&self) -> T;
}
impl Convert<i32> for Meters { /* センチメートル */ }
impl Convert<f64> for Meters { /* キロメートル */ }Convert<i32>とConvert<f64>は別のトレイトとして扱われるので、どちらも実装できます。その代わり、呼び出す側はlet cm: i32 = distance.convert();のように、どちらを使うのかを型で指定する必要があります。
| 観点 | 型引数(trait Convert<T>) |
関連型(type Output) |
|---|---|---|
| 型を決めるのは | 使う側 | 実装側 |
| 同じ型への実装 | 型引数ごとに何度でも | 1回だけ |
| 呼び出し側の指定 | どのTかを書く必要がある |
不要 |
「この型にとっての変換先は1つ」と言い切れるなら関連型、「組み合わせが複数ある」なら型引数、というのが使い分けです。次の問題06で見るAddトレイトは、右辺の型を型引数、結果の型を関連型で表していて、この2つを組み合わせた例になっています。
06 - 演算子オーバーロード
演算子オーバーロードと関連型とトレイトに関する問題です。
次のコードはコンパイルエラー(E0369)になります。Point同士を+で足せるようにして修正してください。
Point { x: 4, y: 6 }#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let sum = p1 + p2;
println!("{:?}", sum);
}Playgroundで開く解答例と解説
use std::ops::Add;
#[derive(Debug)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, rhs: Point) -> Point {
Point {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
fn main() {
let p1 = Point { x: 1, y: 2 };
let p2 = Point { x: 3, y: 4 };
let sum = p1 + p2;
println!("{:?}", sum);
}Playgroundで開くエラーメッセージはcannot add 'Point' to 'Point'(E0369)です。第12章の問題02や第19章の問題03でも出会ったエラーコードで、「この型にはこの演算子が定義されていない」という意味でした。
Rustでは、a + bという式はAddトレイトのaddメソッドの呼び出しとして扱われます。整数や浮動小数点数で+が使えるのは、標準ライブラリがそれらの型にAddを実装しているからです。つまり、自作の型で+を使いたければ、Addトレイトを実装すればよいことになります。これを演算子オーバーロードと呼びます。
Addはstd::opsモジュールにあるので、第14章で学んだuseで持ち込みます。宣言は次のような形です。
trait Add<Rhs = Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}ここに、この章で学んだ2つがそろって出てきます。
type Output;は関連型です。「+の結果の型は実装側で決める」という宣言で、今回はPoint同士を足してPointが返ってくるのでtype Output = Point;としましたRhsは右辺の型を表す型引数で、既定値がSelfです。impl Add for Pointと型引数を省略するとRhs = Pointになり、Point + Pointの意味になります。問題05の表で見た「使う側が決める型引数」と「実装側が決める関連型」の組み合わせです
addの引数が&selfではなくselfである点にも注目してください。+のオペランドは値として渡されるので、p1 + p2を実行した後のp1とp2はムーブされていて使えません。整数でa + bの後もaが使えるのは、整数がCopyだからです。Pointにも#[derive(Clone, Copy)]を付ければ、足した後も元の値を使い続けられます。
07 - 応用: 図形コレクション
トレイトオブジェクトとトレイトとメソッドに関する問題です。
図形を表す3つの構造体にShapeトレイトを実装し、図形の一覧から面積の合計を求めるtotal_area関数を定義して、テストに合格させてください。
// トレイトShapeを定義せよ
// メソッド: area(&self) -> f64 面積を返す
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
struct Triangle {
base: f64,
height: f64,
}
// Circle・Rectangle・TriangleにShapeを実装せよ
// Circle : 半径 × 半径 × 円周率(std::f64::consts::PI)
// Rectangle: 幅 × 高さ
// Triangle : 底辺 × 高さ ÷ 2
// 関数total_areaを定義せよ
// 引数 : 図形の一覧(&[&dyn Shape])
// 戻り値: すべての図形の面積の合計
#[test]
fn test_each_area() {
let circle = Circle { radius: 1.0 };
let rectangle = Rectangle { width: 3.0, height: 4.0 };
let triangle = Triangle { base: 6.0, height: 2.0 };
assert_eq!(circle.area(), std::f64::consts::PI);
assert_eq!(rectangle.area(), 12.0);
assert_eq!(triangle.area(), 6.0);
}
#[test]
fn test_total_area() {
let rectangle = Rectangle { width: 3.0, height: 4.0 };
let triangle = Triangle { base: 6.0, height: 2.0 };
let square = Rectangle { width: 2.0, height: 2.0 };
let shapes: Vec<&dyn Shape> = vec![&rectangle, &triangle, &square];
assert_eq!(total_area(&shapes), 22.0);
}
#[test]
fn test_total_area_empty() {
let shapes: Vec<&dyn Shape> = vec![];
assert_eq!(total_area(&shapes), 0.0);
}Playgroundで開く解答例と解説
// トレイトShapeを定義せよ
// メソッド: area(&self) -> f64 面積を返す
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Rectangle {
width: f64,
height: f64,
}
struct Triangle {
base: f64,
height: f64,
}
// Circle・Rectangle・TriangleにShapeを実装せよ
// Circle : 半径 × 半径 × 円周率(std::f64::consts::PI)
// Rectangle: 幅 × 高さ
// Triangle : 底辺 × 高さ ÷ 2
impl Shape for Circle {
fn area(&self) -> f64 {
self.radius * self.radius * std::f64::consts::PI
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.width * self.height
}
}
impl Shape for Triangle {
fn area(&self) -> f64 {
self.base * self.height / 2.0
}
}
// 関数total_areaを定義せよ
// 引数 : 図形の一覧(&[&dyn Shape])
// 戻り値: すべての図形の面積の合計
fn total_area(shapes: &[&dyn Shape]) -> f64 {
let mut total = 0.0;
for shape in shapes {
total += shape.area();
}
total
}
#[test]
fn test_each_area() {
let circle = Circle { radius: 1.0 };
let rectangle = Rectangle { width: 3.0, height: 4.0 };
let triangle = Triangle { base: 6.0, height: 2.0 };
assert_eq!(circle.area(), std::f64::consts::PI);
assert_eq!(rectangle.area(), 12.0);
assert_eq!(triangle.area(), 6.0);
}
#[test]
fn test_total_area() {
let rectangle = Rectangle { width: 3.0, height: 4.0 };
let triangle = Triangle { base: 6.0, height: 2.0 };
let square = Rectangle { width: 2.0, height: 2.0 };
let shapes: Vec<&dyn Shape> = vec![&rectangle, &triangle, &square];
assert_eq!(total_area(&shapes), 22.0);
}
#[test]
fn test_total_area_empty() {
let shapes: Vec<&dyn Shape> = vec![];
assert_eq!(total_area(&shapes), 0.0);
}Playgroundで開くトレイトオブジェクトの典型的な使い方を、1本の流れで書きました。順に見ていきます。
トレイトは「面積を求められる」という共通点だけを表す
円・長方形・三角形はフィールドも計算式もばらばらですが、「面積を返せる」という1点は共通しています。Shapeトレイトはその1点だけをareaメソッドとして定めていて、それぞれのimpl Shape for ...が型ごとの計算を担当します。第18章の復習です。
&[&dyn Shape]で型の違いを吸収する
total_areaは、円でも長方形でも三角形でも、何種類の図形が何個来ても動く必要があります。Vec<T>や&[T]のTは1つの型にしかなれないので、&dyn Shapeを要素の型にして「Shapeを実装した何か」の並びとして受け取ります。この引数の型をジェネリクスにするとTが1つの型に固定されてしまい、test_total_areaのように長方形と三角形を混ぜた一覧は渡せません。問題03で見た「異なる型の混在」がまさにこの場面です。
引数は&[&dyn Shape]、呼び出しは&shapes
total_areaの引数を&Vec<&dyn Shape>ではなく、第5章で学んだスライス&[&dyn Shape]にしています。テストではVec<&dyn Shape>の参照&shapesを渡していますが、&Vec<T>は必要に応じて&[T]として扱われるので、そのまま渡せます。スライスで受け取っておくと、ベクタだけでなく配列からも呼べる関数になります。
for shape in shapesのshapeは&&dyn Shape
shapesは&[&dyn Shape]なので、forで取り出されるshapeは「&dyn Shapeへの参照」、つまり&&dyn Shapeです。それでもshape.area()とそのまま書けるのは、メソッド呼び出し時に参照が自動的に外されるためです。第10章で&selfのメソッドを値からも参照からも同じように呼べたのと同じ仕組みです。
合計の初期値は0.0
areaがf64を返すので、合計もf64です。第5章の合計ではlet mut total = 0;と整数で始めましたが、ここで0と書くと整数と推論されて+=で型が合わなくなります。0.0と書くことでf64であることが伝わります。