使用泛型的Optionals的覆盖/运算符导致无限循环

时间:2017-01-19 09:56:44

标签: swift

让我们看一下以下代码片段:

func / <T>(lhs: T?,rhs: T?) throws -> T? {
    switch (lhs,rhs) {
        case let (l?,r?):
            return try l/r
        default:
            return nil
    }
}

let x : Double? = 2
let y : Double? = 2

let z = try! x/y

我创建了一个泛型函数,它需要两个可选参数。如果我运行此代码会导致无限循环,因为尝试l / r 使用func / <T>(lhs: T?,rhs: T?)来划分值。任何人都可以解释为什么除了两个没有可选的双值导致对我写的方法的函数调用,而不是 Double 的默认 / 运算符定义?

如果我通过扩展程序扩展 Double ,该扩展程序需要一个静态 / 运算符,那么所有内容都像魅力一样:

protocol Dividable {
    static func /(lhs: Self, rhs: Self) -> Self
}

extension Double: Dividable {}

func / <T:Dividable>(lhs: T?,rhs: T?) throws -> T? {
    switch (lhs,rhs) {
        case let (l?,r?):
            return  l/r
        default:
            return nil
    }
}

let x : Double? = 2
let y : Double? = 2

let z = try! x/y

2 个答案:

答案 0 :(得分:2)

例如二进制算术Double未使用具体的Double类型实现,而是作为符合FloatingPoint的类型的默认通用实现:

在自定义/函数的块中,编译器不知道类型持有者T符合FloatingPoint,并且l/r的重载解析将解析为方法本身(因为FloatingPoint实现,虽然更具体,但是在自定义实现中更一般的非约束类型T无法访问。

您可以通过将FloatingPoint作为类型约束添加到您自己的自定义方法来解决此问题:

func /<T: FloatingPoint>(lhs: T?, rhs: T?) throws -> T? {
    switch (lhs, rhs) {
        case let (l?, r?):
            return try l/r
        default:
            return nil
    }
}

同样,整数类型的二进制算法实现为默认通用实现,约束为符合公共协议IntegerArithmetic符合的内部协议_IntegerArithmetic的类型。

您可以使用后一种公共协议来实现整数类型的自定义运算符函数的重载。

func /<T: IntegerArithmetic>(lhs: T?, rhs: T?) throws -> T? {
    switch (lhs, rhs) {
        case let (l?, r?):
            return try l/r
        default:
            return nil
    }
}

最后,您可能想要考虑为什么要使用此函数。 N还注意到,只有在两者都不同于nil的情况下,才能在处理您想要操作的两个可选值时简化实现。 E.g:

func /<T: FloatingPoint>(lhs: T?, rhs: T?) -> T? {
    return lhs.flatMap { l in rhs.map{ l / $0 } }
}

func /<T: IntegerArithmetic>(lhs: T?, rhs: T?) -> T? {
    return lhs.flatMap { l in rhs.map{ l / $0 } }
}

如果您更喜欢语义而不是简洁,请将switch语句包装在单个if语句中

func /<T: FloatingPoint>(lhs: T?, rhs: T?) -> T? {
    if case let (l?, r?) = (lhs, rhs) {
        return l/r
    }
    return nil
}

func /<T: IntegerArithmetic>(lhs: T?, rhs: T?) -> T? {
    if case let (l?, r?) = (lhs, rhs) {
        return l/r
    }
    return nil
}

答案 1 :(得分:1)

您的函数签名不会让编译器知道lhsrhs的类型,除了它们属于同一类型。例如,您可以像这样调用您的方法:

let str1 = "Left string"
let str2 = "Right string"
let result = try? str1 / str2

这将导致无限循环,因为编译器知道的唯一一个接受相同类型的2个参数(在这种情况下为/)的调用String的方法就是你&#39宣布; return try l/r会一遍又一遍地调用您的func / <T>(lhs: T?,rhs: T?) throws -> T?方法。

正如您在问题中提到的,您将需要一个参数必须符合的协议。不幸的是there is no existing Number or Dividable protocol that would fit your needs,所以你必须自己制作。

请注意,当分母为0并且不会引发错误时,除法会崩溃,因此您应该能够从函数中删除throws关键字,以便它是:

func / <T:Dividable>(lhs: T?, rhs: T?) -> T?

修改以进一步澄清

如果你考虑一下编译器在那一点上所知道的东西,我觉得它更有意义。一旦进入函数,所有编译器都知道lhsrhs类型为T并且是可选的。它不知道 T是什么,或者它的任何属性或功能,而只知道它们都属于T类型。解开值后,仍然只知道它们都是T类型且非可选。即使知道T(在这个例子中)是Double,它也可能是String(根据我上面的示例)。这将要求编译器迭代每个可能的类和结构以找到支持您的方法签名的东西(在这种情况下为func / (lhs: Double, rhs: Double) -> Double),它根本无法做到(在合理的时间内),并且导致无法预测的代码。想象一下,如果你添加了这个全局方法,然后每次/用于现有的东西(例如Float(10) / Float(5))你的方法被调用,这会很快变得混乱和混乱。