17.4 运算符模式和接口

    运算符由包级别的函数实现,以操作一个或两个参数,并返回一个新对象。函数针对要操作的对象,在专门的包中实现。例如,假设要在包 matrix 中实现矩阵操作,就会包含 Add() 用于矩阵相加,Mult() 用于矩阵相乘,他们都会返回一个矩阵。这两个函数通过包名来调用,因此可以创造出如下形式的表达式:

    如果我们想在这些运算中区分不同类型的矩阵(稀疏或稠密),由于没有函数重载,我们不得不给函数起不同的名称,例如:

    1. func addSparseToDense (a *sparseMatrix, b *denseMatrix) *denseMatrix
    2. func addDenseToDense (a *denseMatrix, b *denseMatrix) *denseMatrix
    3. func addSparseToSparse (a *sparseMatrix, b *sparseMatrix) *sparseMatrix

    这可不怎么优雅,我们能选择的最佳方案是将它们隐藏起来,作为包的私有函数,并暴露单一的 Add() 函数作为公共 API。可以在嵌套的 switch 断言中测试类型,以便在任何支持的参数组合上执行操作:

    然而,更优雅和优选的方案是将运算符作为方法实现,标准库中到处都运用了这种做法。有关 Ryanne Dolan 实现的线性代数包的更详细信息,可以在 找到。

    1. func (a *sparseMatrix) Add(b Matrix) Matrix
    2. func (a *denseMatrix) Add(b Matrix) Matrix

    每个方法都返回一个新对象,成为下一个方法调用的接收者,因此我们可以使用链式调用表达式:

    比上一节面向过程的形式更简洁。

    正确的实现同样可以基于类型,通过 switch 类型断言在运行时确定:

    1. case sparseMatrix:
    2. return addSparseToSparse(a.(sparseMatrix), b.(sparseMatrix))
    3. case denseMatrix:
    4. return addSparseToDense(a.(sparseMatrix), b.(denseMatrix))
    5. default:
    6. // 不支持的参数
    7. }
    8. }

    再次地,这比上一节嵌套的 switch 更简单。

    例如定义一个代数 Algebraic 接口:

    然后为我们的 matrix 类型定义 Add()Min(),,……等方法。

    每种实现上述 Algebraic 接口类型的方法都可以链式调用。每个方法实现都应基于参数类型,使用 switch 类型断言来提供优化过的实现。另外,应该为仅依赖于接口的方法,指定一个默认处理分支:

    1. switch b.(type) {
    2. case sparseMatrix:
    3. return addDenseToSparse(a, b.(sparseMatrix))
    4. default:
    5. for x in range b.Elements()
    6. }
    7. }

    如果一个通用的功能无法仅使用接口方法来实现,你可能正在处理两个不怎么相似的类型,此时应该放弃这种运算符模式。例如,如果 a 是一个集合而 b 是一个矩阵,那么编写 a.Add(b) 没有意义。就集合和矩阵运算而言,很难实现一个通用的 a.Add(b) 方法。遇到这种情况,把包拆分成两个,然后提供单独的 AlgebraicSet 和 接口。