Swift基础知识相关(三)|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!
【Swift基础知识相关(三)|Swift基础知识相关(三) —— 重载自定义运算符(一)】您还可以通过标量乘法将向量乘以数字。 要将两个向量相乘,可以将每个分量相乘。 你接下来要实现这个。
您需要考虑的一件事是参数的顺序。 当您实施加法时,顺序无关紧要,因为两个参数都是向量。
对于标量乘法,您需要考虑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基础知识相关(三)|Swift基础知识相关(三) —— 重载自定义运算符(一)
文章图片
以下是关于这些运算符的一些注释,因为您之前可能没有看到它们:
  • 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.centerXview1.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 ?: DotProductPrecedenceextension 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 // falsevectorA ? vectorB // 15vectorA ? vectorB + vectorA // 29

后记
本篇主要讲述了重载自定义运算符,感兴趣的给个赞或者关注~~~
Swift基础知识相关(三)|Swift基础知识相关(三) —— 重载自定义运算符(一)
文章图片

    推荐阅读