[TOC]
@(swift)[溫故而知新]
面試中經常被問到
Objective-C
與Swift
的區別,其實區別還是很多的,重點整理一下個人覺得很重要的:面向協議程式設計。
一、Objective-C與Swift的異同
1.1、swift和OC的共同點:
-
OC
出現過的絕大多數概念,比如引用計數、ARC(自動引用計數)、屬性、協議、介面、初始化、擴充套件類、命名引數、匿名函式等,在Swift
中繼續有效(可能最多換個術語)。 -
Swift
和Objective-C
共用一套執行時環境,Swift
的型別可以橋接到Objective-C
(下面我簡稱OC),反之亦然
1.2、swift的優點:
- swift注重安全,
OC
注重靈活 - swift注重面向協議程式設計、函數語言程式設計、物件導向程式設計,
OC
注重物件導向程式設計 - swift注重值型別,
OC
注重指標和引用 - swift是靜態型別語言,
OC
是動態型別語言 - swift容易閱讀,檔案結構和大部分語法簡易化,只有.swift檔案,結尾不需要分號
- swift中的可選型別,是用於所有資料型別,而不僅僅侷限於類。相比於
OC
中的nil
更加安全和簡明 - swift中的泛型型別更加方便和通用,而非
OC
中只能為集合型別新增泛型 - swift中各種方便快捷的高階函式(
函數語言程式設計
) (Swift的標準陣列支援三個高階函式:map
,filter
和reduce
,以及map的擴充套件flatMap
) - swift新增了兩種許可權,細化許可權。
open
>public
>internal(預設)
>fileprivate
>private
- swift中獨有的元組型別(
tuples
),把多個值組合成複合值。元組內的值可以是任何型別,並不要求是相同型別的。
1.3、swift的不足:
- 版本不穩定
- 公司使用比例不高,使用人數比例偏低
- 有些語法其實可以只規定一種格式,不必這樣也行,那樣也行。像Go一樣禁止一切(Go有點偏激)耍花槍的東西,同一個規範,方便團隊合作和閱讀他人程式碼。
二、Objective-C中的protocol與Swift中的protocol的區別
相比於OC
,Swift
可以做到protocol
協議方法的具體預設實現(通過extension
)相比多型
更好的實現了程式碼複用,而 OC
則不行。
三、面向協議
(面向介面
)與物件導向
的區別
物件導向
和面向協議
的的最明顯區別是對抽象資料的使用方式
,物件導向採用的是繼承,而面向協議
採用的是遵守協議。在面向協議
設計中,Apple
建議我們更多的使用 值型別 (struct
)而非 引用型別 (class
)。這篇文章中有一個很好的例子說明了面向協議
比物件導向
更符合某些業務需求。其中有飛機、汽車、自行車三種交通工具(均繼承自父類交通工具);老虎、馬三種動物(均繼承父類自動物);在古代馬其實也是一種交通工具,但是父類是動物,如果馬也有交通工具的功能,則:
如果採用
物件導向程式設計
,則需要既要繼承動物,還要繼承交通工具,但是父類交通工具有些功能馬是不需要的。由此可見繼承,作為程式碼複用的一種方式,耦合性還是太強。事物往往是一系列特質的組合,而不單單是以一脈相承並逐漸擴充套件的方式構建的。以後慢慢會發現物件導向很多時候其實不能很好地對事物進行抽象。
如果採用
面向協議程式設計
,馬只需要實現出行協議就可以擁有交通工具的功能了。面向協議
就是這樣的抽離方式,更好的職責劃分,更加具象化,職責更加單一。很明顯面向協議
的目的是為了降低程式碼的耦合性。
總結:
面向協議
相對於物件導向
來說更具有可伸縮性和可重用性,並且在程式設計的過程中更加模組化,通過協議以及協議擴充套件替代一個龐大的基類,這在大規模系統程式設計中會有很大的便捷之處。
3.1、協議
和協議擴充套件
比基類
有三個明顯的優點:
-
1、型別可以遵守多個協議但是隻有一個基類。
這意味著型別可以隨意遵守任何想要特性的協議,而不需要一個巨大的基類。 -
2、不需要知道原始碼就可以使用協議擴充套件新增功能。
這意味著我們可以任意擴充套件協議,包括swift內建的協議,而不需要修改基類的原始碼。一般情況下我們會給特定的類而非類的層級(繼承體系)新增擴充套件;但是如果必要,我們依然可以給基類新增擴充套件,使所有的子類繼承新增的功能。使用協議,所有的屬性、方法和建構函式都被定義在遵守協議的型別自身中。這讓我們很容易地檢視到所有東西是怎麼被定義和初始化的。我們不需要在類的層級之間來回穿梭以檢視所有東西是如何初始化的。忘記設定超類可能沒有什麼大問題,但是在更復雜的型別中,忘記合理地設定某個屬性可能會導致意想不到的行為。 -
3、協議可以被類、結構體和列舉遵守,而類層級約束為類型別。
協議和協議擴充套件可以讓我們在更合理的地方使用值型別。引用型別和值型別的一個主要的區別就是型別是如何傳遞的。當我們傳遞引用型別(class)的例項時,我們傳遞的對原例項的引用。這意味著所做的任何更改都會反射回原例項中。當我們傳遞值型別的例項時,我們傳遞的是對原例項的一份拷貝。這意味著所做的任何更改都不會反射回原例項中。使用值型別確保了我們總是得到一個唯一的例項因為我們傳遞了一份對原例項的拷貝而非對原例項的引用。因此,我們能相信沒有程式碼中的其它部分會意外地修改我們的例項。這在多執行緒環境中尤其有用,其中不同的執行緒可以修改資料並建立意外地行為。
3.2、物件導向的特點
優點:
- 封裝
資料封裝、訪問控制、隱藏實現細節、型別抽象為類;
程式碼以邏輯關係組織到一起,方便閱讀;
高內聚、低耦合的系統結構
- 繼承
程式碼重用,繼承關係,更符合人類思維
- 多型
介面重用,父類指標能夠指向子類物件
當父類的引用指向子類物件時,就發生了向上轉型,即把子類型別物件轉成了父類型別。
向上轉型的好處是隱藏了子類型別,提高了程式碼的擴充套件性。
多型的不足:
- 父類有部分public方法是子類不需要的,也不允許子類覆蓋重寫
- 父類有一些方法是必須要子類去覆蓋重寫的,在父類的方法其實也是一個空方法
- 父類的一些方法即便被子類覆蓋重寫,父類原方法還是要執行的
- 父類的一些方法是可選覆蓋重寫的,一旦被覆蓋重寫,則以子類為準
較好的抽象型別應該:
- 更多地支援值型別,同時也支援引用型別
- 更多地支援靜態型別關聯(編譯期),同時也支援動態派發(runtime)
- 結構不龐大不復雜
- 模型可擴充套件
- 不給模型強制新增資料
- 不給模型增加初始化任務的負擔
- 清楚哪些方法該實現哪些方法不需實現
3.3、OneV`s Den提到的物件導向
的三個困境:
1、動態派發的安全性(這應該是OC的困境,在Swift中Xcode是不可能讓這種問題編譯通過的)
在
Objective-C
中下面這段程式碼編譯是不會報警告和錯誤的
NSObject *v1 = [NSObject new];
NSString *v2 = [NSString new];
NSNumber *v3 = [NSNumber new];
NSArray *array = @[v1, v2, v3];
for (id obj in array) {
[obj boolValue];
}
複製程式碼
在
Objective-C
中可以藉助泛型檢查這種潛在的問題,Xocde會提示警告
@protocol SafeProtocol <NSObject>
- (void)func;
@end
@interface SafeObj : NSObject<SafeProtocol>
@end
@implementation SafeObj
- (void)func {
}
@end
@interface UnSafeObj : NSObject
@end
@implementation UnSafeObj
@end
複製程式碼
Objective-C
在Xcode7
中,可以使用帶泛型的容器也可以解決這個問題,但是隻是⚠️,程式執行期間仍可能由於此問題導致的崩潰
SafeObj *v1 = [[SafeObj alloc] init];
UnSafeObj *v2 = [[UnSafeObj alloc] init];
// 由於v2沒有實現協議SafeProtocol,所以此處Xcode會有警告
// Object of type `UnSafeObj *` is not compatible with array element type `id<SafeProtocol>`
NSArray<id<SafeProtocol>> *array = @[v1, v2];
for (id obj in array) {
[obj func];
}
複製程式碼
使用
swift
,必須指定型別,否則不是⚠️,而是❌,所以swift
在編譯階段就可以檢查出問題
// 直接報錯,而不是警告
// Cannot convert value of type `String` to expected argument type `NSNumber`
var array: [NSNumber] = []
array.append(1)
array.append("a")
複製程式碼
2、橫切關注點
我們很難在不同的繼承體系中複用程式碼,用行話來講就是橫切關注點(Cross-Cutting Concerns
)。比如下面的關注點myMethod
,位於兩條繼承鏈 (UIViewController
-> ViewCotroller
和 UIViewController
-> UITableViewController
-> AnotherViewController
) 的橫切面上。物件導向是一種不錯的抽象方式,但是肯定不是最好的方式。它無法描述兩個不同事物具有某個相同特性這一點。在這裡,特性的組合要比繼承更貼切事物的本質。
class ViewCotroller: UIViewController {
func myMethod() {
}
}
複製程式碼
class AnotherViewController: UITableViewController {
func myMethod() {
}
}
複製程式碼
在物件導向程式設計
中,針對這種問題的幾種解決方案:
- 1、Copy & Paste
快速,但是這也是壞程式碼的開頭。我們應該儘量避免這種做法。
- 2、引入 BaseViewController
看起來這是一個稍微靠譜的做法,但是如果不斷這麼做,會讓所謂的
Base
很快變成垃圾堆。職責不明確,任何東西都能扔進Base
,你完全不知道哪些類走了Base
,而這個“超級類”對程式碼的影響也會不可預估。
- 3、依賴注入
通過外界傳入一個帶有 myMethod 的物件,用新的型別來提供這個功能。這是一個稍好的方式,但是引入額外的依賴關係,可能也是我們不太願意看到的。
- 4、多繼承
當然,
Swift
是不支援多繼承的。不過如果有多繼承的話,我們確實可以從多個父類進行繼承,並將myMethod
新增到合適的地方。有一些語言選擇了支援多繼承 (比如C++
),但是它會帶來OOP
中另一個著名的問題:菱形缺陷。
在
Swift
的面向協議程式設計
中,針對這種問題的解決方案(使用協議擴充套件新增預設實現):
protocol P {
func myMethod()
}
extension P {
func myMethod() {
doWork()
}
}
複製程式碼
extension ViewController: P { }
extension AnotherViewController: P { }
viewController.myMethod()
anotherViewController.myMethod()
複製程式碼
3、菱形問題
多繼承中,兩個父類實現了相同的方法,子類無法確定繼承哪個父類的此方法,由於多繼承的拓撲結構是一個菱形,所以這個問題有被叫做菱形缺陷(
Diamond Problem
)。
上面的例子中,如果我們有多繼承,那麼 ViewController
和 AnotherViewController
的關係可能會是這樣的:
如果ViewController
和UITableViewController
都實現了myMethod
方法,則在AnotherViewController
中就會出現菱形缺陷問題。
我們應該著眼於寫乾淨並安全的程式碼,乾淨的程式碼是非常易讀和易理解的程式碼。
四、程式設計實踐:基於protocol
的連結串列實現
import UIKit
protocol ChainListAble {
associatedtype T: Equatable
// 列印
var description: String{get}
// 數量
var count: Int{get}
/// 插入
func insertToHead(node: Node<T>)
func insertToHead(value: T)
func insertToTail(node: Node<T>)
func insertToTail(value: T)
func insert(node: Node<T>, afterNode: Node<T>) -> Bool
func insert(value: T, afterNode: Node<T>) -> Bool
func insert(node: Node<T>, beforNode: Node<T>) -> Bool
func insert(value: T, beforNode: Node<T>) -> Bool
/// 刪除(預設第一個符合條件的)
@discardableResult func delete(node: Node<T>) -> Bool
@discardableResult func delete(value: T) -> Bool
@discardableResult func delete(index: Int) -> Bool
//func delete(fromIndex: Int, toIndex: Int) -> Bool
//func deleteAll()
/// 查詢(預設第一個符合條件的)
func find(value: T) -> Node<T>?
func find(index: Int) -> Node<T>?
/// 判斷結點是否在連結串列中
func isContain(node: Node<T>) -> Bool
}
/// [值型別不能在遞迴裡呼叫](https://www.codetd.com/article/40263),因此Node型別只能是class而不是struct
// 有些時候你只能使用類而不能使用結構體,那就是遞迴裡
// struct報錯:Value type `Node` cannot have a stored property that recursively contains it
class Node<T: Equatable> {
var value: T
var next: Node?
/// 便利構造方法
///
/// - Parameter value: value
convenience init(value: T) {
self.init(value: value, next: nil)
}
/// 預設指定初始化方法
///
/// - Parameters:
/// - value: value
/// - next: next
init(value: T, next: Node?) {
self.value = value
}
// 銷燬函式
deinit {
print("(self.value) 釋放")
}
}
extension Node {
/// 返回當前結點到連結串列尾的長度
var count: Int {
var idx: Int = 1
var node: Node? = self
while node?.value != nil {
node = node?.next
idx = idx + 1
}
return idx
}
}
class SingleChainList: ChainListAble {
typealias T = String
// 哨兵結點,不儲存資料
private var dummy: Node = Node.init(value: "")
}
extension SingleChainList {
var description: String {
var description: String = ""
var tempNode = self.dummy
while let nextNode = tempNode.next {
description = description + " " + nextNode.value
tempNode = nextNode
}
return description
}
var count: Int {
var count: Int = 0
var tempNode = self.dummy
while let nextNode = tempNode.next {
count = count + 1
tempNode = nextNode
}
return count
}
/// 在頭部插入值
///
/// - Parameter value: value
func insertToHead(value: T) {
let node: Node = Node.init(value: value)
self.insertToHead(node: node)
}
/// 在頭部插入結點
///
/// - Parameter node: node
func insertToHead(node: Node<T>) {
node.next = self.dummy.next
self.dummy.next = node
}
/// 在尾部插入值
///
/// - Parameter value: value
func insertToTail(value: T) {
let node: Node = Node.init(value: value)
self.insertToTail(node: node)
}
/// 在尾部插入結點
///
/// - Parameter node: node
func insertToTail(node: Node<T>) {
var tailNode: Node = self.dummy
while let nextNode = tailNode.next {
tailNode = nextNode
}
tailNode.next = node
}
/// 在指定結點的後面插入新value
///
/// - Parameters:
/// - value: 新值
/// - afterNode: 指定結點
/// - Returns: true or false
func insert(value: T, afterNode: Node<T>) -> Bool {
let node: Node = Node.init(value: value)
return self.insert(node: node, afterNode: afterNode)
}
/// 在指定結點的後面插入新結點
///
/// - Parameters:
/// - value: 新結點
/// - afterNode: 指定結點
/// - Returns: true or false
func insert(node: Node<T>, afterNode: Node<T>) -> Bool {
guard self.isContain(node: afterNode) else {
return false
}
node.next = afterNode.next
afterNode.next = node
return true
}
/// 在指定結點的前面插入新value(雙向連結串列實現這種插入方式速度比單向連結串列快)
///
/// - Parameters:
/// - value: 新值
/// - beforNode: 指定結點
/// - Returns: true or false
func insert(value: T, beforNode: Node<T>) -> Bool {
let node: Node = Node.init(value: value)
return self.insert(node: node, beforNode: beforNode)
}
/// 在指定結點的前面插入新結點(雙向連結串列實現這種插入方式速度比單向連結串列快)
///
/// - Parameters:
/// - node: 新結點
/// - beforNode: 指定結點
/// - Returns: true or false
func insert(node: Node<T>, beforNode: Node<T>) -> Bool {
var tempNode: Node = self.dummy
while let nextNode = tempNode.next {
if nextNode === beforNode {
node.next = beforNode
tempNode.next = node
return true
}
tempNode = nextNode
}
return false
}
/// 刪除指定value的結點
///
/// - Parameter value: value
/// - Returns: true or false
func delete(value: T) -> Bool {
var tempNode: Node = self.dummy
while let nextNode = tempNode.next {
// 此處判斷 == 是否合理
if nextNode.value == value {
tempNode.next = nextNode.next
return true
}
tempNode = nextNode
}
return false
}
/// 刪除指定的結點
///
/// - Parameter node: node
/// - Returns: true or false
func delete(node: Node<T>) -> Bool {
var tempNode = self.dummy
while let nextNode = tempNode.next {
if nextNode === node {
tempNode.next = nextNode.next
return true
}
tempNode = nextNode
}
return false
}
/// 刪除指定下標的結點
///
/// - Parameter index: index
/// - Returns: true or false
func delete(index: Int) -> Bool {
var idx: Int = 0
var tempNode: Node = self.dummy
while let nextNode = tempNode.next {
if index == idx {
tempNode.next = nextNode.next
return true
}
tempNode = nextNode
idx = idx + 1
}
return false
}
/// 查詢指定值的node
///
/// - Parameter value: value
/// - Returns: node
func find(value: T) -> Node<T>? {
var tempNode = self.dummy
while let nextNode = tempNode.next {
if nextNode.value == value {
return nextNode
}
tempNode = nextNode
}
return nil
}
/// 查詢指定下標的結點
///
/// - Parameter index: index
/// - Returns: node
func find(index: Int) -> Node<T>? {
var idx: Int = 0
var tempNode: Node = self.dummy
while let nextNode = tempNode.next {
if index == idx {
return nextNode
}
tempNode = nextNode
idx = idx + 1
}
return nil
}
/// 判斷給定的連結串列是否在連結串列中
///
/// - Parameter node: node
/// - Returns: true or false
func isContain(node: Node<T>) -> Bool {
var tempNode = self.dummy.next
while tempNode != nil {
if tempNode === node {
return true
}
tempNode = tempNode?.next
}
return false
}
/// 單向連結串列反轉:方式一非遞迴實現
///
/// - Parameter chainList: 源連結串列
/// - Returns: 反轉後的連結串列
func reverseList() {
var prevNode: Node<String>? = self.dummy.next
var curNode: Node<String>? = prevNode?.next
var tempNode: Node<String>? = curNode?.next
prevNode?.next = nil
while curNode != nil {
tempNode = curNode?.next
curNode?.next = prevNode
prevNode = curNode
curNode = tempNode
}
self.dummy.next = prevNode
}
/// 單向連結串列反轉:方式二遞迴實現
///
/// - Parameter chainList: 源連結串列
/// - Returns: 反轉後的連結串列
func reverseListUseRecursion(head: Node<T>?, isFirst: Bool) {
var tHead = head
if isFirst {
tHead = self.dummy.next
}
guard let rHead = tHead else { return }
if rHead.next == nil {
self.dummy.next = rHead
return
}
else {
self.reverseListUseRecursion(head:rHead.next, isFirst: false)
rHead.next?.next = rHead
rHead.next = nil
}
}
}
class LinkedListVC: UIViewController {
var chainList: SingleChainList = SingleChainList.init()
override func viewDidLoad() {
super.viewDidLoad()
// 初始化連結串列
for i in 0..<10 {
let node: Node = Node.init(value: String(i))
chainList.insertToTail(node: node)
}
// 查詢結點
for i in 0..<12 {
if let find: Node = chainList.find(index: i) {
debugPrint("find = (find.value)")
}
else {
debugPrint("not find idx = (i)")
}
}
// 刪除結點
if chainList.delete(index: 10) {
debugPrint("刪除 index = (index)成功")
}
else {
debugPrint("刪除 index = (index)失敗")
}
// 列印結點value資訊
debugPrint(chainList.description)
// 列印結點個數
debugPrint(chainList.count)
// 單向連結串列反轉
chainList.reverseList()
// 列印結點value資訊
debugPrint(chainList.description)
// 單向連結串列反轉
chainList.reverseListUseRecursion(head: nil, isFirst: true)
// 列印結點value資訊
debugPrint(chainList.description)
}
}
複製程式碼
參考部落格
1、面向協議程式設計與 Cocoa 的邂逅 (上)
2、面向協議程式設計與 Cocoa 的邂逅 (下)
3、[譯] Swift 面向協議程式設計入門