结构组合


结构本身用来组织相关数据,可以将处理结构的相关函数定义为方法,类似面向对象程序语言中,使用类定义值域与方法,那么继承呢?Go 语言并非以面向对象为主要典范的语言,没有继承的概念,不过可以使用组合代替继承。

在组合之前

在〈结构与方法〉中使用struct定义了Account,如果今天你想定义一个支票帐户,方式之一是…

type CheckingAccount struct {
    id string
    name string
    balance float64
    overdraftlimit float64
}

这是个很寻常的作法,也许你想将idnamebalance组织在一起:

package main

import "fmt"

type CheckingAccount struct {
    account struct {
        id      string
        name    string
        balance float64
    }
    overdraftlimit float64
}

func main() {
    checking := CheckingAccount{}
    checking.account = struct {
        id      string
        name    string
        balance float64
    }{"1234-5678", "Justin Lin", 1000}
    checking.overdraftlimit = 30000

    fmt.Println(checking)                // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(checking.account)        // {1234-5678 Justin Lin 1000}
    fmt.Println(checking.account.name)   // Justin Lin
    fmt.Println(checking.overdraftlimit) // 30000
}

这是一种方式,不过使用起来麻烦,或许你可以这么做:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    account        Account
    overdraftlimit float64
}

func main() {
    checking := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}

    fmt.Println(checking)                // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(checking.account)        // {1234-5678 Justin Lin 1000}
    fmt.Println(checking.account.name)   // Justin Lin
    fmt.Println(checking.overdraftlimit) // 300000
}

看来还不错,不过,如果想要fmt.Println(checking.name)就能获取名称的话,这种写法行不通!

结构值域的查找

在定义结构时,可以将另一已定义的结构直接内嵌:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}

    fmt.Println(account)                // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(account.id)             // 1234-5678
    fmt.Println(account.name)           // Justin
    fmt.Println(account.balance)        // 1000
    fmt.Println(account.overdraftlimit) // 30000
}

这称为类型内嵌(type embedding),Account被称为CheckingAccount的内部类型,反之,CheckingAccountAccount的外部类型,虽然是透过account.idaccount.nameaccount.balance来访问,不过内部类型提升,令内部类型定义的值域为可见。

那么,如果想要明确地透过Account的结构来访问呢?也是可以的:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}

    fmt.Println(account)                 // {{1234-5678 Justin Lin 1000} 30000}
    fmt.Println(account.Account.id)      // 1234-5678
    fmt.Println(account.Account.name)    // Justin
    fmt.Println(account.Account.balance) // 1000
    fmt.Println(account.overdraftlimit)  // 30000
}

虽然内部类型会提升,然而,若外部类型中定义了同名值域,就会直接获取外部类型的值域,因此,如果CheckingAccount定义了相同的值域balance,如果透过account.balance,结果会是找到CheckingAccount定义的balance,如果想明确找到Accountbalance,可以指定Account作为前置:

package main

import "fmt"

type Account struct {
    id      string
    name    string
    balance float64
}

type CheckingAccount struct {
    Account
    balance        float64
    overdraftlimit float64
}

func main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 2000, 30000}

    fmt.Println(account.balance)         // 2000
    fmt.Println(account.Account.balance) // 1000
}

无论是结构值域或是方法,若来自两个结构的值域或方法产生了同名冲突,Go 会有 ambiguous selector 的错误提示,此时你必须明确指定结构名称,指定使用来自哪个结构的值域或方法。

方法的查找

如果内部类型原本定义了方法,这些方法也是查找时的对象:

package main

import (
    "errors"
    "fmt"
)

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac *Account) Deposit(amount float64) {
    if amount <= 0 {
        panic("必须存入正数")
    }
    ac.balance += amount
}

func (ac *Account) Withdraw(amount float64) error {
    if amount > ac.balance {
        return errors.New("余额不足")
    }
    ac.balance -= amount
    return nil
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    account.Deposit(2000)
    account.Withdraw(500)
    fmt.Println(account) // {{1234-5678 Justin Lin 2500} 30000}
}

类似地,若外部类型中定义了同名的方法,那么就会使用该方法,这类似重新定义(Override)的概念:

package main

import (
    "errors"
    "fmt"
)

type Account struct {
    id      string
    name    string
    balance float64
}

func (ac *Account) Deposit(amount float64) {
    if amount <= 0 {
        panic("必须存入正数")
    }
    ac.balance += amount
}

func (ac *Account) Withdraw(amount float64) error {
    if amount > ac.balance {
        return errors.New("余额不足")
    }
    ac.balance -= amount
    return nil
}

type CheckingAccount struct {
    Account
    overdraftlimit float64
}

func (ac *CheckingAccount) Withdraw(amount float64) error {
    if amount > ac.balance+ac.overdraftlimit {
        return errors.New("超出信用额度")
    }
    ac.balance -= amount
    return nil
}

func main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    account.Deposit(2000)
    if err := account.Withdraw(50000); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(account)
    }
}

在上面的范例中,会显示「超出信用额度」的消息,拿掉func (account *CheckingAccount) Withdraw(amount float64)该函数的定义,则会显示「余额不足」的消息。

如果想指定使用AccountWithdraw函数,也还是可以的:

func main() {
    account := CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}
    account.Deposit(2000)
    if err := account.Account.Withdraw(50000); err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(account)
    }
}

虽然可以实现方法重新定义的概念,不过,单纯只是如上定义的话,并不支持多态的概念,因为一开始这么指定就会出错了:

// cannot use CheckingAccount literal (type CheckingAccount) as type Account in assignment
var account Account = CheckingAccount{Account{"1234-5678", "Justin Lin", 1000}, 30000}

若想实现出多态的概念,必须使用interface,这在之后的文件会加以说明。


展开阅读全文