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
模板并命名您的playground
为CustomOperators
。删除所有默认代码,以便您可以从空白平板开始。将以下代码添加到您的
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)
。该协议进一步方便,并允许您直接使用数组进行初始化,只要您明确定义类型,这是您为
vectorA
和vectorB
所做的。这种方法的唯一警告是你必须接受任何长度的数组。如果您将此代码放入应用程序中,请记住,如果传入长度不是三的数组,它将会崩溃。如果您尝试初始化少于或多于三个值的
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 * Vector
和Vector * 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-product
。 cross-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)
此代码找到
vectorA
和2
的标量倍数,然后找到该向量与vectorB
的交叉乘积。 请注意,星号运算符始终从左向右,因此前面的代码与使用括号分组操作相同,如(vectorA * 2)* vectorB
。3. Protocol Operators
一些运算符是协议的成员。 例如,符合
Equatable
的类型必须实现==
运算符。 类似地,符合Comparable
的类型必须至少实现<
和==
,因为Comparable
继承自Equatable
。 Comparable
类型也可以选择实现>
,> =
和<=
,但这些运算符具有默认实现。对于
Vector
,Comparable
并没有太多意义,但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标准库中,优先级顺序如下:
文章图片
以下是关于这些运算符的一些注释,因为您之前可能没有看到它们:
- 1) 按位移位运算符
<<
和>>
用于二进制计算。 - 2) 您使用转换运算符,
is
和as
来确定或更改值的类型。 - 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
并不适合任何这些类别。 它必须少于加法(如前所述),但它是否真的适合CastingPrecedence
或RangeFormationPrecedence
?相反,您将为您的点积运算符创建自己的优先级组。
用以下内容替换
?
运算符的原始声明: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中willSet和didSet的简述
- Hacking|Hacking with iOS: SwiftUI Edition - SnowSeeker 项目(一)
- LeetCode算法题-11.|LeetCode算法题-11. 盛最多水的容器(Swift)
- 自我修养--基础知识
- 思维导图作业3—工作相关导图
- iOS-Swift-map|iOS-Swift-map filter reduce、函数式编程
- 微信小程序基础知识
- Swift|Swift ----viewController 中addChildViewController
- SwiftUI|SwiftUI iOS 瀑布流组件之仿CollectionView不规则图文混合(教程含源码)
- AnyProxy抓取http/https请求