美文网首页Swift学习
Swift基础知识相关(三) —— 重载自定义运算符(一)

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

作者: 刀客传奇 | 来源:发表于2019-08-13 12:11 被阅读28次

    版本记录

    版本号 时间
    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标准库中,优先级顺序如下:

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

    • 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<Int, Never>.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基础知识相关(三) —— 重载自定义运算符(一)

        本文链接:https://www.haomeiwen.com/subject/sbzmjctx.html