Menu
Coddy logo textTech

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

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部。レッスン 43/107。

Go は暗黙的なインターフェース実装にダックタイピングを使用しますが、型が実際にインターフェースを満たすかどうかを決定する特定のルールがあります。これらのルールを理解すると、見つけにくいバグを回避できます。

最も重要なルールはpointer receiverに関するものです。メソッドが pointer receiver で定義されている場合、その型への pointer だけが interface を満たし、その値自体は満たしません。

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)  // コンパイルエラー - 値はインターフェースを満たさない
}

ただし、逆の場合のほうが柔軟です。メソッドが値レシーバーを持つ場合、値型とポインタ型の both がインターフェースを満たします。Go は値レシーバーメソッドを呼び出す際に、ポインタを自動的にデリファレンスします。

func (d Document) Info() string {  // 値レシーバー
    return d.Name
}

// Document と *Document の両方が Info() を要求するインターフェースを満たします

この非対称性が存在するのは、Go ではポインターを逆参照することで常にポインターから値を取得できますが、値からポインターを常に取得できるとは限らないためです(値はアドレス指定可能とは限りません)。このルールを覚えておくと、インターフェースを扱う際に混乱を招くコンパイラーエラーを防げます。

challenge icon

チャレンジ

簡単

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

コードを3つのファイルに分けて整理します。

  • config.go: 2つのメソッド、GetValue() stringSetValue(string)を必要とするConfigurableインターフェースをDefineします。次に、2つの設定型を作成します。
    • ReadOnlyConfigにはValueフィールドを持たせます。GetValue()には値レシーバを使用し(Valueを返す)、SetValue()にはポインタレシーバを使用します(Valueを更新する)
    • SettingにはDataフィールドを持たせます。両方のメソッドに値レシーバを使用します(GetValueはDataを返し、SetValueは何も変更せずに"Cannot modify"を表示するだけです)
  • processor.go: Configurableと新しい値の文字列をacceptsするProcessConfigという名前のfunctionをCreateします。GetValue()を使用して現在の値を表示し、新しい値を指定してSetValue()をCallし、その後、changesを示すために値をもう一度表示します。
  • main.go: 入力から設定の詳細を読み取り、インターフェースの実装ルールを示します。
    1. ReadOnlyConfigをCreateし、ProcessConfigポインタを渡します(SetValueがポインタレシーバを持つため必要です)
    2. SettingをCreateし、ProcessConfig値を直接渡します(両方のメソッドが値レシーバを持つため機能します)

次の入力が提供されます。

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

ProcessConfigfunctionは、次のformatで表示する必要があります。

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

たとえば、debugproductionlocalhostremoteが与えられた場合、出力は次のようになります。

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

ReadOnlyConfigは実際に値がchangesすることに注目してください(ポインタを渡したためです)。一方、Settingは変更されません(値レシーバを持つそのSetValueでは元の値をmodifyできないためです)。重要な点は、ReadOnlyConfigの値だけではConfigurableを満たさず、ポインタだけが満たすということです。一方、Settingのすべてのメソッドは値レシーバを使用しているため、Settingの値は直接使用できます。

自分で試してみよう

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腕試し

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

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

自分で練習してみよう: Goオンラインコンパイラ