`
啸笑天
  • 浏览: 3433892 次
  • 性别: Icon_minigender_1
  • 来自: China
社区版块
存档分类
最新评论

Swift 多范式编程语言(对象,函数,协议)

 
阅读更多

Swift 的编程范式

编程范式是程序语言背后的思想。代表了程序语言的设计者认为程序应该如何被构建和执行的看法。常见的编程范式有:过程式,面向对象,函数式,泛型编程等。

一些编程语言是专门为某种特定范式设计的,例如,C 语言是过程式编程语言;Smalltalk 和 Java 是较纯粹的面向对象编程语言;Haskell,Scheme,Clojure 是函数式编程语言。

另外一些编程语言和编程范式的关系并不一一对应,如 Python,Scala,Groovy 都支持面向对象和一定程度上的函数式编程。Swift 也是支持多种编程范式的编程语言。

由于代表了语言背后的思想,编程范式很大程度上决定了语言会呈现为何种面貌。用不着深入学习,仅仅浏览代码,就能发现 Scala 和 Swift 很类似,这是因为它们支持的编程范式是类似的;Scheme 和 Swift 看起来就相差很远,这是因为它们支持的编程范式很不一样。对于理解一门编程语言而言,相对于语言的语法和编写经验,理解语言的编程范式是更重要的。因为,就像看一本书,琢磨作者如何用词,如何构建章节是很重要,但更重要的是理解书所要表达的思想。

Swift 即支持面向对象编程范式,也支持函数式编程范式,同时还支持泛型编程。Swift 支持多种编程范式是由它的目标决定的。Swift 创造的初衷就是提供一门实用的工业语言。不同于 Haskell 这类出自大学和研究机构的学术性质的编程语言。苹果推出 Swift 时就带着着明确的商业目的:Mac OS 和 iOS 系统的主要编程语言 Objective-C 已显老态,Swift 将使得苹果系统的开发者拥有一门更现代的编程语言,从而促进苹果整个生态圈的良性发展。

Swift 的设计和开发无不体现着“实用的工业语言”这一目标。这决定了 Swift 无法做极端的语言实验,它需要在理智地面对现实的基础上,谨慎地寻求突破。这就决定了 Swift 需要继承历史遗产,在照顾现在大多数程序员的现实需求基础上,面向未来有所发展。

面向对象

面向对象编程的核心概念是继承,多态,和封装。以对象构建程序的基本单元的面向对象编程语言中,继承提供了一种复用代码的方法;多态提供了更高的抽象能力,使得我们可以设计出更通用的程序;封装提供一种使用代码更为便捷安全的机制。Swift 拥有以上所有的面向对象特性。所以,Swift 是一门完备的面向对象编程语言。

Swift 继承了 Objective-C 面向对象方面的主要特性,提供以类为主的封装和继承机制。但给予了结构体(Struct)和枚举(Enum)更丰富的面向对象特征,使它们可以用于封装更为复杂的对象。另外,相对于 Objective-C,Swift 是一门更为安全的语言。

单继承,多协议

在继承上,Swift 不同于 C++ 可以继承一个或者若干个类,而类似于 Objective-C 和 Java,只能单继承。但 Swift 可以实现多个协议(Java 中对应的是接口 Interface)。这在一定程度上弥补了没有多继承的局限,同时又避免了多继承难以控制的缺陷。

除了实现协议,Swift 还可以实现多个扩展(Extension)。扩展是一种向已有的类,枚举或者结构体添加新功能的方法。扩展和 Objective-C中的分类(Category)类似,但是与 Objective-C 中的分类不同的是,Swift 中的扩展没有名字。

更强大的结构体,枚举

C++ 和 Java 等大部分面向对象编程语言主要以类(Class)作为实现面向对象的基本结构。Swift 则赋予了结构体(Struct)和枚举(Enum)更多的面向对象特征,使结构体和枚举也能承担部分数据封装工作。在其他一些语言需要用类来解决的场景中,Swift 可以使用结构体和枚举类型,而且更为合适。例如,Swift 的 Array 和 Dictionary 是用结构体实现,而不是用类实现的,这不同于大多数编程语言。

Swift 的结构体和枚举可以像类一样,完成下列事情:

  • 定义属性
  • 定义方法
  • 拥有构造器
  • 可以被扩展(extension)
  • 可以遵守协议 (protocol)

在封装这一点上,结构体和枚举几乎和类完全一致。不同的地方是,结构体和枚举是不能继承或者被继承的。所以,这两种数据类型也就没有多态性。

总结一下,Swift 中的类和其他面向对象编程语言的类一样是面向对象语言的核心概念,具有面向对象的基本特征。Swift 的结构体和枚举拥有比其他面向对象编程语言更多的面向对象特性,可以封装更复杂的对象。但不可继承,也就没有了多态性。

更多的值类型,而不是引用类型

结构体,枚举与类的另外一个区别是:结构体和枚举是值类型,而类是引用类型。

值类型在赋值和作为函数参数被传递时,实际上是在进行复制,操作的是对象的拷贝。Swift 中有大量值类型,包括 Number,String,Array,Dictionary,Tuple,Struct 和 Enum 等。

引用类型在赋值和作为函数参数被传递时,传递的是对象的引用,而并不是对象的拷贝。这些引用都指向同一个实例。对这些引用的操作,都将影响同一个实例。

在 Swift 中区分值类型和引用类型是为了让你将可变的对象和不可变的数据区分开来。可变的对象,使用引用类型;不可变的数据,使用值类型。值类型的数据,可以保证不会被意外修改。值类型的数据传递给函数,函数内部可以自由拷贝,改变值,而不用担心产生副作用。在多线程环境下,多个线程同时运行,可能会意外地错误地修改数据,这常常会是一种难以调试的 bug。而使用值类型,你可以安全地在线程间传递数据,因为值类型传递是拷贝,所以无需在线程间同步数据变化。这就可以保证代码线程环境下的安全性。

结构体是值类型,暗示了结构体应该主要用于封装数据。例如,三维坐标系中的点 Point,代表几何形状的大小的 Size 等。而类是引用类型,意味着类应该用于封装具有状态的,可以继承的对象。例如,人,动物等。

Swift 中,Array,Dictionary,String 都是值类型,它们的行为就像 C 语言中的 Int 一样。你可以像使用 Int 一样简单安全地使用 Array,而不用考虑深度拷贝之类烦人问题。Swift 增强了对值类型的支持,鼓励我们使用值类型。因为值类型更安全。更多地使用值类型,将有助于我们写出行为更可预测,更安全的代码。

更安全的语言

类型安全语言

Swift 是强类型语言,这意味着 Swift 禁止错误类型的参数继续运算。例如,你不能让 String 和 Float 相加。这与 C# ,Java 一致;而与 C,Javascript 这类弱类型语言不一样。

Swift 是静态类型语言,这意味着 Swift 中变量是在编译期进行类型检查的。编译时,编译器会尽力找出包括类型错误在内的相关错误。例如,String 和 Int 相加这种类型运算错误,编译器在编译时就能告诉你,而不会在运行时才报错。这与 C#,Java 一致;而与 Python,Ruby 这类动态类型语言不一样。

Swift 不允许不正确的类型运算或类型转换发生,所以 Swift 是类型安全的。

Swift 支持类型推导,并且有一个相当不错的类型推导器。大部分情况下,你都不用声明类型,编译器可以根据上下文为你推导出变量的类型。

安全的初始化过程

Swift 中类(包括结构体和枚举)的初始化过程类似于 Java 的设计。Swift 有一类特别的方法,被作为初始化方法,它们没有 func 前缀,而是以 init 为方法名。这不同于 Objective-C 中的初始化方法只是一个普通的方法。对于初始化方法的特殊处理可以在语言机制上保证初始化方法只被调用一次。这种机制在 Objective-C 中是不存在的,在 Objective-C 中,初始化方法就像其它的普通方法一样,可以被多次调用。

Swift 中初始化方法必须保证所有实例变量都被初始化。Swift 初始化方法会要求特殊的初始化顺序。先保证当前类的实例变量被初始化,再调用父类的初始化方法完成父类实例变量的初始化。

Swift 保证了初始化方法只会被调用一次,同时所有的实例变量都会被初始化。这使得 Swift 初始化过程很安全。

安全的重写

Swift 提供了重写(overriding)保护机制。如果要重写基类的方法,就必须在子类的重写方法前加上 overriding 关键字。这么做是向编译器声明你想提供一个重写版本。编译器会确认,基类里确实存在具有相同方法定义的方法。如果,基类中没有相同的方法定义,编译器就会报错。另一方面,如果,没有加上 overriding 关键字的方法和基类的某个方法的定义相同,编译器也会报错,以防止意外的重写行为。这样就能从两方面保证重写行为的正确性。

Optionals

Swift 中的 Optionals 让我们能够更安全地应对有可能存在,也有可能不存在的值。在 Objective-C 里我们主要依靠文档来了解一个 API 是否会返回 nil。Optionals 则让我们将这份责任交给了类型系统。如果 API 的返回值声明为 Optional,你就知道它可以是 nil。如果它不是 Optional,你就知道它不可能是 nil。

在 Swift 中,类型后面加问号 ?声明一个 Optional 类型,以及感叹号 ! 对 Optional 类型拆包都只是语法糖。Optionals 其实是由枚举实现的:

 

enum Optional<T> : Reflectable, NilLiteralConvertible {
 case None
 case Some(T)
 //...
}
 

 

也就是说,Optional 其实是一种枚举类型。我们通过语言的类型系统来明确可能为 nil 的情况。这比 Objective-C 中使用文档来说明要安全得多。

面向对象编程总结

现在绝大部分程序员的工作语言仍然是面向对象编程语言。大部分流行的现代编程语言都会允许你创建对象。使用面向对象编程语言,易于建模。因为,对象和类似乎很容易和现实世界中的事物和概念对应。但编程实践表明,任何东西都成为对象并不是一件好事情。举一个 Java 中的蹩足的例子:Java 中只有对象才能作为参数传入函数(当然还有原始类型 Primitive type)。所以为了将函数作为参数传递给另外一个函数,需要将函数包裹在一个对象中,通常会使用一个匿名类(这也是 Java 中,监听器 Listener 通常的实现方法)。而这个类不会有其他作用,只是为了让 Java 一切皆为对象的设计高兴,从而通过编译。

Java 拥有纯粹的面向对象概念。它从设计之初,就希望以一切皆为对象的纯对象模型来为世界建模。但发展到现在,Java 中加入了越来越多非对象的东西。引入了闭包,从而获得了函数式编程中的一等函数;引入泛型,从而获得了参数化的类型。这可能暗示了,这个世界是如此得丰富多彩,使用单一模型为世界建模并不会成功。

Swift 在追求统一纯粹的编程范式这一点上并不固执。Swift 完整地支持面向对象编程,拥有完备的面向对象基础概念。这使得熟悉面向对象编程的程序员学习和使用 Swift 的成本降低了。Java 或者 Objective-C 程序员对 Swift 的很多概念会觉得很熟悉。对他们而言,学习 Swift 并不困难,很快就能将 Swift 投入到实际生产之中。

同时,Swift 还一定程度上支持函数式编程风格。在适合函数式编程的场景下,同时程序员又拥有函数式编程的思维和能力时,可以使用 Swift 以函数式的编程方法改善生产力。这将在下一章详细介绍。

函数式编程

函数式编程是一种以数学函数为程序语言建模的核心的编程范式。它将计算机运算视为数学函数计算,并且避免使用程序状态以及可变对象。函数式编程思想主要有两点:

  • 以函数为程序语言建模的核心
  • 避免状态和可变性

函数是函数式编程的基石。函数式编程语言的代码就是由一个个函数组合而成的。编写函数式语言的过程就是设计函数的过程。大规模程序由成千上万的函数组成,为了有效的组合这些函数。函数式编程语言,会尽量避免状态,避免可变对象。没有可变的状态,就使得函数式语言中的函数变为了纯函数。纯函数更容易模块化,更容易理解,对于复用是友好的。

函数

函数式编程的核心是函数,函数是“头等公民”。这就像面向对象语言的主要抽象方法是类,函数式编程语言中的主要抽象方法是函数。Swift 中的函数具有函数式语言中的函数的所有特点。这使得你可以很容易地使用 Swift 写出函数式风格的代码。

高阶函数,一等函数

高阶函数,指可以将其他函数作为参数或者返回结果的函数。

一等函数,进一步扩展了函数的使用范围,使得函数成为语言中的“头等公民”。这意味函数可在任何其他语言构件(比如变量)出现的地方出现。可以说,一等函数是更严格的高阶函数。

Swift 中的函数都是一等函数,当然也都是高阶函数。

前文中举过 Java 中为了将函数作为参数传递给另外一个函数,需要将函数包裹在一个多余的匿名类中的憋足的例子。Swift 函数都是一等函数,可以直接将函数作为参数传递给另外一个函数。这就避免了 Java 里出现的这种多余的匿名类。

闭包

闭包是一个会对它内部引用的所有变量进行隐式绑定的函数。也可以说,闭包是由函数和与其相关的引用环境组合而成的实体。也可以说,函数实际上是一种特殊的闭包。

Objective-C 在后期加入了对闭包支持。闭包是一种一等函数。通过支持闭包,Objective-C 拓展其语言表达能力。但是如果与 Swift 的闭包语法相比,Objective-C 的闭包会显得有些繁重复杂。可以用一个 Swift 的闭包的例子来表现 Swift 闭包语言的简洁和优雅:

 

let r = 1...3
let t = r.map { (i: Int) -> Int in
 return i * 2
}
 

 

该例中,map 函数遍历了数组,用作为函数参数被传入的闭包处理了数组里的所有元素,并返回了一个处理过的新数组。例子中可以看到,Swift 中使用{}来创建一个匿名闭包。使用in来分割参数和返回类型。在很多情况下,由于存在类型推导,可以省略类型声明。

不变性

在介绍 Swift 的不变性之前,先讨论一下 Haskell 这门纯函数式语言。这将有助于我们对于不变性有更深刻的理解。

简单而言,Haskell 没有变量。这是因为,Haskell 追求更高级别的抽象,而变量其实是对一类低级计算机硬件:存储器空间(寄存器,内存)的抽象。变量存在的原因,可以视为计算机语言进化的遗迹,比如在初期直接操作硬件的汇编语言中,需要变量来操作存储过程。而在计算机出现之前,解决数学计算问题都是围绕构建数学函数。数学中,不存在计算机语言中这种需要重复赋值的变量。

Haskell 基于更抽象的数学模型。使用 Haskell 编程只需专注于设计数据之间的映射关系。而在数学上,表示两个数据之间映射关系的实体就是函数。这使得编写 Haskell 代码和设计数学函数的过程是一致的,Haskell 程序员的思路也更接近数学的本质。Haskell 摒弃了变量的同时,也抛弃了循环控制。这是因为没有变量,也就没有了控制循环位置的循环变量。这也很好理解。回忆一下我们在学习计算机之前的数学课程中,也无需使用到 for 这类概念。我们还是使用函数处理一个序列到另外一个序列的转换。

不变性导致另外一个结果,就是纯函数。没有可变的状态,没有可变对象,就使得函数式语言中的函数变为了纯函数。纯函数即没有副作用的函数,无论多少次执行,相同的输入就意味着相同的输出。一个纯函数的行为并不取决于全局变量、数据库的内容或者网络连接状态。纯代码天然就是模块化的:每个函数都是自包容的,并且都带有定义良好的接口。纯函数具有非常好的特性。它意味着理解起来更简单,更容易组合,测试起来更方便,线程安全性。

Swift 提供了一定程度的不变性支持。在 Swift 中,可以使用var声明普通的变量,也可以使用let快捷方便地声明不变量。

 

//变量
var mutable
//不变量
let immutable = 1
 

 

Swift 区分varlet是为了使用编译器来强制这种区分。Swift 中声明了不变量,就必须在声明时同时初始化,或者在构造器中初始化。这两个地方之外,就无法再改变不变量了。Swift 中不变量是受到鼓励的。因为,使用不变量更容易写出容易理解,容易测试,松耦合的代码。

不变性有诸多好处:

  • 更高层次的抽象。程序员可以以更接近数学的方式思考问题。

  • 更容易理解的代码。由于不存在副作用,无论多少次执行,相同的输入就意味着相同的输出。纯函数比有可变状态的函数和对象理解起来要容易简单得多。你无需再担心对象的某个状态的改变,会对它的某个行为(函数)产生影响。

  • 线程安全的代码。这意味着多线程环境下,运行代码没有同步问题。它们也不可能因为异常的发生而处于无法预测的状态中。

不像 Haskell 这种纯函数式编程语言只能申明不可变量,Swift 提供变量和不可变量两种申明方式。这使得程序员有选择的余地:在使用面向对象编程范式时,可以使用变量。在需要的情况下,Swift 也提供不变性的支持。

惰性求值

惰性计算是函数式编程语言的一个特性。在使用惰性计算时,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。惰性计算有如下优点:

  • 首先,你可以用它们来创建无限序列这样一种数据类型。因为直到需要时才会计算值,这样就可以使用惰性集合模拟无限序列。
  • 第二,减少了存储空间。因为在真正需要时才会发生计算。所以,节约了不必要的存储空间。
  • 第三,减少计算量,产生更高效的代码。因为在真正需要时才会发生计算。所以,节约那部分没有使用到的值的计算时间。例如,寻找数组中第一个符合某个条件的值。找到了之后,数组里该值之后的值都可以不必计算了。

在纯函数式编程语言,如 Haskell 中是默认进行惰性求值的。所以,Haskell 被称为惰性语言。而大多数编程语言如 Java,C++ 求值都是严格的,或者说是及早求值。Swift 默认是严格求值,也就是每一个表达式都需要求值,而不论这个表达式在实际中是否确实需要求值。但是,Swift 也同时提供语法来支持惰性求值。在需要惰性时,你需要显式声明。这为开发者在 Swift 中使用惰性提供了条件。

下面展示了一个将默认是严格求值的数组变为惰性序列的例子:

 

     let r = 1...3
        let seq = r.lazy.map {
            (i: Int) -> Int in
            print("mapping \(i)")
            return i * 2
        }
        for i in seq {
            print(i)
        }
 

 

将获得如下结果:

 

mapping 1
2
mapping 2
4
mapping 3
6
 

 

结果显示 seq 是一个惰性序列。它的值只有在需要时才会真正发生计算。

ps:假如没有懒加载:

  let r = 1...3
        let seq = r.map {
            (i: Int) -> Int in
            print("mapping \(i)")
            return i * 2
        }
        for i in seq {
            print(i)
        }
//输出
mapping 1
mapping 2
mapping 3
2
4
6

 

 

函数式编程总结

函数式编程语言并不年轻,它的历史和面向对象编程一样悠久。1958年被创造出来的 Lisp 是最古老的函数式编程语言。它比 C 语言年代更为久远。但直到最近,函数式编程思想才逐渐被重视。几乎所有新发明的编程语言都或多或少受到了函数式编程思想的影响。Python,Scala,Groovy,Swift 都有一等函数,闭包。使得你可以将函数直接传给另外一个函数,函数也能够以返回值形式被另一个函数返回。消除状态,提供不变性的好处越来越多被接受,Scala,Groovy,Swift 都提供了方便地声明不可变对象的方法,以支持你编写更趋近于函数式风格的代码。

函数编程语言有其优秀的地方,也许将来会成为一个重要的编程范式。但是,函数式编程的重要性可能更多会间接地体现在影响其他编程语言的发展上。未来,可能很难出现一门主要以函数式编程范式设计的主流编程语言。如同 Java 这样的以单一编程范式(面向对象)构建,而成为主流的编程语言的机会应该不会太多了。如同 Haskell 这样追求纯粹的函数式编程语言,更多的可能只是一个偏学术的语言实验。

容我再重复一次上一节提到的理由:这个世界是如此得丰富多彩,使用单一模式为世界建模可能并不会成功。当然,这类预测常常会被打破。如果,将来计算机领域出现了能解决所有问题的统一范式,我将很乐意再次学习和讨论它。但如果仅仅讨论现状的话,我们仍然不得不面对一个分裂和折衷的世界。

Swift 并不是一门主要以函数式编程范式构建的语言,它更多的是借鉴融合了函数式编程一些优秀思想(更灵活强大的函数,不变性的优点)。Swift 在大多数的场景下,仍然主要会以面向对象编程语言的面目出现。因为,作为另一门面向对象编程语言 Objective-C 的继任者,Swift 需要继承 Objective-C 的遗产:Cocoa。我们现在写 Swift 代码,大部分时候还是在 Cocoa 框架之上,可以说 Cocoa 就是 Swift 的实际上的标准库。在一个主要以面向对象语言编写的框架中写代码,最合适的思维方式仍然会是面向对象的。Cocoa 可以说是 Swift 得以在高起点出发的基础,也可以说其发生胎换骨变化的阻碍。

Swift 对函数式编程的支持,使得程序员多了一种选择。Swift 并不强迫程序员一定要以面向对象的方法思维。在场景合适的情况下,程序员可以选择使用函数式风格编写代码。如果确实是合适的场景,就能够改善生产力。

面向对象与函数式编程

如果,我们按语言范式给现在流行的语言分类,支持面向对象的编程语言应该会是最长的队伍。现在大部分流行的现代编程语言都是面向对象的,它们都会允许你创建对象。但同时,你会发现比较流行的几个编程语言,Python,Scala,甚至 Java 都或多或少都受到了函数式编程语言的影响。它们都引入一些函数式编程的概念,让你一定程度上可以编写出具有函数式风格的代码。

在熟悉了类面向对象编程语言之后,再接触函数式编程语言,常常会觉得耳目一新,甚至隐约觉得函数式语言会是救世良方。那我们是否应该就此彻底转向函数式编程语言呢?使用 Haskell 来拯救世界?

面向对象编程语言在大规模实践之后,我们确实更深刻地了解了它们的缺点(例如,难以编写多线程环境下的软件应用;继承并不是代码复用的好方法)。函数式语言也确实有不少优点,有些优点恰恰就能解决面向对象语言的问题(纯函数十分适应多线程环境,纯函数天生就是模块化的,对于代码复用十分友好)。但是,函数式编程也许也存在某些问题。而这些问题,可能要在更大规模的业界实践之后才会暴露出来。现在我们已经认识到,单纯以对象为世界建模是有困难的。那么以数学模型来为世界建模可能也并不会好到哪里去。而可以确信的是,它们都有自己各自擅长的领域和环境。我们仍然还无法使用某种单一的编程范式来解决所有问题。

更大的现实是无数企业已经在面向对象编程语言上做了巨大的投资,即使现在面向对象编程已经暴露出一些问题,而函数式编程又呈现出不少能解决这些问题的优点,任何一个谨慎的人都不会,也不可能马上抛弃面向对象编程,彻底全面地转向函数式编程语言。

现实的选择是支持面向对象编程的同时,提供函数式的支持。这样,在大部分面向对象游刃有余的地方,你仍然可以使用面向对象的方法。而在适合函数式编程的地方,而你又拥有函数式编程的思维和能力时,你还是可以函数式的编程方法改善生产力。

Swift 就是这样一个现实的选择。完善的面向对象支持,使 Swift 继承了 Objective-C 遗留下来的丰厚遗产。在 Swift 中使用 Objective-C 对象并不复杂。如果,你遇到一个对多线程安全性有要求的场景,考虑使用函数式风格编写这部分代码,在 Swift 中也是件轻松的事情。

泛型编程

泛型编程是另外一个有趣的话题。泛型为编程语言提供了更高层级的抽象,即参数化类型。换句话说,就是把一个原本特定于某个类型的算法或类当中的类型信息抽象出来。这个抽象出来的概念在 C++ 的 STL(Standard Template Library)中就是模版(Template)。STL 展示了泛型编程的强大之处,一出现就成为了 C++ 的强大武器。除 C++ 之外,C#,Java,Haskell 等编程语言也都引入了泛型概念。

泛型编程是一个稍微局部一些的概念,它仅仅涉及如何更抽象地处理类型。这并不足以支撑起一门语言的核心概念。我们不会听到一个编程语言是纯泛型编程的,而没有其他编程范式。但正因为泛型并不会改变程序语言的核心,所以在大多数时候,它可以很好的融入到其他的编程范式中。C++,Scala,Haskell 这些风格迥异的编程语言都支持泛型。泛型编程提供了更高的抽象层次,这意味着更强的表达能力。这对大部分编程语言来说都是一道美味佐餐美酒。

在 Swift 中,泛型得到广泛使用,许多 Swift 标准库是通过泛型代码构建出来的。例如 Swift 的数组和字典类型都是泛型集合。这样的例子在 Swift 中随处可见。

泛型函数

Swift 函数支持泛型。泛型函数通过将函数参数和返回值定义为泛型类型,使得函数可以作用于任何适合的类型。下面展示了一个简单的泛型函数:

 

func swapTwoValues<T>(inout a: T, inout b: T) {
 let temporaryA = a
 a = b
 b = temporaryA
}
 

 

泛型类型

除了泛型函数之外,Swift 还允许自定义泛型类,泛型结构体和泛型枚举。这样的泛型类型可以作用于任何类型,其用法和 Swift 提供的 Array 和 Dictionary 相同。

用一个栈(Stack)的例子展示泛型结构体的定义和使用。泛型枚举和泛型类的定义和使用方法是相同的。

 

// 定义一个泛型结构体
struct Stack<T> {
 var items = [T]()
 mutating func push(item: T) {
 items.append(item)
 }
 mutating func pop() -> T {
 return items.removeLast()
 }
}
// 使用一个泛型结构体
var stackOfStrings = Stack<String>()
stackOfStrings.push("uno")
 

 

泛型类型参数 T 被用在了三个地方:

  • 创建数组 items 时,指定了了 items 中可以存储的数据类型;
  • 指定了函数 push 的参数类型;
  • 指定了函数 pop 的返回值类型;

泛型协议

而对于协议,Swift 中没有提供类似结构体或类那样的方法来定义泛型协议。但我们可以使用 typealias 关键字定义一个该协议的关联类型,这样一定程度上可以模拟泛型协议的效果,例子如下:

 

protocol GeneratorType {
 typealias Element
 mutating func next() -> Element?
}
 

 

实现该协议的类必须定义一个别名为 Element 的关联类型。这和泛型的概念异曲同工,一定程度上实现了泛型协议。

泛型约束

在泛型的编程实践中,我们会遇到一些需要对泛型类型做进一步约束的场景。类型约束为泛型参数指定了一个类型,或者要求其实现某个特定的协议。比如,<T: Equatable> 意味着泛型参数指代的对象需要遵守 Equatable 协议。

类型约束对泛型参数的类型做了一定约束,可以强制要求泛型参数代表的类型遵守某个协议。而 where 语句可以更进一步对类型约束中声明的泛型参数所需要遵守的协议作出更详细的要求。where 语句也可以对协议的关联类型作进一步约束。比如,你可以要求两个泛型参数所遵守的协议的关联类型是相同的。

泛型编程总结

总体而言,Swift 提供了全面的泛型编程语法,让程序员可以写出抽象层次更高,更为灵活的代码,在避免了重复代码的同时,又能拥有良好的类型安全性。

总结

最后总结一下,Swift 是一门典型的多范式编程语言,支持面向对象是为了继承面向对象编程丰厚的成果;支持函数式编程,是为了探索新的可能;支持泛型编程,则是一道美味的佐餐美酒。

这使得 Swift 可以允许程序员在大部分使用面向对象就游刃有余的时候,轻松地继续使用面向对象编程;而在适合函数式编程的场景下,同时程序员又拥有函数式编程的思维和能力时,还可以使用 Swift 以函数式的编程方法改善生产力;以及任何时候程序员都可以在 Swift 中使用泛型,来提高抽象层次。

参考文档

面向协议

最近有时间,挑了几个今年WWDC中比较感兴趣的Session视频来学习,今天就抽时间整理一下关于Swift 2.0中一个比较新的概念面向协议编程。

相关的Session视频链接如下:

Session 408: Protocol-Oriented Programming in Swift

Session 414: Building Better Apps with Value Types in Swift

写在前面

面向协议编程是什么?

你可能听过类似的概念:面向对象编程、函数式编程、泛型编程,再加上苹果今年新提出的面向协议编程,这些统统可以理解为是一种编程范式。所谓编程范式,是隐藏在编程语言背后的思想,代表着语言的作者想要用怎样的方式去解决怎样的问题。不同的编程范式反应在现实世界中,就是不同的编程语言适用于不同的领域和环境,比如在面向对象编程思想中,开发者用对象来描述万事万物并试图用对象来解决所有可能的问题。编程范式都有其各自的偏好和使用限制,所以越来越多的现代编程语言开始支持多范式,使语言自身更强壮也更具适用性。

更多编程范式和相关概念请参看:维基百科:编程范式

对Swift语言所采用的编程范式感兴趣的朋友可以参看这篇文章:多范式编程语言-以 Swift 为例

面向协议编程长什么样子?

在详细解释面向协议编程之前,我们先简单地概括一下面向协议编程长什么样子?它与我们熟悉的面向对象编程有什么不一样?

简单来说,面向协议编程是在面向对象编程基础上演变而来,将程序设计过程中遇到的数据类型的抽取(抽象)由使用基类进行抽取改为使用协议(Java语言中的接口)进行抽取。更简单点举个栗子来说,一个猫类、一个狗类,我们很容易想到抽取一个描述动物的基类,也会有人想到抽取一个动物通用的协议,那后者就可以被叫做面向协议编程了。什么?就是这样而已?苹果官方那么正式的称Swift是一门支持面向协议编程的语言,难道就是这么简单的内容?当然不会,有过面向对象编程经验的人都会清楚,协议的使用限制很多,并不能适用于大多数情况下数据类型的抽象。而在Swift语言中,协议被赋予了更多的功能和更广阔的使用空间,在Swift 2.0中,更为协议增加了扩展功能,使其能够胜任绝大多数情况下数据类型的抽象,所以苹果开始声称Swift是一门支持面向协议编程的语言。

面向协议编程对比面向对象编程的好处在哪里?它会对我们程序的设计造成哪些影响?我们会在下文中继续分析。

写在中间

离开面向对象我们失去了什么?

首先,让我们来看看面向对象编程为我们带来的好处。绝大多数熟悉一种或几种面向对象编程语言的开发者都能随口说出几条面向对象编程的优点,比如数据的封装、数据访问的控制、数据类型的抽象、代码的可读性和可扩展性等。这意味着离开了面向对象编程我们也就失去了如此多的好处。

哦,天呐!不要这样好嘛?

回头仔细想想,这些好处只有面向对象编程才有嘛?苹果给了我们另一种答案:It’s Type, not Classes,是抽象的类型带给我们如此多的好处,并不是面向对象中的类,类只是抽象类型的一种方式。比如在Swift语言中,使用结构体和枚举也同样能够实现对类型的抽象、数据的封装和访问控制等,这些好处又都回来了。

那么有没有什么是类能带给我们,而结构体和枚举办不到的呢?当然有,不然我们真的可以离开面向对象了。面向对象编程还有两个非常重要的特性我们还没有提到:继承和多态。继承和多态为我们带来了丰富多彩的世界,想想我们Cocoa Touch中的框架,这才是我们所熟悉的面向对象编程,它使我们能够轻易地解决所面对的问题,并使我们的代码具有高度的可定制和可重用性。

我们的世界终于好像正常了。

拥有面向对象我们又得到了什么?

那么,面向对象编程在带给我们这么多好处的同时,是否还附带了其他一些特性呢?比如说:要花费的代价。

我们先来看出现的第一个问题,多数面向对象语言中的对象都是使用引用类型,在对象传递过程中只是将引用复制一份并指向原有的对象,这样就会出现问题。比如下面代码所示的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Book {
    var name: String
    var pages: Int
    init(name: String, pages: Int) {
        self.name = name
        self.pages = pages
    }
}
class Person {
    var name: String
    var book: Book
    init(name: String, book: Book) {
        self.name = name
        self.book = book
    }
}
let 围城 = Book(name: "围城", pages: 888)
let 小明 = Person(name: "小明", book: 围城) // 小明有一本全新的《围城》
let 小刚 = Person(name: "小刚", book: 围城) // 小刚也有一本全新的《围城》
小明.book.pages = 88 // 小明淘气把书弄坏了,只剩88页了
print(小刚.book.pages) // 输出结果:88  WTF! Where is my new book?

故事的结尾是:小刚因为弄坏书被妈妈打了~ 不对啊,小明哪去了?我也不知道~

相信大多数面向对象编程语言的开发者都明白这是引用传递的原因,通常我们的解决办法也很简单,每次赋值的时候都先拷贝一份再进行赋值。当我们尝试在上述代码中加入copy方法时,却发现在Swift中对象默认并没有copy方法,这是因为Swift更推荐使用值类型变量而不是引用类型的变量。如果真的需要调用copy方法,你可以将Book类继承自NSObject,但这样的做法真的一点都不优雅,也不够Swiftpyer。实际上我们的问题也可以采用如下的解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Book {
    var name: String
    var pages: Int
    init(name: String, pages: Int) {
        self.name = name
        self.pages = pages
    }
}
class Person {
    var name: String
    var book: Book
    init(name: String, book: Book) {
        self.name = name
        self.book = Book(name: book.name, pages: book.pages)
    }
}
let 围城 = Book(name: "围城", pages: 888)
let 小明 = Person(name: "小明", book: 围城) // 小明有一本全新的《围城》
let 小刚 = Person(name: "小刚", book: 围城) // 小刚也有一本全新的《围城》
小明.book.pages = 88 // 小明淘气把书弄坏了,只剩88页了
print(小刚.book.pages) // 输出结果:888

我们在Person的构造方法中,为book属性新创建了一本书,从而保证小明和小刚各自拥有自己的书。这个解决办法可能并不适用于所有引用类型传递的情况,那么在Swift中,最好的解决办法是什么呢?其实答案很简单,使用值类型而非引用类型。Swift中许多常见的数据类型、字符串、集合类型,以及结构体和枚举都是值类型而非引用类型,值类型的变量在赋值时会自动进行一次低消耗的值拷贝,对比对象的copy要更加高效而且不存在线程安全问题。所以我们上面这个故事的最好结局是:将Book修改为结构体类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct Book {
    var name: String
    var pages: Int
    init(name: String, pages: Int) {
        self.name = name
        self.pages = pages
    }
}
struct Person {
    var name: String
    var book: Book
    init(name: String, book: Book) {
        self.name = name
        self.book = book
    }
}
let 围城 = Book(name: "围城", pages: 888)
var 小明 = Person(name: "小明", book: 围城) // 小明有一本全新的《围城》
let 小刚 = Person(name: "小刚", book: 围城) // 小刚也有一本全新的《围城》
小明.book.pages = 88 // 小明淘气把书弄坏了,只剩88页了
print(小刚.book.pages) // 输出结果:888

小刚终于得救了~

想了解更多值类型的使用及其相关信息可以参看:Session 414: Building Better Apps with Value Types in Swift

我们刚刚使用一个例子解释了面向对象编程中使用引用类型可能出现的问题,接下来我们谈论另一个非常重要的话题:继承的代价。这并不是一个新颖的话题,自面向对象编程诞生之日起就饱受争议,我们经常要忍受着愈加繁杂和庞大的继承体系来获得代码的可重用性,而且随着继承层次的增加,代码的复杂性会加速增长,随之而来的bug也会越来越难以发现。这时我们可能需要依靠设计模式来找回我们的思路,然而大多数设计模式只能帮助你理顺你的代码结构,却在同时更加加深了你的代码的复杂度。

继承带给我们的另一个好处就是多态,多态极大地增强了我们代码的可扩展性。然而就像“能量守恒定律”一样,多态也带来了一定的负面影响,那就是类型信息的缺失。形象一点讲,就是我们常常会写出这样的代码:subClassObject as! SubClass,向下类型转换。

那么问题来了:什么是更好的抽象类型?

苹果官方对这个问题的回答如下:

  • 更多地支持值类型,同时也支持引用类型

  • 更多地支持静态类型关联(编译期),同时也支持动态派发(运行时)

  • 结构不庞大不复杂

  • 模型可扩展

  • 不给模型强制添加数据

  • 不给模型增加初始化任务的负担

  • 清楚哪些方法该实现哪些方法不需实现

其实答案就是Swift中的面向协议编程,苹果只是在自卖自夸而已。

面向协议编程

接下来我们就正式进入Swift的面向协议编程的世界。首先我们来对比如下两段示例代码,代码的功能是定义一个更具扩展性的二分查找法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Ordered {
    func precedes(other: Ordered) -> Bool { fatalError("implement me!") }
}
class Number: Ordered {
    var value: Double = 0
    override func precedes(other: Ordered) -> Bool {
        return self.value < (other as! Number).value
    }
}
func binarySearch(sortedKeys: [Ordered], forKey k: Ordered) -> Int {
    var lo = 0
    var hi = sortedKeys.count
    while hi > lo {
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid].precedes(k) { lo = mid + 1 }
        else { hi = mid }
    }
    return lo
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protocol Ordered {
    func precedes(other: Self) -> Bool
}
struct Number: Ordered {
    var value: Double = 0
    func precedes(other: Number) -> Bool {
        return self.value < other.value
    }
}
func binarySearch(sortedKeys: [T], forKey k: T) -> Int {
    var lo = 0
    var hi = sortedKeys.count
    while hi > lo {
        let mid = lo + (hi - lo) / 2
        if sortedKeys[mid].precedes(k) { lo = mid + 1 }
        else { hi = mid }
    }
    return lo
}

应该不难看出两者之间的区别以及孰优孰劣,简单解释一下前者的缺点,反过来也就是后者的优点了。

  • OC语言中没有抽象类这个概念,所有抽象类都是靠文档注释标明,这很蛋疼~

  • 其他类型若想使用该二分查找法,必须继承自Ordered抽象类,在单继承体系中,该类型将无法再继承其他类型

  • 方法参数接收的数组中,类型要求不严格,可以放入多种不同类型的Ordered子类对象

  • 基于前一点原因,为保证严谨性,必须在方法实现内部增加类型判断,这更加蛋疼~~

基于上面的例子,我们可以稍微感受到面向协议编程在扩展性上的优势了,这里再提几个注意点。

  • Swift 2.0新特性之一,将Self用于约束泛型,功能类似于OC中的instancetype,示例:extension Ordered where Self: Comparable

  • Swift 2.0另一个重要的新特性,协议可扩展,意味着你不仅可以扩展一个类型使其遵守Ordered协议,还可以直接扩展某个协议,详见如下两段代码示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 扩展类型
extension Int: Ordered {
    func precedes(other: Int) -> Bool {
        return self < other
    }
}
extension String: Ordered {
    func precedes(other: String) -> Bool {
        return self < other
    }
}
let intIndex = binarySearch([2, 3, 5, 7], forKey: 5) // 输出结果2
let stringIndex = binarySearch(["2""3""5""7"], forKey: "5"// 输出结果2
1
2
3
4
5
6
7
8
9
10
// 扩展协议
extension Comparable {
    func precedes(other: Self) -> Bool {
        return self < other
    }
}
extension Int: Ordered {}
extension String: Ordered {}
let intIndex = binarySearch([2, 3, 5, 7], forKey: 5) // 输出结果2
let stringIndex = binarySearch(["2""3""5""7"], forKey: "5"// 输出结果2

从上面的代码我们可以看出,协议可扩展所带来的功能之一就是能够为协议中的方法提供默认实现。

更多协议可扩展所带来的功能可以参看RayWenderlich上的这篇文章:

英文版:Introducing Protocol-Oriented Programming in Swift 2

中文版:Swift 2.0之初识面向协议编程

关于面向协议编程的完整示例程序可以参看苹果官方的示例代码:

完整示例程序:Crustacean

写在最后

个人总结

面向对象编程和面向协议编程最明显的区别在于程序设计过程中对数据类型的抽取(抽象)上,面向对象编程使用类和继承的手段,数据类型是引用类型;而面向协议编程使用的是遵守协议的手段,数据类型是值类型(Swift中的结构体或枚举)。

面向协议编程是在面向对象编程基础上发展而来的,而并不是完全背离面向对象编程的思想。

面向对象编程是伟大的编程思想,也是当今主流的编程思想,它的问题在于被过多的使用在其实并不需要使用它的情况下。

Swift是一门支持多编程范式的语言,既支持面向对象编程,也支持面向协议编程,同时还支持函数式编程。在项目开发过程中,控制器和视图部分由于使用系统框架,应更多采用面向对象编程的方式;而模型或业务逻辑等自定义类型部分,则应优先考虑面向协议编程。

PS. 这篇文章的写作过程持续了很长时间,中间几乎夭折,最后还是尽量将它写完整(其实后半部分写的很水)。面向协议编程是一个比较新的概念,目前只是隐约可以看出它的一些长处(在一些使用面向对象编程并不太适合的地方),不过苹果已经在自身框架中开始使用了,并确实改善了系统一些类型和方法的使用。

参考资料

Protocol-Oriented Programming in Swift

Protocol Oriented Programming

Protocol-Oriented Programming is Object-Oriented Programming

Heterogeneous vs Homogeneous Containers in Swift

If You’re Subclassing, You’re Doing It Wrong

推荐必读文章:多范式编程语言-以 Swift 为例

 

 

////////////

感谢:

http://lincode.github.io/Swift-Paradigm/

http://www.cocoachina.com/swift/20150902/12824.html

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics