インターフェースの充足ルール
CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 43/107。
Goは、暗黙的なインターフェースの充足にダックタイピングを使用していますが、型が実際にインターフェースを満たしているかどうかを決定する特定のルールがあります。これらのルールを理解することで、微妙なバグを回避できます。
最も重要なルールは、ポインタレシーバに関するものです。メソッドがポインタレシーバで定義されている場合、その型へのポインタのみがインターフェースを満たし、値そのものは満たしません。
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // ポインタレシーバ
return "Saved: " + d.Name
}
func Process(s Saver) {
fmt.Println(s.Save())
}
Save() はポインタレシーバを持っているため、*Document だけが Saver を満たします:
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // 動作する - ポインタはインターフェースを満たす
// Process(doc) // コンパイルエラー - 値はインターフェースを満たさない
}
しかし、その逆はより柔軟です。メソッドが値レシーバを持つ場合、値型とポインタ型の両方がインターフェースを満たします。Goは、値レシーバメソッドを呼び出す際にポインタを自動的にデリファレンスします:
func (d Document) Info() string { // 値レシーバ
return d.Name
}
// Document と *Document の両方が、Info() を必要とするインターフェースを満たします
この非対称性が存在するのは、Goではポインタから(デリファレンスによって)常に値を取得できますが、値から常にポインタを取得できるとは限らない(その値がアドレス可能ではない可能性がある)ためです。このルールを念頭に置いておくことで、インターフェースを扱う際の紛らわしいコンパイルエラーを防ぐことができます。
チャレンジ
簡単ポインタレシーバと値レシーバがインターフェースの充足にどのように影響するかを示す設定システムを構築しましょう。レシーバの選択によって、値、ポインタ、またはその両方がインターフェースで使用できるかどうかが決まる型を作成します。
コードは以下の3つのファイルに分けて構成します:
config.go:GetValue() stringとSetValue(string)の2つのメソッドを必要とするConfigurableインターフェースを定義します。次に、2つの設定型を作成します:ReadOnlyConfig(Valueフィールドを持つ):GetValue()には 値レシーバ を使用し(Valueを返す)、SetValue()には ポインタレシーバ を使用します(Valueを更新する)。Setting(Dataフィールドを持つ): 両方のメソッドに 値レシーバ を使用します(GetValueはDataを返し、SetValueは何も変更せずに "Cannot modify" と出力するだけです)。
processor.go:Configurableと新しい値の文字列を受け取るProcessConfigという関数を作成します。この関数は、GetValue()を使用して現在の値を表示し、新しい値でSetValue()を呼び出し、その後、変更内容を示すためにもう一度値を表示する必要があります。main.go: 入力から設定の詳細を読み取り、インターフェース充足のルールを実証します:ReadOnlyConfigを作成し、ProcessConfigに ポインタ を渡します(SetValueがポインタレシーバを持っているため必須です)。Settingを作成し、ProcessConfigに 値を直接 渡します(両方のメソッドが値レシーバを持っているため動作します)。
以下の入力が提供されます:
- 1行目: ReadOnlyConfig の初期値
- 2行目: ReadOnlyConfig に設定する新しい値
- 3行目: Setting の初期値
- 4行目: Setting に試行する新しい値
ProcessConfig 関数は、以下の形式で出力する必要があります:
Current: [value]
Current: [value after SetValue]例えば、debug、production、localhost、remote が与えられた場合、出力は以下のようになります:
Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhostReadOnlyConfig は(ポインタを渡したため)実際に値が変更されますが、Setting は変更されない(値レシーバを持つ SetValue は元の値を変更できない)ことに注目してください。重要な洞察は、ReadOnlyConfig の値だけでは Configurable を満たさず、ポインタのみが満たすのに対し、Setting の値はすべてのメソッドが値レシーバを使用しているため直接動作するという点です。
チートシート
Goは暗黙的なインターフェース満足のためにダックタイピングを使用しますが、ポインタレシーバと値レシーバに関しては特定のルールがあります:
ポインタレシーバルール
メソッドがポインタレシーバを持つ場合、その型へのポインタのみがインターフェースを満たします:
type Saver interface {
Save() string
}
type Document struct{ Name string }
func (d *Document) Save() string { // ポインタレシーバ
return "Saved: " + d.Name
}
func main() {
doc := Document{Name: "report.txt"}
Process(&doc) // 動作する - ポインタはインターフェースを満たす
// Process(doc) // コンパイルエラー - 値はインターフェースを満たさない
}値レシーバルール
メソッドが値レシーバを持つ場合、値型とポインタ型の両方がインターフェースを満たします:
func (d Document) Info() string { // 値レシーバ
return d.Name
}
// Documentと*Documentの両方がInfo()を必要とするインターフェースを満たすなぜこの非対称性が存在するのか
Goは(デリファレンスによって)ポインタから値を常に取得できますが、値からポインタを常に取得できるとは限りません(値がアドレス可能でない場合があるため)。
自分で試してみよう
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 入力を読み込む
scanner.Scan()
rocInitial := scanner.Text()
scanner.Scan()
rocNew := scanner.Text()
scanner.Scan()
settingInitial := scanner.Text()
scanner.Scan()
settingNew := scanner.Text()
// TODO: rocInitial の値を使用して ReadOnlyConfig を作成する
// TODO: ProcessConfig にポインタを渡す (SetValue がポインタレシーバを持つため必要)
// TODO: settingInitial の値を使用して Setting を作成する
// TODO: ProcessConfig に値を直接渡す (両方のメソッドが値レシーバを持つため動作する)
// 未使用変数エラーを避けるためにこれらの変数を使用する
_ = rocInitial
_ = rocNew
_ = settingInitial
_ = settingNew
}
このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。
オブジェクト指向プログラミングのすべてのレッスン
1Go オブジェクト指向の基礎
外部ファイルGo ワークスペースとモジュールパッケージとインポート公開された名前と非公開の名前Go におけるオブジェクト指向入門クラスとしての構造体構造体へのメソッド定義ポインタレシーバと値レシーバ構造体の初期化コンストラクタ関数まとめ:簡易計算機8エラー処理とOOP
error インターフェースカスタムエラー型エラーラッピング (fmt.Errorf)センチネルエラーerrors.Is() と errors.As()Panic、Defer、Recover復習 - ファイルパーサー