実践Go言語(Effective Go)の翻訳、11回目です。
前回までの訳は実践Go言語[日本語訳]にまとめてあります。
埋込み
Go言語には型によるサブクラス化という典型的な概念はありませんが、構造体またはインタフェースに型を埋込み、実装を「借りる」仕組みがあります。
インタフェースの埋込みはとても単純です。下は以前説明したio.Readerとio.Writerインタフェースの定義です。
type Reader interface {
Read(p []byte) (n int, err os.Error)
}
type Writer interface {
Write(p []byte) (n int, err os.Error)
}
ioパッケージではこれと同じように、オブジェクトに対しメソッドの実装を規定するためのインタフェースが、他にもいくつかエクスポートされています。たとえば、ReadとWrite両方を持つインタフェースio.ReadWriterがあります。これら2つのメソッドを明示的に記述することでio.ReadWriterを定義することもできますが、次のようにして2つのインタフェースを埋込んで新しいインタフェースを作成するほうがより簡単で、意図が伝わりやすくなります。
// ReadWriteは、基本的なメソッドReadとWriteをグルーピングしたインタフェース
type ReadWriter interface {
Reader
Writer
}
このようにすることで、Readerが行えること、およびWriterが行えることをReadWriterが兼ね備えていて、このインタフェースが埋込みインタフェース(メソッド群内に共通のメソッドを持っていてはならない)の結合により作られていることが見て取れます。ただしインタフェース内に埋込むことができるのはインタフェースだけです。
この基本的な考え方は構造体にもあてはまりますが、構造体の場合はより広範囲に渡る影響があります。bufioパッケージは2つの構造体型、bufio.Readerとbufio.Writerを持ち、当然それぞれパッケージioの対応するインタフェースを実装しています。bufioではさらに埋込みを利用して、ひとつの構造体にReaderとWriterを組み込んでバッファ付きの読み書きを実装しています。下の例で構造体内に型を列挙していますが、このときフィールド名は付けていません。
// ReadWriterは、ReaderとWriterのポインタを格納している
// これはio.ReadWriterの実装
type ReadWriter struct {
*Reader
*Writer
}
この構造体はこう書き換えることも可能です。
type ReadWriter struct {
reader *Reader
writer *Writer
}
ただこうした場合は、下のようにして呼び出しを転送するメソッドを用意し、フィールド内の各メソッドを実装しioインタフェースを満たすようにしなければなりません。
func (rw *ReadWriter) Read(p []byte) (n int, err os.Error) {
return rw.reader.Read(p)
}
しかし、直接構造体を埋込んでしまえば、この冗長な記述は要らなくなります。埋込まれた型が持っているメソッドは自動で持ち上げられるため、すなわちbufio.ReadWriterはbufio.Readerと bufio.Writerのメソッドを持つだけでなく、3つのインタフェース(io.Reader、io.Writer、io.ReadWriter)の全てを満たすようになります。
埋込みとサブクラス化では大きな違いがあります。型を埋込んでいるとき、埋込んだ型が持っているメソッドは埋込み先のメソッドともなりますが、実行しているメソッドのレシーバは埋込み先の型ではなく、あくまで元の型です。サンプルコードの bufio.ReadWriterのReadメソッドが実行されることと、先程の転送メソッドが実行されることは結果としてまったく同じです。(レシーバはReadWriterフィールドのreaderで、ReadWriter自身ではありません。)
また、埋込みを使うことで少し扱いやすくもなります。下の例では、通常の名前付きフィールドと並んで、埋込みフィールドを記述しています。
type Job struct {
Command string
*log.Logger
}
これでJob型は、log.Logger型が持つLog、Logfといったメソッドを持つようになりました。もちろん、Loggerにフィールド名を与えることはできますが、それは必須ではありません。これで、Jobを使ってログが記録できるようになりました。
job.Log("starting now...")
このLoggerは普通の構造体フィールドであるため、いままで通りの方法で初期化が行えます。
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
埋込まれているフィールドを直接参照しなければならないときは、フィールドの型名(パッケージ名は不要)をフィールド名として用います。つまりJob型である変数jobの*log.Loggerにアクセスしたいときはjob.Loggerと書きます。これはLoggerのメソッドに手を加えたいときに役立ちます。
func (job *Job) Logf(format string, args ...) {
job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args))
}
型を埋込むことで名前の競合が発生する恐れがありますが、その解決ルールは単純です。最初に、フィールドまたはメソッドXは、その他の、より深い入れ子内にあるXを隠蔽します。たとえばlog.LoggerにCommandと名づけられたフィールドまたはメソッドが含まれていても、JobのCommandフィールドがそれより優先されます。
2番目として、同一の入れ子階層に同じ名前が現れたとき、通常ではエラーとなります。たとえばJob構造体に、Loggerと名づけられた別のフィールドまたはメソッドが含まれているときlog.Loggerの埋め込みは不正です。ただし、このとき重複した名前が、型定義より外側のプログラムから一切アクセスされなければ大丈夫です。この決まりによって、埋め込まれた型へ外部から変更を加えてもある程度保護されます。つまり、フィールドの追加で他の型が持つフィールドとかち合ってしまっても、どちらのフィールドも一切使用されることがなければ何ら問題ありません。
Trackback URL
Leave a comment
Comments