在可能抛出异常和不会抛出异常的方法中,Swift类型推断

4

如您所知,Swift可以根据使用情况推断类型。例如,您可以有仅在返回类型上不同的重载方法,并且只要编译器能够推断类型,就可以自由使用它们。例如,通过帮助额外显式类型变量来保存此类方法的返回值。

我发现了一些有趣的时刻。想象一下这个类:

class MyClass {
    enum MyError: Error {
        case notImplemented
        case someException
    }

    func fun1() throws -> Any {
        throw MyError.notImplemented
    }

    func fun1() -> Int {
        return 1
    }

    func fun2() throws -> Any {
        throw MyError.notImplemented
    }

    func fun2() throws -> Int {
        if false {
            throw MyError.someException
        } else {
            return 2
        }
    }
}

当然,它会像这样工作:
let myClass = MyClass()
// let resul1 = myClass.fun1() // error: ambiguous use of 'fun1()'
let result1: Int = myClass.fun1() // OK

但是接下来你可以写类似这样的内容:
// print(myClass.fun1()) // error: call can throw but is not marked with 'try'
// BUT
print(try? myClass.fun1()) // warning: no calls to throwing functions occur within 'try' expression

看起来像是互斥的语句。编译器试图选择正确的函数;在第一次调用时,它试图将Int强制转换为Any,但在第二个语句中它试图做什么呢?

此外,像下面这样的代码

if let result2 = try? myClass.fun2() { // No warnings
    print(result2)
}

没有警告,因此可以假设编译器能够在这里选择正确的重载(也许基于一个事实,即其中一个重载实际上什么都不返回,只是抛出异常)。

我的最后一种假设正确吗?对于 fun1() 的警告是否合理?我们有一些技巧可以欺骗编译器或帮助它进行类型推断吗?

2个回答

5
显然,你绝不能编写像这样的代码。它有太多的问题,正如你所看到的那样。但是我们来看看为什么会这样。
首先,在Swift中,try只是一种修饰符。它不是用于编译器的,而是为了你自己。编译器会计算出所有的类型,然后确定是否需要使用try。它不使用try来计算类型。你可以在这里看到这一点的实际应用:
class X {
    func x() throws -> X {
        return self
    }
}

let y = try X().x().x()

你只需要尝试一次,即使在链中有多个抛出调用。想象一下,如果你基于抛出和非抛出创建了x()的重载,这将如何工作。答案是“无关紧要”,因为编译器不关心try
接下来是类型推断与类型强制的问题。这是类型推断:
let resul1 = myClass.fun1() // error: ambiguous use of 'fun1()'

Swift永远不会推断出模糊的类型。它可能是Any或者是Int,所以它放弃了。

这不是类型推断(类型已知):

let result1: Int = myClass.fun1() // OK

这也有一个已知的、明确的类型(请注意没有 ?):
let x : Any = try myClass.fun1()

但是这需要类型强制转换(就像您的打印示例一样)。
let x : Any = try? myClass.fun1() // Expression implicitly coerced from `Int?` to `Any`
                                  // No calls to throwing function occur within 'try' expression

为什么调用了Int版本?try?返回一个Optional(是Any类型)。因此,Swift可以选择将表达式转换为Int?并强制转换为AnyAny?并强制转换为Any。Swift几乎总是更喜欢真实的类型而不是Any(它也确实讨厌Any?)。这是避免在代码中使用Any的许多原因之一。它会以奇怪的方式与Optional交互。可以有争议地认为这应该是一种错误,但是Any是如此棘手的类型,以至于很难确定它所有的角落案例。
那么这如何适用于print呢?print的参数是Any,因此这就像let x: Any = ...示例,而不像let x =...示例。
在考虑这些内容时,请记住以下几个自动转换:
- 每个T都可以轻松地转换为T? - 每个T都可以明确地转换为Any - 每个T?也可以明确地转换为Any - Any可以轻松地转换为Any?(也是Any??,Any???等) - Any?(Any??,Any???等)可以明确地转换为Any - 每个非抛出函数都可以轻松地转换为抛出版本 - 因此,纯粹基于“throws”进行重载是危险的。
所以,在混合使用throws/non-throws转换、Any/Any?转换以及将throwing try?混合在一起时(它将所有内容提升为可选项),您已经创造了一个完美的混乱风暴。
显然,您永远不应编写这样的代码。

感谢您的回答。 实际上,在这里抛出错误只是为了消除编译器有关缺少返回值的错误。一般思路是推断类型。 老实说,我完全同意你关于这种方法的危险性的看法。我只能想到一个使用它的原因:强制使用某些函数的返回值。就像 @discardableResult 朝相反的方向工作一样。 - Bohdan Ivanov
这不就是 @warn_unused_result 吗?(但我以为现在已经默认了?) - Rob Napier
是的,但这只是一个警告。如果您不使用返回值,拥有这样的方法可能会引发编译错误。但我怀疑它是否具有实际价值 - 它从外部不太容易理解。 - Bohdan Ivanov

3
Swift编译器始终尝试调用最具体的重载函数,如果存在多个重载实现。
您问题中展示的行为是预期的,因为Swift中的任何类型都可以表示为Any,所以即使您将结果值类型注释为Any,例如let result2: Any = try? myClass.fun1(),编译器实际上会调用返回Intfun1实现,然后将返回值转换为Any,因为这是fun1更具体的重载实现。
您可以通过将返回值转换为Any而不是注释类型来使编译器调用返回Any版本。
let result2 = try? myClass.fun1() as Any //nil, since the function throws an error

如果你在类中添加另一个重载版本的fun1,例如:

这种行为可以更好地观察到。

func fun1() throws -> String {
    return ""
}

有三个重载版本的fun1函数,输出结果如下:

let result1: Int = myClass.fun1() // 1
print(try? myClass.fun1()) //error: ambiguous use of 'fun1()'
let result2: Any = try? myClass.fun1() //error: ambiguous use of 'fun1()'
let stringResult2: String? = try? myClass.fun1() // ""

正如你所看到的,在这个例子中,即使你添加了 Any 类型注释,编译器也无法确定使用哪个重载版本的 fun1,因为返回 IntString 的版本都比返回 Any 更专业化,因此不会调用返回 Any 的版本,但由于两个专业化的版本都是正确的,编译器不能决定调用哪一个。

当然,拥有更多的重载函数会导致编译错误。 - Bohdan Ivanov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接