Polymorphism 多态性
在了解多态之前我们先来了解一下什么是多态。
same name many forms
在编程环境中,我们还遇到了几个用例,其中不同上下文中的行为相似。这种行为最好用同一个名字来表示。这就是多态性在编程环境中发挥作用的地方。从编程的角度来看,有两种可能的多态性
- 编译时多态性——在这种类型的多态性中,编译器能够知道特定调用将执行哪些确切函数。编译时多态性的一些例子是
- 函数重载——相同的函数名具有不同的参数
- 运算符重载
- 运行时多态性——在这种类型的多态性中,要调用的函数是在运行时决定的。
让我们看看 GO 中可以实现什么类型的编译和运行时多态性
Compile Time Polymorphism in Go (Golang)
在编译时多态性中,调用是在编译时由编译器解析的。编译时多态性的一些形式是
- 方法/函数重载: 存在多个具有相同名称但具有不同签名或可能不同返回类型的方法/函数
- 运算符重载: 相同的运算符用于操作不同的数据类型
Go 不支持方法重载。例如,请看下面的程序,它演示了 go 不支持方法重载。
package main
type maths struct{}
func (m *maths) add(a, b int) int {
return a + b
}
func (m *maths) add(a, b, c int) int {
return a + b + c
}
func main() {
m := &maths{}
}
Output:
(*maths).add redeclared in this block
previous declaration at ./main.go:5:6
Go 也不支持运算符重载。原因在 go 的常见问题解答中说明。
如果方法分派也不需要进行类型匹配,那么它就会被简化。其他语言的经验告诉我们,使用具有相同名称但不同签名的多种方法有时是有用的,但在实践中也可能会造成混乱和脆弱。仅按名称匹配并要求类型一致是 Go 类型系统中的一个重大简化决策。
关于运算符重载,它似乎更多的是一种方便,而不是绝对的要求。同样,如果没有它,事情会变得更简单。
现在的问题是有没有其他方法可以在 GO 中进行方法重载。这就是 Go 中的 Variadic 参数(可变参数)发挥作用的地方。见下面的程序
package main
import "fmt"
type maths struct{}
func (m *maths) add(numbers ...int) int {
result := 0
for _, num := range numbers {
result += num
}
return result
}
func main() {
m := &maths{}
fmt.Printf("Result: %d\n", m.add(2, 3))
fmt.Printf("Result: %d\n", m.add(2, 3, 4))
}
Output:
Result: 5
Result: 9
Go 不直接支持方法/函数/运算符重载,但可变参数函数提供了一种通过增加代码复杂性来实现相同目的的方法
Runtime Polymorphism in Go (Golang)
运行时多态性意味着调用在运行时解析。它是在GO中使用接口实现的。
让我们通过一个例子来理解它。不同的国家有不同的税收计算方式。这可以通过接口来表示。
type taxCalculator interface {
calculatorTax()
}
现在,不同的国家可以拥有自己的结构,并实现 calculateTax() 方法。例如,印度的税收结构可以表示如下。它还可以定义一个方法 calculateTax(),根据百分比进行实际计算。
同样,其他国家的税收系统也可以用结构表示,它们也可以实现自己的culateTax()方法来给出自己的税值。
现在,让我们看看如何使用这个 taxCalcuator 界面来计算一个在不同国家定居的人在一年中不同时间的税款。请参考下面的完整程序。
package main
import "fmt"
type taxSystem interface {
calculateTax() int
}
type indianTax struct {
taxPercentage int
income int
}
func (i *indianTax) calculateTax() int {
tax := i.income * i.taxPercentage / 100
}
type singaporeTax struct {
taxPercentage int
income int
}
func (i *singaporeTax) calculateTax() int {
tax := i.income * i.taxPercentage / 100
return tax
}
type usaTax struct {
taxPercentage int
income int
}
func (i *usaTax) calculateTax() int {
tax := i.income * i.taxPercentage / 100
return tax
}
func main() {
indianTax := &indianTax {
taxPercentage: 30,
income: 1000,
}
signaporeTax := &signaporeTax {
taxPercentage: 10,
income: 2000,
}
taxSystems := []taxSystem{indianTax, singaporeTax}
totalTax := calculateTotalTax(taxSystems)
fmt.Sprintf("Total tax is %d\n", totalTax)
}
func calculateTotalTax(taxSystems []taxSytsem) int {
totalTax := 0
for _, t := range taxSystem {
totalTax += t.calculateTax() //This is where runtime polymorphism happens
}
return totalTax
}
Output:
Total Tax is 300
下面是运行时多态性发生所在的那行代码:
totalTax += t.calculateTax() //This is where runtime polymorphism happens
在不同的上下文中会使用相同的 calculateTax 来计算税款。当编译器看到这个调用时,它会延迟运行时调用的具体方法。这个神奇的过程是在幕后进行的。
Extension to add more tax systems
现在,让我们扩展上述程序,将美国国家的税收制度也包括在内
type usaTax struct {
taxPercentage int
income int
}
func (i *usaTax) calculateTax() int {
tax := i.income * i.taxPercentage / 100
return tax
}
我们只需要改变我们的 main 函数,增加美国税收制度。
func main() {
indianTax := &indianTax{
taxPercentage: 30,
income: 1000,
}
singaporeTax := &singaporeTax{
taxPercentage: 10,
income: 2000,
}
usaTax := &usaTax{
taxPercentage: 40,
income: 500,
}
taxSystems := []taxSystem{indianTax, singaporeTax, usaTax}
totalTax := calculateTotalTax(taxSystems)
fmt.Printf("Total Tax is %d\n", totalTax)
}
func calculateTotalTax(taxSystems []taxSystem) int {
totalTax := 0
for _, t := range taxSystems {
totalTax += t.calculateTax()
}
return totalTax
}
Output:
Total Tax is 700
请注意:在上面的程序扩展时, calculateTotalTax 函数适应美国税收系统而改变,这是接口和多态性的好处。
符合开闭原则,对扩展开放,对修改关闭
Golang 中的函数/方法重载(替代方案/解决方法)
函数/方法重载意味着相同的函数/方法名称可以使用不同数量和类型的参数
看看这篇文章,你会发现 Go 中函数和方法的不同之处
Eg.
func X()
func X(name string)
func X(name, address string)
func X(name string, age int)
在 GO 中,我们可以通过使用
- Variadic Function(可变参数函数):可变参数函数是接受可变数量参数的函数
- Empty Interface(空接口):它是一个没有任何方法的接口(意味着 golang 中所有的类型都实现了该接口)。
方法/函数重载有两种情况
1. 参数数量不同但类型相同
使用可变参数函数可以轻松处理上述情况。请注意,在下面的代码中,参数是一种类型,即 int。
package main
import "fmt"
func main() {
fmt.Println(add(1, 2))
fmt.Println(add(1, 2, 3))
fmt.Println(add(1, 2, 3, 4))
}
func add(numbers ...int) int {
sum := 0
for _, num := range numbers {
sum += num
}
return sum
}
Output:
3
6
10
2. 不同数量和不同类型的参数
这种情况可以使用可变参数函数和空接口来处理
package main
import (
"fmt"
)
func main() {
handle(1, "abc")
handle("abc", "xyz", 3)
handle(1, 2, 3, 4)
}
func handle(params ...interface{}) {
fmt.Println("handle func called with parameters:")
for _, param := range params {
fmt.Printf("%v\n", param)
}
}
Output:
Handle func called with parameters:
1
abc
Handle func called with parameters:
abc
xyz
3
Handle func called with parameters:
1
2
3
4
我们还可以使用 switch case 来获取准确的参数并相应地使用它们。请参阅下面的示例。
package main
import (
"fmt"
)
type person struct {
name string
gender string
age int
}
func main() {
err := addPerson("Tina", "Female", 20)
if err != nil {
fmt.Println("PersonAdd Error: " + err.Error())
}
err = addPerson("John", "Male")
if err != nil {
fmt.Println("PersonAdd Error: " + err.Error())
}
err = addPerson("Wick", 2, 3)
if err != nil {
fmt.Println("PersonAdd Error: " + err.Error())
}
}
func addPerson(args ...interface{}) error {
if len(args) > 3 {
return fmt.Errorf("Wrong number of arguments passed")
}
p := &person{}
// 0 is name 1 is gender 2 is age
for i, arg := range args {
switch i {
case 0: // name
name, ok := arg.(string)
if !ok {
return fmt.Errorf("Name is not passed as string")
}
p.name = name
case 1: // gender
gener, ok := arg.(string)
if !ok {
return fmt.Errorf("Gender is not passed as string")
}
p.gender = gender
case 2: // age
age, ok := args.(int)
if !ok {
return fmt.Errorf("Age is not passed as int")
}
p.age = age
default:
return fmt.Errorf("Wrong parameters passed")
}
}
fmt.Printf("Person struct is %+v\n", p)
return nil
}
注:未传递 arg 的地方将作为默认值替代。
Output:
Person struct is &{name:Tina gender:Female age:20}
Person struct is &{name:John gender:Male age:0}
PersonAdd Error: Gender is not passed as string
GO 中方法和函数的区别
方法和函数之间有一些重要的区别。让我们看看两者的签名
Function
func some_func_name(arguments) return_values
Method
func (receiver receiver_type) some_func_name(arguments) return_values
从上面的签名可以看出,方法有一个接收器参数。接收器可以是结构体或任何其他类型。
方法可以访问接收器的属性,也可以调用接收器的其他方法。
这是函数和方法之间唯一的区别,但也正因为如此,它们提供的功能有所不同。
- 函数可以用作第一级对象,并且可以传递,而方法不能。
- 方法可用于在接收器上进行链式传输,而函数则不能。
- 对于不同的接收方,可以存在同名的不同方法,但在同一个包中不能存在两个同名的不同函数。