Files
the-way-to-go_ZH_CN/eBook/17.4.md
marjune 9be772e9bb Add chapter 17.x (#697)
* improve chapter 15.12

* add chapter 17.2

* add chapter 17.3

* add chapter 17.4
2019-07-27 19:26:44 -07:00

110 lines
4.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 17.4 运算符模式和接口
运算符是一元或二元函数,它返回一个新对象而不修改其参数,类似 C++ 中的 `+``*`,特殊的中缀运算符(`+``-``*`可以被重载以支持类似数学运算的语法。但除了一些特殊情况Go 语言并不支持运算符重载:为了克服该限制,运算符必须由函数来模拟。既然 Go 同时支持面向过程和面向对象编程,我们有两种选择:
## 17.4.1 函数作为运算符
运算符由包级别的函数实现,以操作一个或两个参数,并返回一个新对象。函数针对要操作的对象,在专门的包中实现。例如,假设要在包 `matrix` 中实现矩阵操作,就会包含 `Add()` 用于矩阵相加,`Mult()` 用于矩阵相乘,他们都会返回一个矩阵。这两个函数通过包名来调用,因此可以创造出如下形式的表达式:
```go
m := matrix.Add(m1, matrix.Mult(m2, m3))
```
如果我们想在这些运算中区分不同类型的矩阵(稀疏或稠密),由于没有函数重载,我们不得不给函数起不同的名称,例如:
```go
func addSparseToDense (a *sparseMatrix, b *denseMatrix) *denseMatrix
func addDenseToDense (a *denseMatrix, b *denseMatrix) *denseMatrix
func addSparseToSparse (a *sparseMatrix, b *sparseMatrix) *sparseMatrix
```
这可不怎么优雅,我们能选择的最佳方案是将它们隐藏起来,作为包的私有函数,并暴露单一的 `Add()` 函数作为公共 API。可以在嵌套的 `switch` 断言中测试类型,以便在任何支持的参数组合上执行操作:
```go
func Add(a Matrix, b Matrix) Matrix {
switch a.(type) {
case sparseMatrix:
switch b.(type) {
case sparseMatrix:
return addSparseToSparse(a.(sparseMatrix), b.(sparseMatrix))
case denseMatrix:
return addSparseToDense(a.(sparseMatrix), b.(denseMatrix))
}
default:
// 不支持的参数
}
}
```
然而,更优雅和优选的方案是将运算符作为方法实现,标准库中到处都运用了这种做法。有关 Ryanne Dolan 实现的线性代数包的更详细信息,可以在 https://github.com/skelterjohn/go.matrix 找到。
## 17.4.2 方法作为运算符
根据接收者类型不同,可以区分不同的方法。因此我们可以为每种类型简单地定义 `Add` 方法,来代替使用多个函数名称:
```go
func (a *sparseMatrix) Add(b Matrix) Matrix
func (a *denseMatrix) Add(b Matrix) Matrix
```
每个方法都返回一个新对象,成为下一个方法调用的接收者,因此我们可以使用*链式调用*表达式:
```go
m := m1.Mult(m2).Add(m3)
```
比上一节面向过程的形式更简洁。
正确的实现同样可以基于类型,通过 `switch` 类型断言在运行时确定:
```go
func (a *sparseMatrix) Add(b Matrix) Matrix {
switch b.(type) {
case sparseMatrix:
return addSparseToSparse(a.(sparseMatrix), b.(sparseMatrix))
case denseMatrix:
return addSparseToDense(a.(sparseMatrix), b.(denseMatrix))
default:
// 不支持的参数
}
}
```
再次地,这比上一节嵌套的 `switch` 更简单。
## 17.4.3 使用接口
当在不同类型上执行相同的方法时,创建一个通用化的接口以实现多态的想法,就会自然产生。
例如定义一个代数 `Algebraic` 接口:
```go
type Algebraic interface {
Add(b Algebraic) Algebraic
Min(b Algebraic) Algebraic
Mult(b Algebraic) Algebraic
Elements()
}
```
然后为我们的 `matrix` 类型定义 `Add()``Min()``Mult()`,……等方法。
每种实现上述 `Algebraic` 接口类型的方法都可以链式调用。每个方法实现都应基于参数类型,使用 `switch` 类型断言来提供优化过的实现。另外,应该为仅依赖于接口的方法,指定一个默认处理分支:
```go
func (a *denseMatrix) Add(b Algebraic) Algebraic {
switch b.(type) {
case sparseMatrix:
return addDenseToSparse(a, b.(sparseMatrix))
default:
for x in range b.Elements()
}
}
```
如果一个通用的功能无法仅使用接口方法来实现,你可能正在处理两个不怎么相似的类型,此时应该放弃这种运算符模式。例如,如果 `a` 是一个集合而 `b` 是一个矩阵,那么编写 `a.Add(b)` 没有意义。就集合和矩阵运算而言,很难实现一个通用的 `a.Add(b)` 方法。遇到这种情况,把包拆分成两个,然后提供单独的 `AlgebraicSet``AlgebraicMatrix` 接口。
## 链接
- [目录](directory.md)
- 上一节:[可见性模式](17.3.md)
- 下一章:[出于性能考虑的实用代码片段](18.0.md)