加载中...

内嵌(Embedding)


Go没有提供经典的类型驱动式的派生类概念,但却可以通过内嵌其他类型或接口代码的方式来实现类似的功能。

接口的“内嵌”比较简单。我们之前曾提到过io.Readerio.Writer这两个接口,以下是它们的实现:

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

io包中,还提供了许多其它的接口,它们定义一类可以同时实现几个不同接口的类型。例如io.ReadWriter接口,它同时包含了ReadWrite两个接口。尽管可以通过列出ReadWrite两个方法的详细声明的方式来定义io.ReadWriter接口,但是以内嵌两个已有接口进行定义的方式会使代码显得更加简洁、直观:

// ReadWriter is the interface that combines the Reader and Writer interfaces.
type ReadWriter interface {
    Reader
    Writer
}

这段代码的意义很容易理解:一个ReadWriter类型可以同时完成ReaderWriter的功能,它是这些内嵌接口的联合(这些内嵌接口必须是一组不相干的方法)。接口只能“内嵌”接口类型。

类似的想法也可以应用于结构体的定义,其实现稍稍复杂一些。在bufio包中,有两个结构体类型:bufio.Reader和 bufio.Writer,它们分别实现了io包中的类似接口。bufio包还实现了一个带缓冲的reader/writer类型,实现的方法是将reader和writer组合起来内嵌到一个结构体中:在结构体中,只列出了两种类型,但没有给出对应的字段名。

// ReadWriter stores pointers to a Reader and a Writer.
// It implements io.ReadWriter.
type ReadWriter struct {
    *Reader  // *bufio.Reader
    *Writer  // *bufio.Writer
}

内嵌的元素是指向结构体的指针,因此在使用前,必须将其初始化并指向有效的结构体数据。结构体ReadWriter可以被写作如下形式:

type ReadWriter struct {
    reader *Reader
    writer *Writer
}

为了使各字段对应的方法能满足io的接口规范,我们还需要提供如下的方法:

func (rw *ReadWriter) Read(p []byte) (n int, err error) {
    return rw.reader.Read(p)
}

通过对结构体直接进行“内嵌”,我们避免了一些复杂的记录。所有内嵌类型的方法可以不受约束的使用,换句话说,bufio.ReadWriter类型不仅具有bufio.Readerbufio.Writer两个方法,同时也满足io.Readerio.Writerio.ReadWriter这三个接口。

在“内嵌”和“子类型”两种方法间存在一个重要的区别。当我们内嵌一个类型时,该类型的所有方法会变成外部类型的方法,但是当这些方法被调用时,其接收的参数仍然是内部类型,而非外部类型。在本例中,一个bufio.ReadWriter类型的Read方法被调用时,其效果和调用我们刚刚实现的那个Read方法是一样的,只不过前者接收的参数是ReadWriterreader字段,而不是ReadWriter本身。

“内嵌”还可以用一种更简单的方式表达。下面的例子展示了如何将内嵌字段和一个普通的命名字段同时放在一个结构体定义中。

type Job struct {
    Command string
    *log.Logger
}

现在,Job类型拥有了LogLogf以及*log.Logger的其他所有方法。当然,我们可以给Logger提供一个命名字段,但完全没有必要这样做。现在,当初始化结束后,就可以在Job类型上调用日志记录功能了。

job.Log("starting now...")

Logger是结构体Job的一个常规字段,因此我们可以在Job的构造方法中按通用方式对其进行初始化:

func NewJob(command string, logger *log.Logger) *Job {
    return &Job{command, logger}
}

或者写成下面的形式:

job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}

如果我们需要直接引用一个内嵌的字段,那么将该字段的类型名称省略了包名后,就可以作为字段名使用,正如之前在ReaderWriter结构体的Read方法中实现的那样。可以用job.Logger访问Job类型变量job*log.Logger字段。当需要重新定义Logger的方法时,这种引用方式就变得非常有用了。

func (job *Job) Logf(format string, args ...interface{}) {
    job.Logger.Logf("%q: %s", job.Command, fmt.Sprintf(format, args...))
}

内嵌类型会引入命字冲突,但是解决冲突的方法也很简单。首先,一个名为X的字段或方法可以将其它同名的类型隐藏在更深层的嵌套之中。假设log.Logger中也包含一个名为Command字段或方法,那么可以用JobCommand字段对其访问进行封装。

其次,同名冲突出现在同一嵌套层里通常是错误的;如果结构体Job本来已经包含了一个名为log.Logger的字段或方法,再继续内嵌log.Logger就是不对的。但假设这个重复的名字并没有在定义之外的地方被使用到,就不会造成什么问题。这个限定为在外部进行类型嵌入修改提供了保护;如果新加入的字段和某个内部类型的字段有命名冲突,但该字段名没有被访问过,那么就不会引起任何问题。


还没有评论.