Menu
Coddy logo textTech

インターフェースの充足ルール

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ではポインタから(デリファレンスによって)常に値を取得できますが、値から常にポインタを取得できるとは限らない(その値がアドレス可能ではない可能性がある)ためです。このルールを念頭に置いておくことで、インターフェースを扱う際の紛らわしいコンパイルエラーを防ぐことができます。

challenge icon

チャレンジ

簡単

ポインタレシーバと値レシーバがインターフェースの充足にどのように影響するかを示す設定システムを構築しましょう。レシーバの選択によって、値、ポインタ、またはその両方がインターフェースで使用できるかどうかが決まる型を作成します。

コードは以下の3つのファイルに分けて構成します:

  • config.go: GetValue() stringSetValue(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: 入力から設定の詳細を読み取り、インターフェース充足のルールを実証します:
    1. ReadOnlyConfig を作成し、ProcessConfigポインタ を渡します(SetValue がポインタレシーバを持っているため必須です)。
    2. Setting を作成し、ProcessConfig値を直接 渡します(両方のメソッドが値レシーバを持っているため動作します)。

以下の入力が提供されます:

  • 1行目: ReadOnlyConfig の初期値
  • 2行目: ReadOnlyConfig に設定する新しい値
  • 3行目: Setting の初期値
  • 4行目: Setting に試行する新しい値

ProcessConfig 関数は、以下の形式で出力する必要があります:

Current: [value]
Current: [value after SetValue]

例えば、debugproductionlocalhostremote が与えられた場合、出力は以下のようになります:

Current: debug
Current: production
Current: localhost
Cannot modify
Current: localhost

ReadOnlyConfig は(ポインタを渡したため)実際に値が変更されますが、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
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン