Swift 3中带有隐式解包的可选项的字符串插值不正确

73

Swift 3中,为什么在使用字符串插值时隐式解包可选项不会被解包?

示例:在Playground中运行以下代码:

var str: String!
str = "Hello"

print("The following should not be printed as an optional: \(str)")

生成此输出:

The following should not be printed as an optional: Optional("Hello")
当然,我可以使用+操作符来连接字符串,但我在我的应用程序中几乎到处都在使用字符串插值,现在由于这个(错误?)导致它不再起作用。
这到底是一个bug还是Swift 3故意改变了这种行为?

4
请注意,这很快会产生警告:https://github.com/apple/swift/pull/5110。 - jtbandes
1
在我看来,这是最干净的解决方案,因为它明确说明了当 strnil 时会发生什么:print("The following should not be printed as an optional: \(str ?? "")") - hashemi
2
@hashemi 这里的核心问题是根本不应该有这种必要性。IUO 的整个重点在于当访问时你知道它总是会有一个值,因此你不需要执行显式的nil检查。而且 - 如其名所示 - 编译器应该为您完成所有的解包操作,但它没有这么做。 - Keiwan
Ole Begemann 关于 3.0 和 3.1 中的可选项和字符串插值(虽然不是特别关注 IUOs)。https://oleb.net/blog/2016/12/optionals-string-interpolation/ - hashemi
嗨Keiwan,你只需要在print语句中的字符串变量旁边加上!,像这样:print("以下内容不应作为可选项打印:(str!)") - Nand Parikh
1个回答

71
根据SE-0054ImplicitlyUnwrappedOptional<T>不再是一个独立的类型;现在只有Optional<T>
声明仍然允许被标注为隐式解包可选项T!,但这样做只是添加了一个隐藏属性来告诉编译器它们的值可能会在需要其解包类型T的上下文中被强制解包;它们的实际类型现在是T?
所以你可以将这个声明看作:
var str: String!

实际上看起来是这样的:
@_implicitlyUnwrapped // this attribute name is fictitious 
var str: String?

只有编译器能看到这个@_implicitlyUnwrapped属性,但它允许在需要String(它的解包类型)的上下文中隐式地解包str的值:

// `str` cannot be type-checked as a strong optional, so the compiler will
// implicitly force unwrap it (causing a crash in this case)
let x: String = str

// We're accessing a member on the unwrapped type of `str`, so it'll also be
// implicitly force unwrapped here
print(str.count)

但在所有其他情况下,如果str可以被类型检查为强制可选项,那么它将是一个强制可选项:

// `x` is inferred to be a `String?` (because we really are assigning a `String?`)
let x = str 

let y: Any = str // `str` is implicitly coerced from `String?` to `Any`

print(str) // Same as the previous example, as `print` takes an `Any` parameter.

编译器总是更倾向于将其视为强制解包,而不是强制展开。

正如提案所说(重点在于我):

如果表达式可以使用强可选类型进行显式类型检查,那么就会这样做。但是,如果必要的话,类型检查器将退回到强制使用可选项。这种行为的效果是,任何引用声明为 T! 的值的表达式的结果都将具有类型 T 或类型 T?

当涉及到字符串插值时,在幕后编译器使用_ExpressibleByStringInterpolation 协议中的此初始化程序来评估字符串插值段:

/// Creates an instance containing the appropriate representation for the
/// given value.
///
/// Do not call this initializer directly. It is used by the compiler for
/// each string interpolation segment when you use string interpolation. For
/// example:
///
///     let s = "\(5) x \(2) = \(5 * 2)"
///     print(s)
///     // Prints "5 x 2 = 10"
///
/// This initializer is called five times when processing the string literal
/// in the example above; once each for the following: the integer `5`, the
/// string `" x "`, the integer `2`, the string `" = "`, and the result of
/// the expression `5 * 2`.
///
/// - Parameter expr: The expression to represent.
init<T>(stringInterpolationSegment expr: T)

因此,当由您的代码隐式调用时:

var str: String!
str = "Hello"

print("The following should not be printed as an optional: \(str)")

作为 str 的实际类型是 String?,默认情况下编译器将推断泛型占位符 T 为此类型。因此,str 的值不会被强制解包,你最终会看到一个可选项的描述。
如果你希望在字符串插值中强制解包 IUO,只需使用强制解包运算符 ! 即可:
var str: String!
str = "Hello"

print("The following should not be printed as an optional: \(str!)")

或者你可以强制将其转换为非可选类型(在本例中为String),以便强制编译器隐式地强制解包它:
print("The following should not be printed as an optional: \(str as String)")

当然,如果strnil,这两者都会崩溃。


29
好像使用IUO还不够糟糕,现在它们变得不那么有用并且表现出矛盾的行为。如果我使用一个IUO,那是因为我希望它被隐式解包。从现在开始,它们应该被称为Schrödinger可选项。 - Andreas
5
不要使用它们。 - Alexander
6
给出的解释很好。需要给出这种解释的事实并不好。我与Swift一起工作的时间越长,我就越有这样的印象:他们只是用另一种类型的复杂性替换了Objective-C中的一种复杂性,并且最终使用其中任何一种都同样困难/容易。这种东西如何能让程序员的生活更轻松,超出了我的理解。 - Joris Mans
6
虽然我出乎意料地喜欢Swift而不是Obj-C,但我必须承认,似乎75%的编码工作直接或间接涉及到更正可选变量要求。对于一个足够简单的问题:对象是否为空,这需要花费大量的精力。毫无疑问,肯定有更好的选择(打了个双关语)。 - drew..
来自JS/PHP背景,我从未意识到我在处理变量类型时是多么的被宠坏了 :O当然,这些语言也有它们自己的问题 ;) - Robert Dundon
显示剩余4条评论

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