Swift基础知识相关(三) —— 重载自定义运算符(一)

版本记录

版本号 时间
V1.0 2019.08.13 星期二

前言

这个专题我们就一起看一下Swfit相关的基础知识。感兴趣的可以看上面几篇。
1. Swift基础知识相关(一) —— 泛型(一)
2. Swift基础知识相关(二) —— 编码和解码(一)

开始

首先看下主要内容

主要内容:在本Swift教程中,您将学习如何创建自定义运算符,重载现有运算符以及设置运算符优先级。

接着,看下写作环境

Swift 5, iOS 13, Xcode 11

运算符是任何编程语言的核心构建块。你能想象编程而不使用+=吗?

运算符非常基础,大多数语言都将它们作为编译器(或解释器)的一部分。另一方面,Swift编译器并不对大多数操作符进行硬编码,而是为库提供了创建自己的操作符的方法。它将工作留给了Swift标准库(Swift Standard Library),以提供您期望的所有常见标准库。这种差异是微妙的,但为巨大的定制潜力打开了大门。

Swift运算符特别强大,因为您可以通过两种方式更改它们以满足您的需求:为现有运算符分配新功能(称为运算符重载 operator overloading),以及创建新的自定义运算符。

在本教程中,您将使用一个简单的Vector结构体并构建自己的一组运算符,以帮助组合不同的向量。

打开Xcode,然后转到File▶New▶Playground创建一个新playground。选择Blank模板并命名您的playgroundCustomOperators。删除所有默认代码,以便您可以从空白平板开始。

将以下代码添加到您的playground

struct Vector {
  let x: Int
  let y: Int
  let z: Int
}

extension Vector: ExpressibleByArrayLiteral {
  init(arrayLiteral: Int...) {
    assert(arrayLiteral.count == 3, "Must initialize vector with 3 values.")
    self.x = arrayLiteral[0]
    self.y = arrayLiteral[1]
    self.z = arrayLiteral[2]
  }
}

extension Vector: CustomStringConvertible {
  var description: String {
    return "(\(x), \(y), \(z))"
  }
}

在这里,您可以定义一个新的Vector类型,其中三个属性符合两个协议。 CustomStringConvertible协议和description计算属性允许您打印Vector的友好字符串表示。

playground的底部,添加以下行:

let vectorA: Vector = [1, 3, 2]
let vectorB = [-2, 5, 1] as Vector

你刚刚用简单的数组创建了两个向量Vectors,没有初始化器!那是怎么发生的?

ExpressibleByArrayLiteral协议提供无摩擦的接口来初始化Vector。该协议需要一个具有可变参数的不可用初始化程序:init(arrayLiteral:Int ...)

可变参数arrayLiteral允许您传入由逗号分隔的无限数量的值。例如,您可以创建Vector,例如Vector(arrayLiteral:0)Vector(arrayLiteral:5,4,3)

该协议进一步方便,并允许您直接使用数组进行初始化,只要您明确定义类型,这是您为vectorAvectorB所做的。

这种方法的唯一警告是你必须接受任何长度的数组。如果您将此代码放入应用程序中,请记住,如果传入长度不是三的数组,它将会崩溃。如果您尝试初始化少于或多于三个值的Vector,则初始化程序顶部的断言assert将在开发和内部测试期间在控制台中提醒您。

单独的矢量Vectors很好,但如果你能用它们做事情会更好。正如你在小学时所做的那样,你将从加法开始你的学习之旅。


Overloading the Addition Operator

运算符重载的一个简单示例是加法运算符。 如果您将它与两个数字一起使用,则会发生以下情况:

1 + 1 // 2

但是,如果对字符串使用相同的加法运算符,则它具有完全不同的行为:

"1" + "1" // "11"

+与两个整数一起使用时,它会以算术形式添加它们。 但是当它与两个字符串一起使用时,它会将它们连接起来。

为了使运算符重载,您必须实现一个名称为运算符符号的函数。

注意:您可以将重载函数定义为类型的成员,这是您将在本教程中执行的操作。 这样做时,必须将其声明为静态static,以便可以在没有定义它的类型的实例的情况下访问它。

playground的尾部添加以下代码:

// MARK: - Operators
extension Vector {
  static func + (left: Vector, right: Vector) -> Vector {
    return [
      left.x + right.x,
      left.y + right.y,
      left.z + right.z
    ]
  }
}

此函数将两个向量作为参数,并将它们的和作为新向量返回。 要做矢量加法,只需相加其各个组件即可。

要测试此功能,请将以下内容添加到playground的底部:

vectorA + vectorB // (-1, 8, 3)

您可以在playground的右侧边栏中看到合成矢量。

1. Other Types of Operators

加法运算符是所谓的中缀infix运算符,意味着它在两个不同的值之间使用。 还有其他类型的运算符:

  • infix:在两个值之间使用,例如加法运算符(例如,1 + 1
  • prefix:在值之前添加,如负号运算符(例如 -3)。
  • postfix:在一个值之后添加,比如force-unwrap运算符(例如,mayBeNil!
  • ternary:在三个值之间插入两个符号。 在Swift中,不支持用户定义的三元运算符,只有一个内置的三元运算符,您可以在 Apple’s documentation中阅读。

您想要重载的下一个运算符是负号符号,它将更改Vector的每个组件的符号。 例如,如果将它应用于vectorA,即(1,3,2),则返回(-1,-3,-2)

在扩展名内的上一个静态static函数下面添加此代码:

static prefix func - (vector: Vector) -> Vector {
  return [-vector.x, -vector.y, -vector.z]
}

假设运算符是中缀infix,因此如果您希望运算符是不同的类型,则需要在函数声明中指定运算符类型。 负号运算符不是中缀,因此您将前缀prefix修饰符添加到函数声明中。

playground的底部,添加以下行:

-vectorA // (-1, -3, -2)

在侧栏中检查结果是否正确。

接下来是减法,留给你自己实现。 完成后,请检查以确保您的代码与我的代码类似。 提示:减法与添加负号相同。

试一试,如果您需要帮助,请查看下面的解决方案!

static func - (left: Vector, right: Vector) -> Vector {
  return left + -right
}

通过将此代码添加到playground的底部来测试您的新运算符:

vectorA - vectorB // (3, -2, 1)

2. Mixed Parameters? No Problem!

您还可以通过标量乘法将向量乘以数字。 要将两个向量相乘,可以将每个分量相乘。 你接下来要实现这个。

您需要考虑的一件事是参数的顺序。 当您实施加法时,顺序无关紧要,因为两个参数都是向量。

对于标量乘法,您需要考虑Int * VectorVector * Int。 如果您只实现其中一种情况,Swift编译器将不会自动知道您希望它以其他顺序工作。

要实现标量乘法,请在刚刚添加的减法函数下添加以下两个函数:

static func * (left: Int, right: Vector) -> Vector {
  return [
    right.x * left,
    right.y * left,
    right.z * left
  ]
}

static func * (left: Vector, right: Int) -> Vector {
  return right * left
}

为避免多次写入相同的代码,第二个函数只是将其参数转发给第一个。

在数学中,向量有另一个有趣的操作,称为cross-productcross-product的原理超出了本教程的范围,但您可以在Cross product Wikipedia page页面上了解有关它们的更多信息。

由于在大多数情况下不鼓励使用自定义符号(谁想在编码时打开表情符号菜单?),重复使用星号和cross-product运算符会非常方便。

与标量乘法不同,Cross-products将两个向量作为参数并返回一个新向量。

添加以下代码以在刚刚添加的乘法函数之后添加cross-product实现:

static func * (left: Vector, right: Vector) -> Vector {
  return [
    left.y * right.z - left.z * right.y,
    left.z * right.x - left.x * right.z,
    left.x * right.y - left.y * right.x
  ]
}

现在,将以下计算添加到playground的底部,同时利用乘法和cross-product运算符:

vectorA * 2 * vectorB // (-14, -10, 22)

此代码找到vectorA2的标量倍数,然后找到该向量与vectorB的交叉乘积。 请注意,星号运算符始终从左向右,因此前面的代码与使用括号分组操作相同,如(vectorA * 2)* vectorB

3. Protocol Operators

一些运算符是协议的成员。 例如,符合Equatable的类型必须实现==运算符。 类似地,符合Comparable的类型必须至少实现<==,因为Comparable继承自EquatableComparable类型也可以选择实现>> =<=,但这些运算符具有默认实现。

对于VectorComparable并没有太多意义,但Equatable却很重要,因为如果它们的组件全部相等,则两个向量相等。 接下来你将实现Equatable

要符合协议,请在playground的末尾添加以下代码:

extension Vector: Equatable {
  static func == (left: Vector, right: Vector) -> Bool {
    return left.x == right.x && left.y == right.y && left.z == right.z
  }
}

将以下行添加到playground的底部以测试它:

vectorA == vectorB // false

此行按预期返回false,因为vectorA具有与vectorB不同的组件。

符合Equatable不仅能够检查这些类型的相等性。您还可以获取矢量数组的contains(_:)方法!


Creating Custom Operators

还记得我是怎么说通常不鼓励使用自定义符号吗?与往常一样,该规则也有例外。

关于自定义符号的一个好的经验法则是,只有在满足以下条件时才应使用它们:

  • 它们的含义是众所周知的,或者对阅读代码的人有意义。
  • 它们很容易在键盘上打字。

您将实现的最后一个运算符匹配这两个条件。矢量点积产生两个向量并返回单个标量数。您的运算符会将向量中的每个值乘以另一个向量中的对应值,然后将所有这些乘积相加。

点积的符号为,您可以使用键盘上的Option-8轻松键入。

您可能会想,“我可以在本教程中对其他所有操作符执行相同的操作,对吧?”

不幸的是,你还不能那样做。在其他情况下,您正在重载已存在的运算符。对于新的自定义运算符,您需要首先创建运算符。

直接在Vector实现下面,但在CustomStringConvertible一致性扩展之上,添加以下声明:

infix operator •: AdditionPrecedence

这将定义为必须放在两个其他值之间的运算符,并且与加法运算符+具有相同的优先级。 暂时忽略优先级别。

既然已经注册了此运算符,请在运算符扩展的末尾添加其实现,紧接在乘法和cross-product运算符*的实现之下:

static func • (left: Vector, right: Vector) -> Int {
  return left.x * right.x + left.y * right.y + left.z * right.z
}

将以下代码添加到playground的底部以进行测试:

vectorA • vectorB // 15

到目前为止,一切看起来都不错......或者是吗? 在playground的底部尝试以下代码:

vectorA • vectorB + vectorA // Error!

Xcode对你不满意。 但为什么?

现在,+具有相同的优先级,因此编译器从左到右解析表达式。 编译器将您的代码解释为:

(vectorA • vectorB) + vectorA

此表达式归结为Int + Vector,您尚未实现并且不打算实现。 你能做些什么来解决这个问题?


Precedence Groups

Swift中的所有运算符都属于一个优先级组(precedence group),它描述了运算符的计算顺序。 还记得学习小学数学中的操作顺序吗? 这基本上就是你在这里所要处理的。

在Swift标准库中,优先级顺序如下:

Swift基础知识相关(三) —— 重载自定义运算符(一)_第1张图片

以下是关于这些运算符的一些注释,因为您之前可能没有看到它们:

  • 1) 按位移位运算符<<>>用于二进制计算。
  • 2) 您使用转换运算符,isas来确定或更改值的类型。
  • 3) nil合并运算符??有助于为可选值提供回退值。
  • 4) 如果您的自定义运算符未指定优先级,则会自动分配DefaultPrecedence
  • 5) 三元运算符,? :,类似于if-else语句。
  • 6) 对于=的衍生,AssignmentPrecedence在其他所有内容之后进行评估,无论如何。

编译器解析具有左关联性的类型,以便v1 + v2 + v3 ==(v1 + v2)+ v3。 对于右关联性结果也是正确的。

操作符按它们在表中出现的顺序进行解析。 尝试使用括号重写以下代码:

v1 + v2 * v3 / v4 * v5 == v6 - v7 / v8

当您准备好数学知识时,请查看下面的解决方案。

(v1 + (((v2 * v3) / v4) * v5)) == (v6 - (v7 / v8))

在大多数情况下,您需要添加括号以使代码更易于阅读。 无论哪种方式,理解编译器评估运算符的顺序都很有用。

1. Dot Product Precedence

您的新dot-product并不适合任何这些类别。 它必须少于加法(如前所述),但它是否真的适合CastingPrecedenceRangeFormationPrecedence

相反,您将为您的点积运算符创建自己的优先级组。

用以下内容替换运算符的原始声明:

precedencegroup DotProductPrecedence {
  lowerThan: AdditionPrecedence
  associativity: left
}

infix operator •: DotProductPrecedence

在这里,您创建一个新的优先级组并将其命名为DotProductPrecedence。 您将它放在低于AdditionPrecedence的位置,因为您希望加法优先。 你也可以将它设为左关联,因为你想要从左到右进行评估,就像你在加法和乘法中一样。 然后,将此新优先级组分配给运算符。

注意:除了lowerThan之外,您还可以在DotProductPrecedence中指定higherThan。 如果您在单个项目中有多个自定义优先级组,这一点就变得很重要。

您的旧代码行现在运行并按预期返回:

vectorA • vectorB + vectorA // 29

恭喜 - 您已经掌握了自定义操作符!

此时,您知道如何根据需要重载Swift操作符。 在本教程中,您专注于在数学上下文中使用运算符。 在实践中,您将找到更多使用运算符的方法。

ReactiveSwift ReactiveSwift framework 框架中可以看到自定义操作符使用的一个很好的演示。 一个例子是<~,这是反应式编程中的一个重要函数。 以下是此运算符的使用示例:

let (signal, _) = Signal.pipe()
let property = MutableProperty(0)
property.producer.startWithValues {
  print("Property received \($0)")
}

property <~ signal

Cartography 是另一个大量使用运算符重载的框架。 此AutoLayout工具重载相等和比较运算符,以使NSLayoutConstraint创建更简单:

constrain(view1, view2) { view1, view2 in
  view1.width   == (view1.superview!.width - 50) * 0.5
  view2.width   == view1.width - 50
  view1.height  == 40
  view2.height  == view1.height
  view1.centerX == view1.superview!.centerX
  view2.centerX == view1.centerX

  view1.top >= view1.superview!.top + 20
  view2.top == view1.bottom + 20
}

此外,您始终可以参考Apple的官方文档 custom operator documentation。

有了这些新的灵感来源,您可以走出世界,通过运算符重载使代码更简单。不过还是要小心使用自定义操作符!

下面看下相关整体代码

struct Vector {
  let x: Int
  let y: Int
  let z: Int
}

extension Vector: ExpressibleByArrayLiteral {
  init(arrayLiteral: Int...) {
    assert(arrayLiteral.count == 3, "Must initialize vector with 3 values.")
    self.x = arrayLiteral[0]
    self.y = arrayLiteral[1]
    self.z = arrayLiteral[2]
  }
}

precedencegroup DotProductPrecedence {
  lowerThan: AdditionPrecedence
  associativity: left
}

infix operator •: DotProductPrecedence

extension Vector: CustomStringConvertible {
  var description: String {
    return "(\(x), \(y), \(z))"
  }
}

let vectorA: Vector = [1, 3, 2]
let vectorB: Vector = [-2, 5, 1]

// MARK: - Operators
extension Vector {
  static func + (left: Vector, right: Vector) -> Vector {
    return [
      left.x + right.x,
      left.y + right.y,
      left.z + right.z
    ]
  }
  
  static prefix func - (vector: Vector) -> Vector {
    return [-vector.x, -vector.y, -vector.z]
  }
  
  static func - (left: Vector, right: Vector) -> Vector {
    return left + -right
  }
  
  static func * (left: Int, right: Vector) -> Vector {
    return [
      right.x * left,
      right.y * left,
      right.z * left
    ]
  }
  
  static func * (left: Vector, right: Int) -> Vector {
    return right * left
  }
  
  static func * (left: Vector, right: Vector) -> Vector {
    return [
      left.y * right.z - left.z * right.y,
      left.z * right.x - left.x * right.z,
      left.x * right.y - left.y * right.x
    ]
  }
  
  static func • (left: Vector, right: Vector) -> Int {
    return left.x * right.x + left.y * right.y + left.z * right.z
  }
}

vectorA + vectorB // (-1, 8, 3)
-vectorA // (-1, -3, -2)
vectorA - vectorB // (3, -2, 1)

extension Vector: Equatable {
  static func == (left: Vector, right: Vector) -> Bool {
    return left.x == right.x && left.y == right.y && left.z == right.z
  }
}

vectorA == vectorB // false

vectorA • vectorB // 15

vectorA • vectorB + vectorA // 29

后记

本篇主要讲述了重载自定义运算符,感兴趣的给个赞或者关注~~~

Swift基础知识相关(三) —— 重载自定义运算符(一)_第2张图片

你可能感兴趣的:(Swift基础知识相关(三) —— 重载自定义运算符(一))