Slug rich cover

如何让 Swift 在数值运算中自由转换

在计算结果的类型可以轻易推导,却还要转换不同类型数值的时候,确实是一件非常痛苦的事情。在这次讲座中,Rich Fox 通过一个小小的类型安全操作,来完成数值类型转换这件麻烦的事情。通过 Swift 2.0 的协议扩展、模式匹配、泛型以及运算符重载机制,Rich 简化了数值类型在运算中互相转换的操作。


大家好,我是 Richard Fox。我目前正着力撰写我的博客,并且是 Propeller Labs 的一名 iOS 工程师,这个是一个专注于 MVP 以及移动端的开发服务商店(我们现在正寻找同样爱好 Swift 的开发者,如果您感兴趣的话就来加入我们吧!)。我今天的讲座是关于如何让 Swift 在数值运算中自由转换的问题。

运算比较 (01:06)

在 Objective-C 中,我们可以进行如下形式的运算:

float   w = 5;
double  x = 10;
int     y = 15;

CGFloat z = w + x + y; 

而在 Swift 中,等价的运算却是这种模样:

let w:Float  = 5
let x:Double = 10
let y:Int    = 15
let z:CGFloat = CGFloat(w) + CGFloat(x) + CGFloat(y)

类型安全是个好东西,但是有些时候却会让人十分抓狂。对于数值运算应该有更简洁的方法才对。

Swift 2.0 之前的版本 (01:31)

在 Swift 2.0 之前,我就已经试着来解决这个问题,使用一个简单的数值类型扩展,在里面添加 getter 方法。这确实有用,不过如果这样的话你就必须给每个数字类型中添加大量重复的 getter 代码:

extension Double {
    var c:CGFloat {
        return CGFloat(self)
    }
    //. . .
}

extension Int {
    var c:CGFloat {
        return CGFloat(self)
    }
    //. . .
}

extension Float {
    var c:CGFloat {
        return CGFloat(self)
    }
    //. . .
}

//let y:CGFloat = 1 + x.c

Receive news and updates from Realm straight to your inbox

通过 Swift 2.0 的协议类型,我想到我可以为所有兼容的数值类型添加一个共有的协议。这样通过强大的模式匹配,我就可以使用简单的点语法来转换这些类型。

数值转换是如何实现的? (02:54)

每个数值类型本质上是一个结构体。在每个数值类型的定义当中,都建有转换为其他数值类型的构造器。在标准库的定义中,我们可以看到这些构造器。

init(_ v: Float ), init(_ v: Double)
let x: Float = 5.0
let y = Float(x) 或者 let y = Float.init(x)

我们甚至可以使用 Float.init() 来执行转换。如果我们按住 Command 单击这些定义,就可以确切地看到所选定义位于标准库的哪个部位。

为了让这个定义能够为我们所用,我们需要给我们的协议定义一个用来执行转换的构造器。我们可以让这些数值类型实现这个执行转换的协议。当所有的数值类型都实现所有的构造器方法后,我们就无需做其他事情了,不过 CGFloat 却是例外。

CGFloat 有一点奇葩,它并没有定义在标准库当中——它是 Core Graphics 中的一部分。和标准库中的数字不同,它并没有初始化转换方法。不过幸运的是,我们可以简单创建一个扩展,然后使用 self = value 来添加初始化转换方法。

protocol NumberConvertible {
    init (_ value: Int) 
    init (_ value: Float) 
    init (_ value: Double) 
    init (_ value: CGFloat)
}

extension CGFloat : NumberConvertible {}
extension Double  : NumberConvertible {}
extension Float   : NumberConvertible {}
extension Int     : NumberConvertible {}

extension CGFloat{
    public  init(_ value: CGFloat){
        self = value 
    }
}

模式匹配 (05:02)

switch self {
    case let x as CGFloat:
        print("x is a CGFloat")
    case let x as Float:
        print("x is a CGFloat")
    case let x as Int:
        print("x is a CGFloat")
    case let x as Double:
        print("x is a CGFloat")

    default:
        print("x is unknown..")
}

我们在协议中使用这个模式匹配的目的是找出这个协议自身对应的确切类型。我们需要知道这个信息,这样我们才能够将其插入到我们在协议中定义的构造器当中。像这样进行类型转换可能会导致内存泄露,但是现在在 Swift 2.1 已经修复了这个问题。

建立扩展 (05:56)

extension NumberConvertible {

    private func convert<T: NumberConvertible>() -> T {
        switch self {
            case let x as CGFloat:
                return T(x) //T.init(x)
            case let x as Float:
                return T(x)
            case let x as Double:
                return T(x)
            case let x as Int:
                return T(x)
            default:
                assert(false, "NumberConvertible convert cast failed!")
                return T(0)
        }
    }
    public var c:CGFloat{
        return convert()
    }
    //...
}

在扩展中,我们定义了一个私有的函数 convert(),这个函数将返回一个泛型。通过检索 self 的具体类型,得到拥有构造器的具体兼容数值类型。一旦找到了我们决定的数值类型,并且由于 T 实现了 NumberConvertible 协议,因此我们就可以调用构造器 T.init(x)。现在我们就能够使用点语法来调用属性的 getter 方法了。在这个扩展中,如果需要增加新的点语法访问需求,我们只需要添加一次即可。

不借助构造器实现转换 (07:29)

现在我们就可以进行转换而无需声明类型了。比如说,假设有两个不同的数值类型,我们只需要执行 convert() 方法,然后让它们相加等于 Y,然后类型会自动对其进行推断,而无需我们告诉编辑器应该转换的类型。

let w: Double = 4.4
let x: Int = 5
let y: Float = w.convert() + x.convert()

但是我们还可以更简便一些。下面可能是最简单的例子了。我们有两个不同的类型,然后相加返回给另一个类型。

let x: Int = 5
let y: CGFloat = 10
let z: Double = x + y

我们可以使用运算符重载。我们将使用三个实现 NumberConvertible 的泛型来重载我们的运算符。我们将会对运算符 lhs(左操作数) 和 rhs(右操作数) 同时使用 convert() 函数,这样它们的类型就相同了。我们将使用标准库定义(因为它们是相同的类型,因此我们可以这样做),然后实现这个功能。我们会多次使用 convert() 以便能够转换返回的泛型类型。以下是解决方案:

func + <T:NumberConvertible, U:NumberConvertible, V:NumberConvertible>(lhs: T, rhs: U) -> V {
    let v: Double = lhs.convert()
    let w: Double = rhs.convert()
    return (v + w).convert()
}

我们现在重载了“+”运算符,这解决了第一种情况。现在让我们添加第二个运算符和第三个数字,现在我们总共有四个类型了。然而,下面的代码却没法正常工作了。

let x:Int     = 5
let y:CGFloat = 10
let z:Float   = 15

//`Float` is not convertible to `Double`
let res:Double = x + y + z

编译器使用哪一个运算符定义? (09:44)

第一个运算符看起来应该是使用我们的自定义运算符,然而第二个则是使用标准库定义的运算符。因为我们自定义的运算符是泛型,因此第一个运算符必须完成某种推断第三个数字类型的操作。并且由于第三个数字类型是 float 类型,因此当我们执行到第二个运算符时,我们执行的是 float 加上 float。处于某些原因,编译器并不能够智能地分析出返回类型,实际上它并不是 float 类型。

处理编译器 (10:51)

为了解决这个问题,我首先做的就是向编译器妥协:

public typealias PreferredType = Double

public func + <T:NumberConvertible, U:NumberConvertible>(lhs: T, rhs: U) -> PreferredType
{
    let v: PreferredType = lhs.convert()
    let w: PreferredType = rhs.convert()
    return v+w
}

一个确认的返回值能够正常运行,但是它仅仅只返回 Double 类型。编译器是高兴了,但是我们却不开心,这不是个好办法,因为我们只能够返回一种类型。

我们可以给予编译器更多选项。我们将会对复制我们的定义(只尝试使用 lhsrhs 都是相同类型的两个泛型)。当然,我们仍然能够推断出返回类型(我们的原始类型)。

将这两种运算符结合起来,我们的方案几乎快完成了。然而,某些时候编译器会产生紊乱,它不知道该用哪一个运算符。

现在我们删掉刚刚我们创建的这个多余的重载。在这里面执行加零操作会导致编译器停止工作。这似乎是两个操作同时运行所导致的问题。

优化运算符 (13:39)

extension NumberConvertible {
    private typealias CombineType = (Double,Double) -> Double
    private func operate<T:NumberConvertible,V:NumberConvertible>(b:T, @noescape combine:CombineType) -> V{
        let x:Double = self.convert()
        let y:Double =    b.convert()
        return combine(x,y).convert()
    }
}

public func + <T:NumberConvertible, U:NumberConvertible,V:NumberConvertible>(lhs: T, rhs: U) -> V {
    return lhs.operate(rhs, combine: + )
}
public func - <T:NumberConvertible, U:NumberConvertible,V:NumberConvertible>(lhs: T, rhs: U) -> V {
    return lhs.operate(rhs, combine: - )
}

通过给我们的协议中添加更多的扩展可以让上面的代码更加优化。我回到 NumberConvertible 协议那里,我向扩展中添加了一个函数,它接受两个 Double 类型,返回一个 Double 类型。接下来,我定义这个操作函数接收一个泛型类型。我同样也增加了一个组合的类型,为其添加了一个接收 CombineType 别名的函数,然后返回另外的泛型。

在函数实现中,我将自身和泛型输入转换为 Double 类型,然后我们就可以在我们的 combine 函数中执行相加了。结果是我们可以转换其为返回类型。接下来,我们用一行代码:lhs.operate 可以替换掉我们之前的运算符实现,然后其参数分别是 rhs 和一个简单的运算符。这样看起来十分棒,并且用起来也是十分简单。

“Expression Too Complex” (15:21)

表达式过于复杂说明表达式应该在一个合理的时间内解决。这表明我们必须将表达式分成两个不同的子表达式,或者使用多个表达式(当我们使用重载运算符的时候可能用得更多)。

如果你遇到了这种错误,或许你可以向 Chis Lattner 发送 #ExpressionTooComplex 话题表达你的愤怒。如果你找到更简单的表达方法也说不定,你可以发送 radar 请求,但是别的事你也没法做。

结论 (16:47)

综上所述,我们可以在某些情况下执行无转换的数字运算。我们出现了加零出错的情况,还遇到了表达式过于复杂导致的错误。我们原始的点方法的实现方式看起来也不是那么糟糕。我自己就用那种方法,即时在现有产品中也是一样,虽然它可能有点背离了类型安全的做法。

所有的代码都可以在此下载。

问与答 (17:51)

Q:Swify 的 & 操作符明确指出了数字运算溢出的可能性。由于这种情况可能会导致数据丢失,我们是否应该使用其他的运算符来规避这种可能?

Rich: 这个主意很不错。我自己出于兴趣简单的实现了一下,但是处于可读性的考虑,这只是一个有待考虑的想法。

About the content

This content has been published here with the express permission of the author.

Richard Fox

Rich is an iOS Developer at Propeller Labs where he has been developing in Swift since the beginning of 2015. In January 2014, he moved to the Bay Area from Hangzhou, China, where he lived for four years while completing a master’s program in teaching Mandarin as a second language, studied the game of Go, and worked at Viva Video on an iOS video editing app. Currently obsessed with Swift 2.0, he tweets about it @RGfox, and blogs about it at foxinswift.com.

4 design patterns for a RESTless mobile integration »

close