Swift的7大误区

Swift正在完成一个惊人的壮举,它正在改变我们在苹果设备上编程的方式,引入了很多现代范例,例如:函数式编程和相比于OC这种纯面向对象语言更丰富的类型检查。

Swift语言希望通过采用安全的编程模式去帮助开发者避免bug。然而这也会不可避免的产生一些人造的陷阱,他们会在编译器不报错的情况下引入一些Bug。这些陷阱有的已经在Swift book中提到,有一些还没有。这里有七个我在去年遇到的陷阱,它们涉及Swift协议扩展、可选链和函数式编程。

协议扩展:强大但是需要谨慎使用

一个Swift类可以去继承另一个类,这种能力是强大的。继承将使类之间的特定关系更加清晰,并且支持细粒度代码分享。但是,Swift中如果不是引用类型的话(如:结构体、枚举),就不能具有继承关系。然而,一个值类型可以继承协议,同时协议可以继承另一个协议。虽然协议除了类型信息外不能包含其他代码,但是协议扩展(protocol extension)可以包含代码。照这种方式,我们可以用继承树来实现代码的分享共用,树的叶子是值类型(结构体或枚举类),树的内部和根是协议和与他们对应的扩展。

但是Swift协议扩展的实现依然是一片新的、未开发的领域,尚存在一些问题。代码并不总是按照我们期望的那样执行。因为这些问题出现在值类型(结构体与枚举)与协议组合使用的场景下,我们将使用类与协议组合使用的例子去说明这种场景下不存在陷阱。当我们重新改为使用值类型和协议的时候将会发生令人惊奇的事。

开始介绍我们的例子:classy pizza

假设这里有使用两种不同谷物制作的三种Pizza:


1

2

3

4

5

enum Grain  { case Wheat, Corn }

class  NewYorkPizza  { let crustGrain: Grain = .Wheat }

class  ChicagoPizza  { let crustGrain: Grain = .Wheat }

class CornmealPizza  { let crustGrain: Grain = .Corn  }

我们可以通过crustGrain属性取得披萨所对应的原料


1

2

3

NewYorkPizza().crustGrain     // returns Wheat

ChicagoPizza().crustGrain     // returns Wheat

CornmealPizza().crustGrain     // returns Corn

因为大多数的Pizza是用小麦(wheat)做的,这些公共代码可以放进一个超类中作为默认执行的代码。


1

2

3

4

5

6

7

8

enum Grain { case Wheat, Corn }

class Pizza {

    var crustGrain: Grain { return .Wheat }

    // other common pizza behavior

}

class NewYorkPizza: Pizza {}

class ChicagoPizza: Pizza {}

这些默认的代码可以被重载去处理其它的情况(用玉米制作)


1

2

3

class CornmealPizza: Pizza {

    override var crustGain: Grain { return .Corn }

}

哎呀!这代码是错的,并且很幸运的是编译器发现了这些错误。你能发现这个错误么?我们在第二个crustGain中少写了r。Swift通过显式的标注override避免这种错误。比如在这个例子中,我们用到了override,但是拼写错误的"crustGain"其实并没有重写任何属性,下面是修改后的代码:


1

2

3

class CornmealPizza: Pizza {

        override var crustGrain: Grain { return .Corn }

}

现在它可以通过编译并成功运行:


1

2

3

NewYorkPizza().crustGrain         // returns Wheat

ChicagoPizza().crustGrain         // returns Wheat

CornmealPizza().crustGrain         // returns Corn

同时Pizza超类允许我们的代码在不知道Pizza具体类型的时候去操作pizzas。我们可以声明一个Pizza类型的变量。


1

var pie: Pizza

但是通用类型Pizza仍然可以去得到特定类型的信息。


1

2

3

pie =  NewYorkPizza();        pie.crustGrain     // returns Wheat

pie =  ChicagoPizza();      pie.crustGrain     // returns Wheat

pie = CornmealPizza();      pie.crustGrain     // returns Corn

Swift的引用类型在这个Demo中工作的很好。但是如果这个程序涉及到并发性、竞争条件,我们可以使用值类型来避免这些。让我们来试一下值类型的Pizza吧!

这里和上面一样简单,只需要把class修改为struct即可:


1

2

3

4

5

enum Grain { case Wheat, Corn }

struct  NewYorkPizza     { let crustGrain: Grain = .Wheat }

struct  ChicagoPizza     { let crustGrain: Grain = .Wheat }

struct CornmealPizza     { let crustGrain: Grain = .Corn  }

执行


1

2

3

NewYorkPizza()    .crustGrain     // returns Wheat

ChicagoPizza()    .crustGrain     // returns Wheat

CornmealPizza()    .crustGrain     // returns Corn

当我们使用引用类型的时候,我们通过一个超类Pizza来达到目的。但是对于值类型将要求一个协议和一个协议扩展来合作完成。


1

2

3

4

5

6

7

protocol Pizza {}

extension Pizza {  var crustGrain: Grain { return .Wheat }  }

struct  NewYorkPizza: Pizza { }

struct  ChicagoPizza: Pizza { }

struct CornmealPizza: Pizza {  let crustGain: Grain = .Corn }

这段代码可以通过编译,我们来测试一下:


1

2

3

NewYorkPizza().crustGrain         // returns Wheat

ChicagoPizza().crustGrain         // returns Wheat

CornmealPizza().crustGrain         // returns Wheat  What?!

对于执行结果,我们想说cornmeal pizza并不是Wheat制作的,返回结果出现错误!哎呀!我把


1

struct CornmealPizza: Pizza {  let crustGain: Grain = .Corn }

中的  crustGrain写成了crustGain,再一次忘记了r,但是对于值类型这里没有override关键字去帮助编译器去发现我们的错误。没有编译器的帮助,我们不得不更加小心的编写代码。

在协议扩展中重写协议中的属性时要仔细核对

ok,我们把这个拼写错误改正过来:


1

struct CornmealPizza: Pizza {  let crustGrain: Grain = .Corn }

重新执行


1

2

3

NewYorkPizza().crustGrain         // returns Wheat

ChicagoPizza().crustGrain         // returns Wheat

CornmealPizza().crustGrain     // returns Corn  Hooray!

为了在讨论Pizza的时候不需要担心到底是New York, Chicago, 还是 cornmeal,我们可以使用Pizza协议作为变量的类型。


1

var pie: Pizza

这个变量能够在不同种类的Pizza中去使用


1

2

3

pie =  NewYorkPizza(); pie.crustGrain  // returns Wheat

pie =  ChicagoPizza(); pie.crustGrain  // returns Wheat

pie = CornmealPizza(); pie.crustGrain  // returns Wheat    Not again?!

为什么这个程序显示cornmeal pizza 包含wheat?Swift编译代码的时候忽略了变量的目前实际值。代码只能够使用编译时期的知道的信息,并不知道运行时期的具体信息。程序中可以在编译时期得到的信息是pie是pizza类型,pizza协议扩展返回wheat,所以在结构体CornmealPizza中的重写起不到任何作用。虽然编译器本能够在使用静态调度替换动态调度时,为潜在的错误提出警告,但它实际上并没有这么做。这里的粗心将带来巨大的陷阱。

在这种情况下,Swift提供一种解决方案,除了在协议扩展中(extension)定义crustGrain属性之外,还可以在协议中声明。


1

2

protocol  Pizza {  var crustGrain: Grain { get }  }

extension Pizza {  var crustGrain: Grain { return .Wheat }  }

在协议内声明变量并在协议拓展中定义,这样会告诉编译器关注变量pie运行时的值。

在协议中一个属性的声明有两种不同的含义,静态还是动态调度,取决于是否这个属性在协议扩展中定义。

补充了协议中变量的声明后,代码可以正常运行了:


1

2

3

pie =  NewYorkPizza();  pie.crustGrain     // returns Wheat

pie =  ChicagoPizza();  pie.crustGrain     // returns Wheat

pie = CornmealPizza();  pie.crustGrain     // returns Corn    Whew!

在协议扩展中定义的每一个属性,需要在协议中进行声明。

然而这个设法避免陷阱的方式并不总是有效的。

导入的协议不能够完全扩展。

框架(库)可以使一个程序导入接口去使用,而不必包含相关实现。例如苹果提供给我们提供了需要框架,实现了用户体验、系统设施和其他功能。Swift的扩展允许程序向导入的类、结构体、枚举和协议中添加自己的属性(这里的属性并不是存储属性)。通过协议拓展添加的属性,就好像它原来就在协议中一样。但实际上定义在协议拓展中的属性并非一等公民,因为通过协议拓展无法添加属性的声明。

我们首先实现一个框架,这个框架定义了Pizza协议和具体的类型


1

2

3

4

5

6

7

// PizzaFramework:

public protocol Pizza { }

public struct  NewYorkPizza: Pizza  { public init() {} }

public struct  ChicagoPizza: Pizza  { public init() {} }

public struct CornmealPizza: Pizza  { public init() {} }

导入框架并且扩展Pizza


1

2

3

4

5

6

import PizzaFramework

public enum Grain { case Wheat, Corn }

extension Pizza         { var crustGrain: Grain { return .Wheat    } }

extension CornmealPizza { var crustGrain: Grain { return .Corn    } }

和以前一样,静态调度产生一个错误的答案


1

2

var pie: Pizza = CornmealPizza()

pie.crustGrain                            // returns Wheat   Wrong!

这个是因为(与刚才的解释一样)这个crustGrain属性并没有在协议中声明,而是只是在扩展中定义。然而,我们没有办法对框架的代码进行修改,因此也就不能解决这个问题。因此,想要通过扩展增加其他框架的协议属性是不安全的。

不要对导入的协议进行扩展,新增可能需要动态调度的属性

正像刚才描述的那样,框架与协议扩展之间的交互,限制了协议扩展的效用,但是框架并不是唯一的限制因素,同样,类型约束也不利于协议扩展。

Attributes in restricted protocol extensions: declaration is no longer enough

回顾一下此前Pizza的例子:


1

2

3

4

5

6

7

8

enum Grain { case Wheat, Corn }

protocol  Pizza { var crustGrain: Grain { get }  }

extension Pizza { var crustGrain: Grain { return .Wheat }  }

struct  NewYorkPizza: Pizza  { }

struct  ChicagoPizza: Pizza  { }

struct CornmealPizza: Pizza  { let crustGrain: Grain = .Corn }

让我们用Pizza做一顿饭。不幸的是,并不是每顿饭都会吃pizza,所以我们使用一个通用的Meal结构体来适应各种情况。我们只需要传入一个参数就可以确定进餐的具体类型。


1

2

3

struct Meal: MealProtocol {

       let mainDish: MainDishOfMeal

}

结构体Meal继承自MealProtocol协议,它可以测试meal是否包含谷蛋白。


1

2

3

4

5

protocol MealProtocol {

    typealias MainDish_OfMealProtocol

    var mainDish: MainDish_OfMealProtocol {get}

    var isGlutenFree: Bool {get}

}

为了避免中毒,代码中使用了默认值(不含有谷蛋白)


1

2

3

extension MealProtocol {

    var isGlutenFree: Bool  { return false }

}

Swift中的 Where提供了一种方式去表达约束性协议扩展。当主菜是pizza的时候,我们知道pizza有scrustGrain属性,我们就可以访问这个属性。如果没where这里的限制,我们在不是Pizza的情况下访问scrustGrain是不安全的。


1

2

3

extension MealProtocol  where  MainDish_OfMealProtocol: Pizza {

    var isGlutenFree: Bool  { return mainDish.crustGrain == .Corn }

}

一个带有Where的扩展叫做约束性扩展。

让我们做一份美味的cornmeal Pizza


1

let meal: Meal = Meal(mainDish: CornmealPizza())

结果:


1

2

meal.isGlutenFree    // returns false

// 根据协议拓展,理论上应该返回true

正像我们在前面小节演示的那样,当发生动态调度的时候,我们应该在协议中声明,并且在协议扩展中进行定义。但是约束性扩展的定义总是静态调度的。为了防止由于意外的静态调度而引起的bug:

如果一个新的属性需要动态调度,避免使用约束性协议扩展。

使用可选链赋值和副作用

Swift可以通过静态地检查变量是否为nil来避免错误,并使用一种方便的缩略表达式,可选链,用于忽略可能出现的nil。这一点也正是Objective-C的默认行为。

不幸的是,如果可选链中被赋值的引用有可能为空,就可能导致错误,考虑下面这段代码,Holder中存放一个整数:


1

2

3

4

5

6

7

class Holder  {

    var x = 0

}

var n = 1

var h: Holder? = nil

h?.x = n++

在这段代码的最后一行中,我们把n++赋值给h的属性。除了赋值以外,变量n还会自增,我们称此为副作用。

变量n最终的值会取决于h是否为nil。如果h不为nil,那么赋值语句执行,n++也会执行。但如果h为nil,不仅赋值语句不会执行,n++也不会执行。为了避免没有发生副作用导致的令人惊讶的结果,我们应该:

避免把一个有副作用的表达式的结果通过可选链赋值给等号左边的变量

函数编程陷阱

由于Swift的支持,函数式编程的优点得以被带入苹果的生态圈中。Swift中的函数和闭包都是一等公民,不仅方便易用而且功能强大。不幸的是,其中也有一些我们需要小心避免的陷阱。

比如,inout参数会在闭包中默默的失效。

Swift的inout参数允许函数接受一个参数并直接对参数赋值,Swift的闭包支持在执行过程中引用被捕获的函数。这些特性有助于我们写出优雅易读的代码,所以你也许会把它们结合起来使用,但这种结合有可能会导致问题。

我们重写crustGrain属性来说明inout参数的使用,为简单起见,开始时先不使用闭包:


1

2

3

4

5

6

7

8

9

enum Grain {

    case Wheat, Corn

}

struct CornmealPizza {

    func setCrustGrain(inout grain: Grain)  {

        grain = .Corn

    }

}

为了测试这个函数,我们给它传一个变量作为参数。函数返回后,这个变量的值应该从Wheat变成了Corn:


1

2

3

4

let pizza = CornmealPizza()

var grain: Grain = .Wheat

pizza.setCrustGrain(&grain)

grain        // returns Corn

现在我们尝试在函数中返回闭包,然后在闭包中设置参数的值:


1

2

3

4

5

6

7

struct CornmealPizza {

    func getCrustGrainSetter() -> (inout grain: Grain) -> Void {

        return { (inout grain: Grain) in

            grain = .Corn

        }

    }

}

使用这个闭包只需要多一次调用:


1

2

3

4

5

6

var grain: Grain = .Wheat

let pizza = CornmealPizza()

let aClosure = pizza.getCrustGrainSetter()

grain            // returns Wheat (We have not run the closure yet)

aClosure(grain: &grain)

grain            // returns Corn

到目前为止一切正常,但如果我们直接把参数传进getCrustGrainSetter函数而不是闭包呢?


1

2

3

4

5

struct CornmealPizza {

    func getCrustGrainSetter(inout grain: Grain)  ->  () -> Void {

        return { grain = .Corn }

    }

}

然后再试一次:


1

2

3

4

5

6

var grain: Grain = .Wheat

let pizza = CornmealPizza()

let aClosure = pizza.getCrustGrainSetter(&grain)

print(grain)                // returns Wheat (We have not run the closure yet)

aClosure()

print(grain)                // returns Wheat  What?!?

inout参数在传入闭包的作用域外时会失效,所以:

避免在闭包中使用in-out参数

这个问题在Swift文档中提到过,但还有一个与之相关的问题值得注意,这与创建的闭包的等价方法:柯里化有关。

在使用柯里化技术时,inout参数显得前后矛盾。

在一个创建并返回闭包的函数中,Swift为函数的类型和主体提供了一种简洁的语法。尽管这种柯里化看上去仅是一种缩略表达式,但它与inout参数结合使用时却会给人们带来一些惊讶。为了说明这一点,我们用柯里化语法实现上面那个例子。函数没有声明为返回一个闭包,而是在第一个参数列表后加上了第二个参数列表,然后在函数体内省略了显式的闭包创建:


1

2

3

4

5

struct CornmealPizza {

    func getCrustGrainSetterWithCurry(inout grain: Grain)() -> Void {

        grain = .Corn

    }

}

和显式创建闭包时一样,我们调用这个函数然后返回一个闭包:


1

2

3

var grain: Grain = .Wheat

let pizza = CornmealPizza()

let aClosure = pizza.getCrustGrainSetterWithCurry(&grain)

在上面的例子中,闭包被显式创建但没能成功为inout参数赋值,但这次就成功了:


1

2

aClosure()

grain                // returns Corn

这说明在柯里化函数中,inout参数可以正常使用,但是显式的创建闭包时就不行了。

避免在柯里化函数中使用inout参数,因为如果你后来将柯里化改为显式的创建闭包,这段代码就会产生错误

总结:七个避免

  • 在协议扩展中重写协议中的属性时要仔细核对
  • 在协议扩展中定义的每一个属性,需要在协议中进行声明
  • 不要对导入的第三方协议进行属性扩展,那样可能需要动态调度
  • 如果一个新的属性需要动态调度,避免使用约束性协议扩展
  • 避免把一个有副作用的表达式的结果通过可选链赋值给等号左边的变量
  • 避免在闭包中使用inout参数
  • 避免在柯里化函数中使用inout参数,因为如果你后来将柯里化改为显式的创建闭包,这段代码就会产生错误
时间: 2024-11-09 00:43:26

Swift的7大误区的相关文章

微信公众平台营销应警惕4大误区

导语:微信公众平台营销,对于很多企业来说,都好比一块熠熠发光的翡翠,都想赢得它,越是这样,企业之间的竞争压力就更大,其中难免会有很多企业沦 为 微信公众平台营销的失败者,笔者总结这些运营失败的教训有以下几个: 1.把微信公众号当做微博来用 微信打败了微博,所以微信替代了微博,这简直就是错误的.无论微信怎么横行霸道,微信始终是微信,微博始终是微博,没有谁替代谁的问题.微博只是 沉淀下 来了,并不是没有人用了.微信跟微博相比,起码有以下两个重要区别: a.微信是"推送"消息,而不是&quo

“有则改之无则加勉”,避免互联网3大误区

这些年的互联网真是是让一些人笑了,又让一些人哭了.互联网是一把锋利的xxx剑,霹雳的开启了人们新的生活体验,又霹雳的让部分人慷概悲歌. 佛山巍建网络公是一家专门做陶瓷建材的互联网公司.最近了解到,针对部分建材行业对于互联网有3大误区,当然目前不完全包括陶瓷建材部分企业是否也进入到了这几大误区,但是要提醒广大陶瓷建材企业要"有则改之无则加勉",要做好应对互联网的准备.哪里找富婆包养的徽油气父贴吧 哪里找富婆包养的徽油气父贴吧 哪里找富婆包养的吩认同捶贴吧 哪里找富婆包养的吩认同捶贴吧 哪

DevOps热门发展趋势中的十大误区

如今的IT企业全部是自动化.新一代的代码和应用将我们带进一个融合了基础设施和云计算的时代,企业原有系统正在遭到这些新赶上的庞大的新环境的挑战. 因此,DevOps(Development和Operations的组合)作为一项新的业务脱颖而出,它的出现旨在解决复杂的系统管理员和开发者每天要面对的信息技术问题.尽管有一些组织也在实施DevOps 的方法,但还是有很多人不能完全理解DevOps 具体是什么,他们要么是抗拒,要么是意识不到这种部署的优点. DevOps是一组方法.过程与系统的统称,用于促

Swift -- 中文版两大官方文档汇总

Swift官方文档由CocoaChina翻译小组精心翻译制作而成,目前两本文档中文版已全部完成!在此,我们对所有参与的译者.组织人员以及工作人员表示衷心的感谢!本文为您提供两本文档的在线阅读以及下载!请多多关注Swift!!多多关注CocoaChina!!! The Swift Programming Language 欢迎使用Swift (一)关于Swift--About Swift (二)Swift 初见--A Swift Tour Swift -- 语言指南 (一)基础部分 -- The

【转】使用缓存的9大误区(上)

原文连接 http://www.infoq.com/cn/articles/misunderstanding-using-cache 如果说要对一个站点或者应用程序经常优化,可以说缓存的使用是最快也是效果最明显的方式.一般而言,我们会把一些常用的,或者需要花费大量的资源或时间而产生的数据缓存起来,使得后续的使用更加快速. 如果真要细说缓存的好处,还真是不少,但是在实际的应用中,很多时候使用缓存的时候,总是那么的不尽人意.换句话说,假设本来采用缓存,可以使得性能提升为100(这里的数字只是一个计量

【转载】使用缓存的9大误区(下)

本文在<使用缓存的9大误区(上)>的基础上继续讨论了使用缓存的几个误区,包括:缓存大量的数据集合,而读取其中一部分:缓存大量具有图结构的对象导致内存浪费:缓存应用程序的配置信息:使用很多不同的键指向相同的缓存项:没有及时的更新或者删除再缓存中已经过期或者失效的数据. 缓存大量的数据集合,而读取其中一部分 在很多时候,我们往往会缓存一个对象的集合,但是,我们在读取的时候,只是每次读取其中一部分. 我们举个例子来说明这个问题(例子可能不是很恰当,但是足以说明问题). 在购物站点中,常见的操作就是查

知道用杀毒软件的十大误区吗

电脑几乎成了家庭.公司必备的工具,处处都可见到它的身影.几乎每个用电脑的人都遇到过计算机病毒,也使用过杀毒软件.但是,对病毒和杀毒软件的认识许多人还存在误区.杀毒软件不是万能的,但也绝不是废物. 使用杀毒软件的十大误区 误区一:好的杀毒软件可以查杀所有的病毒 误区二:杀毒软件是专门查杀病毒的,木马专杀才是专门杀木马的 误区三:我的机器没重要数据,有病毒重装系统,不用杀毒软件 误区四:查毒速度快的杀毒软件才好 误区五:杀毒软件不管正版盗版,随便装一个能用的就行 误区六:根据任务管理器中的内存占用判

使用缓存的9大误区

如果说要对一个站点或者应用程序经常优化,可以说缓存的使用是最快也是效果最明显的方式.一般而言,我们会把一些常用的,或者需要花费大量的资源或时间而产生的数据缓存起来,使得后续的使用更加快速. 如果真要细说缓存的好处,还真是不少,但是在实际的应用中,很多时候使用缓存的时候,总是那么的不尽人意.换句话说,假设本来采用缓存,可以使得性能提升为100(这里的数字只是一个计量符号而已,只是为了给大家一个“量”的体会),但是很多时候,提升的效果只有80,70,或者更少,甚至还会导致性能严重的下降,这个现象在使

使用缓存的九大误区

为了使得后文的阐述更加的方便,也使得文章更为的完整,我们首先来看看缓存的两种形式:本地内存缓存,分布式缓存. 首先对于本地内存缓存,就是把数据缓存在本机的内存中,如下图1所示: 从上图中可以很清楚的看出: 应用程序把数据缓存在本机的内存,需要的时候直接去本机内存进行获取. 对于.NET的应用而言,在获取缓存中的数据的时候,是通过对象的引用去内存中查找数据对象的,也就说,如果我们通过引用获取了数据对象之后,我们直接修改这个对象,其实我们真正的是在修改处于内存中的那个缓存对象. 对于分布式的缓存,此