在Swift语言中,感叹号表示什么意思?

542

Swift编程语言指南中有以下示例:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { println("\(name) is being deinitialized") }
}

class Apartment {
    let number: Int
    init(number: Int) { self.number = number }
    var tenant: Person?
    deinit { println("Apartment #\(number) is being deinitialized") }
}

var john: Person?
var number73: Apartment?

john = Person(name: "John Appleseed")
number73 = Apartment(number: 73)

//From Apple's “The Swift Programming Language” guide (https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html)

然后当分配公寓给某个人时,他们使用感叹号来"解包实例(instance):"

john!.apartment = number73

"解包实例"是什么意思?为什么需要解包?与仅执行以下操作有何不同:

john.apartment = number73

我对Swift语言非常陌生。只是在努力掌握基础。


更新:
我所缺少的重要一环(答案中没有直接说明,至少在撰写此文时没有)是当您执行以下操作时:

var john: Person?

这并不意味着"johnPerson 类型,它可能为空(nil)", 我最初的想法是错误的。我只是误解了PersonPerson? 是完全不同的类型。一旦我理解了这一点,所有其他的 ?! 混乱以及下面的伟大答案就变得更加合理了。

23个回答

550

"拆包实例"是什么意思?为什么需要这样做?

据我所知(这对我来说也很新颖)...

术语“已封装”意味着我们应该将可选变量视为一个礼物,用闪亮的纸包裹起来,这个礼物可能(遗憾地!)为空。

当“已封装”时,可��变量的值是一个枚举类型,有两个可能的值(有点像布尔类型)。这个枚举描述了变量是否拥有值(Some(T)),或没有值(None)。

如果存在一个值,可以通过“拆包”变量来获取这个值(从Some(T)中获取T)。

john!.apartment = number73john.apartment = number73有何不���?(改写)

如果你写了一个 Optional 变量的名称(例如文本 john,没有 !),这将引用“包装”的枚举(Some/None),而不是值本身(T)。因此,john 不是 Person 的实例,并且它没有 apartment 成员:
john.apartment
// 'Person?' does not have a member named 'apartment'

实际的Person值可以通过多种方式进行解包:
  • “强制解包”:john!(如果存在Person值,则给出该值,如果为nil,则运行时错误)
  • “可选绑定”:if let p = john { println(p) }(如果存在值,则执行println
  • “可选链接”:john?.learnAboutSwift()(如果存在值,则执行此虚构方法)
我猜你会选择其中一种方法来解包,这取决于nil情况下应该发生什么以及它有多么可能。这种语言设计强制要求显式处理nil情况,我认为这比Obj-C更安全(在那里很容易忘记处理nil情况)。 更新
感叹号也用于声明“隐式解析可选项”的语法。
到目前为止的示例中,john变量已声明为var john:Person?,它是一个可选项。如果您想要该变量的实际值,则必须使用上述三种方法之一进行解包。
如果将变量声明为var john:Person!,那么该变量将是一个隐式解包可选项(请参阅苹果书中的此标题部分)。在访问值时不需要解包这种类型的变量,可以直接使用john而不需要任何其他语法。但是苹果的书中说:

如果存在变量在稍后可能变成nil的可能性,则不应使用隐式解包的可选项。如果需要在变量的生命周期内检查nil值,请始终使用普通的可选类型。

更新2:

Mike Ash的文章“有趣的Swift功能”提供了一些关于可选类型的动机。我认为这是很好的、清晰的写作。

更新3:

另一篇关于感叹号的 隐式解包可选值 使用的有用文章是 Chris Adamson 的 "Swift and the Last Mile"。该文章解释了这是苹果采用的实用措施,用于声明其可能包含 nil 的 Objective-C 框架中使用的类型。将类型声明为可选项(使用 ?)或隐式解包(使用 !)是 "安全与方便之间的权衡"。在文章中给出的示例中,苹果选择将类型声明为隐式解包,使调用代码更加方便,但不太安全。

也许苹果将来会检查他们的框架,消除隐式解包(“可能永远不会为空”)参数的不确定性,并根据其 Objective-C 代码的确切行为替换为可选(“在特定[希望有文档记录的]情况下肯定可能为空”)或标准非可选(“永远不为空”)声明。


我对这个解释不太确定。如果你只是运行代码而没有加上!,它仍然会返回实际值。也许!是为了提高速度? - Richard Washington
好的。所以文档中提到当你确定可以解包时,使用!。但是你可以在不使用它(隐式解包的第四个选项)和不先检查的情况下正常运行代码。如果为nil,则返回值或nil。但是如果您确定它不是nil,则使用!......但我仍然看不出为什么要这样做? - Richard Washington
2
嗨@RichardWashington-我已经更新了我的答案,希望能澄清其中的一些问题。 - Ashley
这里有另一种思考方式:假设艾米想确认她在手机中保存了约翰的电话号码。为此,她在电话簿中查找约翰的名字。如果没有找到,她可以采取两种行动之一:她可以假设她拥有的号码与约翰的号码不同,或者她可以陷入恐慌,因为她知道约翰在电话簿中。 - user749127
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Dominic
显示剩余2条评论

130

这是我认为它们之间的区别:

var john: Person?

意味着john可以为nil

john?.apartment = number73
编译器将解释这一行代码为:
if john != nil {
    john.apartment = number73
}

虽然

john!.apartment = number73
编译器将简单地将此行解释为:
john.apartment = number73
因此,使用!将取消if语句的包装,使其运行更快,但如果John为空,则会发生运行时错误。

所以这里的包装并不是指内存包装,而是指代码包装。在这种情况下,它被if语句包装,并且由于Apple非常关注运行时性能,他们希望为您提供一种让应用程序以最佳性能运行的方法。

更新:

4年后回到这个答案,因为我在Stackoverflow上得到了最高的声望 :) 我当时对解包含义有些误解。现在,经过4年的时间,我相信这里的解包含义是扩展代码的原始紧凑形式。另外,它意味着消除该对象周围的模糊不清,因为从定义上我们无法确定它是否为空值。就像Ashley的答案中所述,把它想象成可能什么都没有的礼物。但我仍然认为解包是代码解包,而不是基于内存的解包,如使用枚举。


9
在我的游乐场里,"john.apartment = number73"无法编译通过,你必须指定为"john?.apartment = number73"。此翻译保持原文意思,同时使句子更加通俗易懂。 - Chuck Pinkert
2
@ChuckPinkert是正确的,第四行应该编辑为john?.apartment = number73,不过回答很好! - Bruce
john.apartment = number73 出现错误: 可选类型 'Person?' 的值未解包:您是否意味着使用 '!' 或 '?'? - spiderman
4
解释:所述“解包”与性能无关。 "空值检查" 仍须在运行时执行,唯一的区别在于,如果 Optional 中没有值,则会抛出运行时错误。翻译:解包与性能无关。在运行时仍需执行“空值检查”,唯一的区别是,如果 Optional 中没有值,则会抛出运行时错误。 - madidier

71

简述:

在Swift语言中,叹号表示“我知道这个可选类型肯定有值,请使用它。” 这被称为强制解包可选类型的值。

示例:

let possibleString: String? = "An optional string."
print(possibleString!) // requires an exclamation mark to access its value
// prints "An optional string."

let assumedString: String! = "An implicitly unwrapped optional string."
print(assumedString)  // no exclamation mark is needed to access its value
// prints "An implicitly unwrapped optional string."

来源: https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/TheBasics.html#//apple_ref/doc/uid/TP40014097-CH5-XID_399


11
你的回答很好,因为我现在明白了正在发生的事情。但我不明白为什么会存在隐式解包可选项。为什么要创建一个被定义为隐式解包可选类型的字符串,而不是一个普通的字符串类型呢?它们之后的使用方式是相同的。我错过了什么吗? - pachun
这似乎不再是真的了。在 Swift 5 repl 中,如果我执行 let f: String! = "hello" 然后 print(f),输出将是 Optional("hello") 而不仅仅是 "hello" - numbermaniac

37

如果 John 是一个可选变量(声明如下)

var john: Person?

那么约翰就有可能没有值(在ObjC术语中,是nil值)

感叹号基本上告诉编译器:“我知道这个有值,你不需要测试它”。 如果你不想使用它,你可以有条件地进行测试:

if let otherPerson = john {
    otherPerson.apartment = number73
}

只有在 john 有值的情况下,这个内部才会被计算。


4
谢谢回复,现在我明白了感叹号的作用是什么,但我仍然在尝试理解它的原因……你说它告诉编译器“我知道这个变量有值,你不需要检查它”。当编译器不进行检查时,它会为我带来什么好处呢?如果没有感叹号,编译器会抛出错误吗?(我还没有一个Swift测试环境)所有可选变量都需要使用感叹号吗?如果是这样,为什么苹果公司要费心引入它,而不是直接让缺少感叹号的变量表示“我知道这个变量有值,你不需要检查它”呢? - Troy
编译器会抛出错误吗?不会,它仍然正常工作并返回预期的值。我不太明白这个。感叹号是用来加速,当你确定的时候使用吗? - Richard Washington
事实上,后面的文档讨论了隐式解包可选项,使用以下示例:“let possibleString: String? = "An optional string." println(possibleString!) // 需要感叹号才能访问其值 // 输出 "An optional string.”但是它在没有感叹号的情况下也可以正常工作。这里似乎有些奇怪。 - Richard Washington
3
@RichardWashington,你感到困惑是有道理的!文档中的示例遗漏了一些内容,并且具有误导性,因为它们只使用了println。显然,在某些情况下不需要解包可选项。其中之一是在使用println时。另一个是在使用字符串插值时。因此,也许println和字符串插值在幕后解包了可选项?也许有人对此有更深入的见解。查看Xcode6 Beta头文件的定义并没有揭示关于这种魔法的任何信息。 - mbeaty
是的,我知道John?John是两种不同的类型。一个是可选类型的Person,另一个是Person类型。在获取Person之前,需要对可选类型的Person进行解包。但正如你所说,至少在这些情况下似乎不需要实际做任何事情就能发生这种情况。这似乎使感叹号变得多余。除非感叹号始终是可选的,但建议在编译时捕获错误时使用。有点像将变量/常量分配给特定类型可以是显式的let John: Person = ...,但也可以是推断的let John = ... - Richard Washington

34
一些大局观的观点可以补充其他有用但更注重细节的答案:
在Swift中,感叹号出现在几个不同的上下文中:
- 强制解包:`let name = nameLabel!.text` - 隐式解包可选类型:`var logo: UIImageView!` - 强制转换:`logo.image = thing as! UIImage` - 未处理的异常:`try! NSJSONSerialization.JSONObjectWithData(data, [])`
这些都是不同的语言结构,具有不同的含义,但它们都有三个重要的共同点:
1. 感叹号绕过了Swift的编译时错误处理检查。
当你在Swift中使用`!`时,实际上是在说:“嘿,编译器,我知道你认为这里可能会发生错误,但我完全确定它永远不会发生。”
(编辑以澄清:在语言设计意义上,强制解包的“!”仍然是“安全”的,即它不会产生未定义行为,并且保证以一种干净、可预测的方式崩溃(与在C或C++中取消引用空指针不同)。但从应用程序开发者的角度来看,这并不“安全”,因为语言不能确保您的应用程序不会因为错误的假设而突然失败。)
并非所有有效的代码都适用于Swift的编译时类型系统 - 或者说任何语言的静态类型检查。有些情况下,你可以逻辑上证明一个错误永远不会发生,但你无法向编译器证明这一点。这就是为什么Swift的设计者首先添加了这些功能。
然而,每当你使用“!”时,你就排除了对错误进行恢复的路径,这意味着...
2. 感叹号可能导致崩溃。
感叹号也表示:“嘿,Swift,我非常确定这个错误永远不会发生,所以让你整个应用程序崩溃比我编写一个恢复路径更好。”

这是一个危险的断言。它可能是正确的:在关键任务代码中,当你对代码的不变性进行了深思熟虑时,错误的输出可能比崩溃更糟糕。

然而,当我在实际应用中看到!时,很少有人会如此谨慎地使用它。相反,它往往意味着:“这个值是可选的,我并没有仔细考虑为什么它可能为空或如何正确处理这种情况,但是加上!后它可以编译通过...所以我的代码是正确的,对吗?”

小心感叹号的傲慢。相反...

3. 尽量少用感叹号。

每一个!结构都有一个?对应项,强制你处理错误/空值的情况:

  • 条件解包: if let name = nameLabel?.text { ... }
  • 可选类型: var logo: UIImageView?
  • 条件转换: logo.image = thing as? UIImage
  • 失败时返回nil的异常处理: try? NSJSONSerialization.JSONObjectWithData(data, [])

如果你想使用!,最好仔细考虑为什么不使用?。如果!操作失败,崩溃程序真的是最好的选择吗?为什么该值是可选的/可能失败的?

在空值/错误情况下,你的代码是否有合理的恢复路径?如果有,编写相应的代码。

如果它绝对不可能为空,如果错误永远不会发生,那么是否有合理的方法来重新设计你的逻辑,以便编译器知道这一点?如果有,那就去做;你的代码将更少出错。

有时候,处理错误没有合理的方法,而忽略错误——从而继续使用错误的数据——会比崩溃更糟糕。在这种情况下,可以使用强制解包。
我定期搜索整个代码库中的!并审查每次使用它的情况。很少有用法经得起审查。(截至本文撰写时,整个Siesta框架只有两个 实例使用了它。)
这并不是说你在代码中永远不应该使用!,只是要谨慎使用,永远不要将其作为默认选项。

func isSubscriptionActive(receiptData: NSDictionary?) -> Bool { if(receiptData == nil) { return false; } return (hasValidTrial(receiptData!) || isNotExpired(receiptData!)) && isNotCancelled(receiptData!) }在给定的情况下,是否有更好的编写方式? - jenson-button-event
你可以这样说:func isSubscriptionActive(receiptData: NSDictionary?) -> Bool { guard let nonNilReceiptData = receiptData else { return false} return (hasValidTrial(nonNilReceiptData) || isNotExpired(nonNilReceiptData)) && isNotCancelled(nonNilReceiptData) } - rkgibson2
感叹号不能规避任何安全检查。如果可选项为nil,则会发生_保证的_崩溃。可选项_始终_被检查。这只是一种非常残酷的安全检查方式。 - gnasher729
正如答案所述,它绕过编译时的安全检查,以换取运行时的安全失败。 - Paul Cantrell

25

john是一个可选的var,它可以包含nil值。为了确保该值不是nil,请在var名称的末尾加上!

来自文档

“一旦确定可选项包含一个值,您就可以通过在可选项名称的末尾添加感叹号(!)来访问其基础值。感叹号实际上表示,“我知道这个可选项肯定有一个值,请使用它。””

另一种检查非nil值的方法是(可选项解包)

    if let j = json {
        // do something with j
    }

2
是的,我在文档中看到过这个,但对我来说似乎仍然是不必要的...因为 john.apartment = number73 也表示“我知道这个可选项肯定有一个值,请使用它。” - Troy
8
可以,但是重点是,默认情况下 Swift 试图在编译时捕获错误。如果您知道或认为该变量不可能包含 nil,则可以使用感叹号标记来删除检查。 - lassej

16

以下是一些例子:

var name:String = "Hello World"
var word:String?

其中word是可选值,这意味着它可能包含值也可能不包含值。

word = name 

这里name已经有一个值,所以我们可以进行赋值操作。

var cow:String = nil
var dog:String!

dog 被强制展开的意思是它必须包含一个值。

dog = cow

应用程序将崩溃,因为我们正在将 nil 赋值给已解包的变量


1
var c:Int = nil 会得到错误信息:"无法用 'nil' 初始化指定类型 'Int'"。 - Asususer

14
在这种情况下...

var John: Person!

意思是,最初John将具有空值(nil value),它会被设置并一旦设置就不会再次变成空值(nil value)。因此,为了方便起见,您可以使用可选变量的更简单语法,因为这是一个"隐式解析可选项(Implicitly unwrapped optional)"。


1
谢谢。我还找到了以下链接,解释了这个问题。这种类型被称为“隐式解包选项”。https://dev59.com/IWAf5IYBdhLWcg3w7mf3 - Kaydell

5
如果您熟悉C系列语言,您会认为“指向类型为X的对象,可能是内存地址0(NULL)”,如果您来自动态类型语言,则会认为“对象可能是类型为X但可能是未定义类型”。实际上,这两种都不正确,尽管第一种说法在某种程度上接近。您应该将其视为以下形式的对象:
struct Optional<T> {
   var isNil:Boolean
   var realObject:T
}

当你使用foo == nil测试可选值时,实际上返回的是foo.isNil。而当你使用foo!时,它会返回带有断言foo.isNil == falsefoo.realObject。需要注意的是,如果在执行foo!时,foo实际上为nil,那么就会出现运行时错误。因此,通常情况下,除非你非常确定该值不会为nil,否则应该使用条件let来代替。这种巧妙的处理方式使得语言可以强类型化,而又不需要在每个地方都测试值是否为nil。
实际上,它并不完全像这样工作,因为编译器完成了大部分工作。在高层次上,有一种名为Foo?的类型,它与Foo不同,这样可以防止接受Foo类型的函数接收到nil值。但在低层次上,可选值并不是真正的对象,因为它没有属性或方法;实际上它很可能是一个指针,可能为NULL(0),需要在强制解包时进行适当的测试。
还有一种情况会看到感叹号,那就是在类型上,例如:
func foo(bar: String!) {
    print(bar)
}

这大致相当于接受一个带有强制解包的可选项,即:
func foo(bar: String?) {
    print(bar!)
}

您可以使用此方法来接受可选值,但如果该值为nil,则会在运行时出现错误。在当前版本的Swift中,这似乎绕过了非空断言,因此您将遇到低级别的错误。通常不是一个好主意,但在从其他语言转换代码时可能很有用。


3
如果您熟悉C#,那么这就像Nullable类型一样,它们也是使用问号进行声明的:
Person? thisPerson;

在这种情况下,感叹号等同于访问可空类型的.Value属性,就像这样:

thisPerson.Value

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