Swift正在完成一项惊人的壮举,它正在改变我们在Apple设备上编程的方式,引入了许多现代范例,例如:函数式编程和更丰富的纯面向对象语言的类型检查。Swift语言希望通过采用安全的编程模式来帮助开发人员避免错误。但是,这不可避免地会产生一些人为的陷阱,它们会在编译器不报错的情况下引入一些错误。Swift书中提到了其中一些陷阱,有些则没有。以下是我在去年遇到的七个陷阱,涉及Swift协议扩展、可选链和函数式编程。ProtocolExtensions:强大但需谨慎使用一个Swift类可以继承另一个类,这个能力很强大。继承将使类之间的特定关系更加清晰,并实现细粒度的代码共享。但是在Swift中如果不是引用类型(如:结构体、枚举),则不能有继承关系。但是,一个值类型可以继承一个协议,一个协议又可以继承另一个协议。虽然协议不能包含类型信息以外的代码,但协议扩展可以包含代码。这样,我们就可以使用继承树来共享和共享代码。树的叶子是值类型(结构或枚举类),树的内部和根是协议及其相应的扩展。但是Swift协议扩展的实现仍然是一个新的、未开发的领域,并且还存在一些问题。代码并不总是像我们期望的那样执行。因为当值类型(结构和枚举)与协议结合使用时会出现这些问题,所以我们将以使用类和协议的示例来说明在这种情况下没有陷阱。当我们切换回使用值类型和协议时,将会发生惊人的事情。让我们从我们的例子开始:经典比萨假设有三种用两种不同谷物制成的比萨:enumGrain{caseWheat,Corn}classNewYorkPizza{letcrustGrain:Grain=.Wheat}classChicagoPizza{letcrustGrain:Grain=.Wheat}classCornmealPizza{letcrustGrain:Grain=.Corn}我们可以通过crustGrain属性获取披萨对应的原材料NewYorkPizza().crustGrain//returnsWheatChicagoPizza().crustGrain//returnsWheatCornmealPizza().crustGrain//returnsCorn因为大部分披萨都是用小麦(小麦)是的,这些公共代码可以作为默认执行的代码放入超类中。enumGrain{caseWheat,Corn}classPizza{varcrustGrain:Grain{return.Wheat}//othercommonpizzabehavior}classNewYorkPizza:Pizza{}classChicagoPizza:Pizza{}这些默认代码可以重载以处理其他情况(用玉米制作)classCornmealPizza:Pizza{overridevarcrustGain:粮食{回报。玉米}}哎呀!代码是错误的,幸运的是编译器发现了错误。你能发现错误吗?我们在第二个crustGain中少写r。Swift通过显式注释override避免了这个错误。例如,在这个例子中,我们使用了override,但拼写错误的“crustGain”实际上并没有覆盖任何属性。下面是修改后的代码:classCornmealPizza:Pizza{overridevarcrustGrain:Grain{return.Corn}}现在可以通过编译运行成功了:NewYorkPizza().crustGrain//returnsWheatChicagoPizza().crustGrain//returnsWheatCornmealPizza().crustGrain//returnsCornPizza超类允许我们的代码在不知道特定类型的比萨饼的情况下操作比萨饼。我们可以声明一个Pizza类型的变量。varpie:Pizza但一般类型的Pizza仍然可以获得特定类型的信息。pie=NewYorkPizza();pie.crustGrain//returnsWheatpie=ChicagoPizza();pie.crustGrain//returnsWheatpie=CornmealPizza();pie.crustGrain//returnsCornSwift引用类型在此演示中工作正常。但是如果这个程序涉及并发,竞争条件,我们可以使用值类型来避免这些。来尝尝超值型Pizza吧!这里和上面一样简单,把class改成struct即可:enumGrain{caseWheat,Corn}structNewYorkPizza{letcrustGrain:Grain=.Wheat}structChicagoPizza{letcrustGrain:Grain=.Wheat}structCornmealPizza{letcrustGrain:Grain=.Corn}executeNewYorkPizza().crustGrain//returnsWheatChicagoPizza().crustGrain//returnsWheatCornmealPizza().crustGrain//returnsCorn当我们使用引用类型时,我们使用超类Pizza来达到目的。但是值类型需要一个协议和一个协议扩展来合作。protocolPizza{}extensionPizza{varcrustGrain:Grain{return.Wheat}}structNewYorkPizza:Pizza{}structChicagoPizza:Pizza{}structCornmealPizza:Pizza{letcrustGain:Grain=.Corn}这段代码可以通过编译,测试一下:NewYorkPizza().crustGrain//returnsWheatChicagoPizza().crustGrain//returnsWheatCornmealPizza().crustGrain//returnsWheatWhat?!对于执行结果,我们想说玉米面披萨不是小麦做的,返回的结果是错误的!哎呀!我在structCornmealPizza:Pizza{letcrustGain:Grain=.Corn}中把crustGrain写成crustGain,又忘记了r,但是没有针对值类型的override关键字来帮助编译器发现我们的错误。没有编译器的帮助,我们必须更加小心地编写代码。在protocolextension中改写protocol中的properties时,一定要仔细勾选ok。让我们更正这个错字:structCornmealPizza:Pizza{letcrustGrain:Grain=.Corn}重新执行NewYorkPizza().crustGrain//returnsWheatChicagoPizza().crustGrain//returnsWheatCornmealPizza().crustGrain//returnsCornHooray!为了不在讨论披萨时纠结是纽约、芝加哥还是玉米面,我们可以使用披萨协议作为变量类型。varpie:Pizza是一个变量,可以在不同类型的披萨中使用!为什么这个程序显示玉米面披萨含有小麦?当Swift编译代码时,它会忽略变量当前的实际值。代码只能使用编译时已知的信息,并不知道运行时的具体信息。程序在编译时可以得到的信息是pie是pizza的一种,pizza协议扩展返回的是wheat,所以在结构体CornmealPizza中重写没有效果。虽然编译器在用静态分派替换动态分派时可能会警告潜在的错误,但它实际上并没有这样做。这里的粗心会导致很大的陷阱。针对这种情况,Swift提供了解决方案。除了在协议扩展(extension)中定义crustGrain属性外,也可以在协议中声明。protocolPizza{varcrustGrain:Grain{get}}extensionPizza{varcrustGrain:Grain{return.Wheat}}在协议内部声明变量,并在协议扩展中定义,告诉编译器在运行时注意变量pie的值。协议中属性的声明有两种不同的含义,静态调度或动态调度,这取决于该属性是否在协议扩展中定义。在协议中补充变量声明后,代码可以正常运行:协议扩展中定义的每个属性都需要在协议中声明。然而,这种试图避免陷阱的方法并不总是有效。无法完全扩展导入的协议。框架(库)使程序能够导入要使用的接口,而无需包括相关的实现。例如,Apple为我们提供了实现用户体验、系统设施等功能所需的框架。Swift扩展允许程序将自己的属性(这里的属性不是存储属性)添加到导入的类、结构、枚举和协议中。协议扩展添加的属性就像它最初在协议中一样。但实际上,协议扩展中定义的属性并不是一等公民,因为不能通过协议扩展添加属性声明。我们首先实现一个定义Pizza协议和特定类型的框架//PizzaFramework:publicprotocolPizza{}publicstructNewYorkPizza:Pizza{publicinit(){}}publicstructChicagoPizza:Pizza{publicinit(){}}publicstructCornmealPizza:Pizza{publicinit(){}}导入框架并扩展PizzaimportPizzaFrameworkpublicenumGrain{caseWheat,Corn}extensionPizza{varcrustGrain:Grain{return.Wheat}}extensionCornmealPizza{varcrustGrain:Grain{return.Corn}}和以前一样,静态调度会产生错误答案varpie:Pizza=CornmealPizza()pie.crustGrain//返回小麦错误!这是因为(和前面的解释一样)crustGrain属性没有在协议中声明,而只是在扩展中定义。但是,我们没有办法去修改框架的代码,所以也就没有办法解决这个问题。因此,扩展其他框架的协议属性是不安全的。不扩展引入的协议,增加可能需要动态调度的属性。正如刚才所描述的,框架和协议扩展之间的交互限制了协议扩展的效用,但框架并不是唯一的限制因素。同样,类型约束也不利于协议扩展。受限协议扩展中的属性:声明不再足够回顾之前的Pizza示例:enumGrain{caseWheat,Corn}protocolPizza{varcrustGrain:Grain{get}}extensionPizza{varcrustGrain:Grain{return.Wheat}}structNewYorkPizza:Pizza{}structChicagoPizz:披萨{}structCornmealPizza:Pizza{letcrustGrain:Grain=.Corn}让我们用披萨做一顿饭。不幸的是,并不是每顿饭都会有披萨,所以我们使用通用的Meal结构来适应每一种情况。我们只需要传入一个参数就可以确定具体的餐点类型。structMeal:MealProtocol{letmainDish:MainDishOfMeal}结构Meal继承自MealProtocol协议,可以检测餐点是否含有麸质。protocolMealProtocol{typealiasMainDish_OfMealProtocolvarmainDish:MainDish_OfMealProtocol{get}varisGlutenFree:Bool{get}}为了避免中毒,代码中使用了默认值(无麸质)extensionMealProtocol{varisGlutenFree:Bool{returnfalse}}其中在Swift中提供了一种表达绑定的方式协议扩展。当主菜是披萨时,我们知道披萨有一个crustGrain属性,所以我们可以访问这个属性。如果这里没有限制的话,我们在没有Pizza的情况下访问ScrustGrain是不安全的。extensionMealProtocolwhereMainDish_OfMealProtocol:Pizza{varisGlutenFree:Bool{returnmainDish.crustGrain==.Corn}}带有Where的扩展称为受限扩展。做一道美味的玉米面Pizzaletmeal:Meal=Meal(mainDish:CornmealPizza())结果:meal.isGlutenFree//returnsfalse//根据协议展开,理论上应该返回true。正如我们在上一节中演示的那样,当动态调度发生时,我们应该在协议中声明它并在协议扩展中定义它。但是受限扩展的定义总是静态调度的。为了防止由于意外的静态分配而导致的错误:如果新属性需要动态分配,请避免使用绑定协议扩展。使用可选链接赋值和副作用Swift可以通过静态检查变量是否为nil来避免错误,并使用一个方便的速记表达式,可选链接,来忽略可能出现的nil。这正是Objective-C的默认行为。不幸的是,如果可选链中分配的引用可能为空,则可能会导致错误。考虑以下代码,它在Holder中存储一个整数:classHolder{varx=0}varn=1varh:Holder?=nilh?.x=n++在这段代码的第一行,我们将n++赋值给h的属性。除了被赋值外,变量n也会递增,我们称之为副作用。变量n的最终值取决于h是否为nil。如果h不为nil,则执行赋值语句,同时执行n++。但是如果h为nil,不仅赋值语句不会执行,n++也不会执行。为了避免没有副作用的意外结果,我们应该:避免通过可选链将带有副作用的表达式的结果赋值给等号左边的变量带入苹果的生态系统。Swift中的函数和闭包是一等公民,不仅方便易用,而且功能强大。不幸的是,我们需要小心避免一些陷阱。例如,inout参数在闭包中静默失效。Swift的inout参数允许函数接受一个参数并直接给参数赋值,Swift的闭包支持在执行过程中引用捕获的函数。这些特性帮助我们编写出优雅可读的代码,因此您可以组合使用它们,但这种组合可能会导致问题。我们覆盖crustGrain属性来说明inout参数的使用。为简单起见,我们将在没有闭包的情况下开始:enumGrain{caseWheat,Corn}structCornmealPizza{funcsetCrustGrain(inoutgrain:Grain){grain=.Corn}}为了测试这个函数,我们将一个变量作为参数传递给它。函数返回后,这个变量的值应该从Wheat变成Corn:在闭包中设置参数的值:structCornmealPizza{funcgetCrustGrainSetter()->(inoutgrain:Grain)->Void{return{(inoutgrain:Grain)ingrain=.Corn}}}使用这个闭包只需要多调用一次:vargrain:Grain=.Wheatletpizza=CornmealPizza()letaClosure=pizza.getCrustGrainSetter()grain//returnsWheat(Wehavenotruntheclosureyet)aClosure(grain:&grain)grain//returnsCorn到目前为止一切正常,但是如果我们直接将参数传递给getCrustGrainSetter函数而不是封闭呢绒布?structCornmealPizza{funcgetCrustGrainSetter(inoutgrain:Grain)->()->Void{return{grain=.Corn}}}再试一次:vargrain:Grain=.Wheatletpizza=CornmealPizza()letaClosure=pizza.getCrustGrainSetter(&grain)print(grain)//returnsWheat(Wehavenotruntheclosureyet)aClosure()print(grain)//returnsWheatWhat?!?inout参数传到闭包作用域外时会失效,所以:避免在闭包中使用in-out参数。这个问题在It'smentionedintheSwiftdocs,但有一个相关的问题值得注意,它与创建闭包的等效方法有关:柯里化。使用柯里化技术时,inout参数看起来不一致。在创建并返回闭包的函数中,Swift为函数的类型和主体提供了简洁的语法。尽管这种柯里化看起来只是一种速记表达式,但当它与inout参数结合使用时会引起一些意外。为了说明这一点,让我们用柯里化语法来实现上面的例子。该函数没有声明返回闭包,而是在第一个参数列表之后添加了第二个参数列表,然后在函数体中省略了显式创建闭包:structCornmealPizza{funcgetCrustGrainSetterWithCurry(inoutgrain:Grain)()->Void{grain=.Corn}}与显式创建闭包时相同,我们调用此函数并返回闭包:vargrain:Grain=.Wheatletpizza=CornmealPizza()letaClosure=pizza.getCrustGrainSetterWithCurry(&grain)创建但是给inout参数赋值失败,但是这次成功了:aClosure()grain//returnsCorn这说明在curried函数中,inout参数可以正常使用,但是显式的时候就不行了以相同的方式创建闭包。避免在curry函数中使用inout参数,因为如果你稍后将currying更改为显式创建闭包,这段代码会产生错误总结:七避免在ProtocolExtensions中覆盖Protocols中的Properties仔细检查协议扩展中定义的每个属性,它需要在协议中声明。不要扩展引入的第三方协议的属性,可能需要动态调度。如果新属性需要动态调度,请避免使用绑定协议。扩展避免通过可选链将具有副作用的表达式的结果分配给等号左边的变量避免在闭包中使用inout参数避免在curried函数中使用inout参数,因为如果你稍后将curried更改为显式创建一个闭包,这段代码会产生一个错误
