春節先後一直在忙晉升的事情,整理了下以前記錄的Swift知識點,同時在網上看到的一些有意思的面試題我也加了進來,有的題目我稍微變了下,感受會更有趣,固然老規矩全部代碼在整理時都從新跑了一遍確認~ 另外後續文章的更新頻率加快,除了iOS開發上的一些文章,也會增長一些跨平臺、前端、算法等文章,也算是對本身技術棧的回顧吧
let和var前端
let用來聲明常量,var用來聲明變量。瞭解js的對於這兩個應該不陌生,可是區別仍是挺大的,尤爲是let,在js中是用來聲明變量的,const纔是用來聲明常量的。git
類型標註面試
聲明常量/變量時能夠增長類型標註,來講明存儲類型,例如算法
var message: String
複製代碼
若是不顯示說明,Swift會根據聲明時賦值自動推斷出對應類型。通常不太須要標註類型,可是以前遇到過在某些狀況下須要大量聲明時,因爲沒有標註類型,Xcode直接內存爆了致使Mac死機,後來加上就行了。編程
命名swift
常量與變量名不能包含數學符號,箭頭,保留的(或者非法的)Unicode 碼位,連線與製表符。也不能以數字開頭,可是能夠在常量與變量名的其餘地方包含數字,除此外你能夠用任何你喜歡的字符,例如api
let 🐶 = ""
let 蝸牛君 = ""
複製代碼
可是不推薦使用,騷操做太多容易閃着腰,Camel-Case還是更好的選擇。數組
另外若是須要使用Swift保留關鍵字,可使用**反引號`**包裹使用,例如安全
enum BlogStyle {
case `default`
case colors
}
複製代碼
Swift支持把多個值組合成一個複合值,稱爲元組。元組內的值能夠是任意類型,並不要求是相同類型,例如性能優化
let size = (width: 10, height: 10)
print("\(size.0)")
print("\(size.width)")
// 也能夠不對元素命名
let size = (10, 10)
複製代碼
在函數須要返回多個值時,元組很是有用。但它並不適合複雜的數據表達,而且儘可能只在臨時須要時使用。
以前有同事經過元組返回多個值,且沒有對元素命名,而後很多地方都使用了該元組,致使後面的同事接手時沒法快速理解數據含義,而且在須要改動返回數據時,必須經過代碼邏輯去查找哪些地方使用了該元組,耗費了很多時間。
複製代碼
可選類型是Swift區別與ObjC的另外一個新特性,它用於處理值可能缺失的狀況,能夠經過在類型標註後面加上一個?
來表示這是一個可選類型,例如
// 可選類型沒有明確賦值時,默認爲nil
var message: String?
// 要麼有確切值
message = "Hello"
// 要麼沒有值,爲nil
message = nil
複製代碼
Optional
其實是一個泛型枚舉,大體源碼以下:
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
/// The absence of a value.
///
/// In code, the absence of a value is typically written using the `nil`
/// literal rather than the explicit `.none` enumeration case.
case none
/// The presence of a value, stored as `Wrapped`.
case some(Wrapped)
....
}
複製代碼
因此上面的初始化還能夠這麼寫:
str = .none
複製代碼
Swift 的 nil 和 ObjC 中的 nil 並不同。在 ObjC 中,nil 是一個指向不存在對象的指針。在 Swift 中,nil 不是指針——它是一個肯定的值,用來表示值缺失。任何類型的可選狀態均可以被設置爲 nil,不僅是對象類型。
複製代碼
可選綁定
可是在平常開發過程當中,經常咱們須要判斷可選類型是否有值,而且獲取該值,這時候咱們就要用到可選綁定了
if let message = message {
...
}
guard let message = message else {
return
}
...
複製代碼
上述代碼能夠理解爲:若是 message
返回的可選 String 包含一個值,建立一個叫作 message
的新常量並將可選包含的值賦給它。
隱式解析可選類型
雖然咱們能夠經過可選綁定來獲取可選類型的值,可是對於一些除了初始化時爲nil,後續再也不爲nil的值來講,使用可選綁定會顯的臃腫.
隱式解析可選類型支持在可選類型後面加!
來直接獲取有效值, 例如
let message: String = message!
複製代碼
若是你在隱式解析可選類型沒有值的時候嘗試取值,會觸發運行時錯誤。所以Apple建議若是一個變量以後可能變成nil
的話請不要使用隱式解析可選類型,而是使用普通可選類型。
但事實是靠人爲去判別是否能使用隱式解析可選類型是很是危險的,尤爲是團隊合做,一旦出問題就會形成崩潰,所以在咱們團隊不容許使用隱式解析可選類型。
複製代碼
空合運算符
空合運算符(a ?? b
)將對可選類型 a
進行空判斷,若是 a
包含一個值就進行解封,不然就返回一個默認值 b
。表達式 a
必須是 Optional 類型。默認值 b
的類型必需要和 a
存儲值的類型保持一致。
let message: String = message ?? "Hello"
// 實際上等同於三目運算符
message != nil ? message! : "Hello"
複製代碼
區間運算符
Swift提供了幾種方便表達一個區間的值的區間運算符,I like it😀
let array = [1, 2, 3, 4, 5]
// 閉區間運算符,表示截取下標0~2的數組元素
array[0...2]
// 半開區間運算符,表示截取下標0~1的數組元素
array[0..<2]
// 單側區間運算符,表示截取開始到下標2的數組元素
array[...2]
// 單側區間運算符,表示截取從下標2到結束的數組元素
array[2...]
複製代碼
除此以外,還能夠經過 ...
或者 ..<
來鏈接兩個字符串。一個常見的使用場景就是檢查某個字符是不是合法的字符。
// 判斷是否包含大寫字母,並打印
let str = "Hello"
let test = "A"..."Z"
for c in str {
if test.contains(String(c)) {
print("\(c)是大寫字母")
}
}
// 打印 H是大寫字母
複製代碼
恆等運算符
有時候咱們須要斷定兩個常量或者變量是否引用同一個類實例。爲了達到這個目的,Swift 內建了兩個恆等運算符:
===
)!==
)運用這兩個運算符檢測兩個常量或者變量是否引用同一個實例。
++和--
Swift不支持這種寫法,ObjC還用得蠻多的。
閉包是自包含的函數代碼塊,能夠在代碼中被傳遞和使用。Swift 中的閉包與 C 和 ObjC 中的代碼塊(blocks)比較類似。
Swift 的閉包表達式擁有簡潔的風格,並鼓勵在常見場景中進行語法優化,主要優化以下:
-利用上下文推斷參數和返回值類型
-隱式返回單表達式閉包,即單表達式閉包能夠省略 return
關鍵字
-參數名稱縮寫
-尾隨閉包語法
閉包表達式語法有以下的通常形式:
{ (parameters) -> returnType in
statements
}
複製代碼
尾隨閉包
當函數的最後一個參數是閉包時,可使用尾隨閉包來加強函數的可讀性。在使用尾隨閉包時,你不用寫出它的參數標籤:
func test(closure: () -> Void) {
...
}
// 不使用尾隨閉包
test(closure: {
...
})
// 使用尾隨閉包
test() {
...
}
複製代碼
逃逸閉包
當一個閉包做爲參數傳到一個函數中,可是這個閉包在函數返回以後還可能被使用,咱們稱該閉包從函數中逃逸。例如
var completions: [() -> Void] = []
func testClosure(completion: () -> Void) {
completions.append(completion)
}
複製代碼
此時編譯器會報錯,提示你這是一個逃逸閉包,咱們能夠在參數名以前標註 @escaping
,用來指明這個閉包是容許「逃逸」出這個函數的。
var completions: [() -> Void] = []
func testEscapingClosure(completion: @escaping () -> Void) {
completions.append(completion)
}
複製代碼
另外,將一個閉包標記爲 @escaping
意味着你必須在閉包中顯式地引用 self
,而非逃逸閉包則不用。這提醒你可能會一不當心就捕獲了self
,注意循環引用。
自動閉包
自動閉包是一種自動建立的閉包,用於包裝傳遞給函數做爲參數的表達式。這種閉包不接受任何參數,讓你可以省略閉包的花括號,用一個普通的表達式來代替顯式的閉包。
而且自動閉包讓你可以延遲求值,由於直到你調用這個閉包,代碼段纔會被執行。要標註一個閉包是自動閉包,須要使用@autoclosure
。
// 未使用自動閉包,須要顯示用花括號說明這個參數是一個閉包
func test(closure: () -> Bool) {
}
test(closure: { 1 < 2 } )
// 使用自動閉包,只須要傳遞表達式
func test(closure: @autoclosure () -> String) {
}
test(customer: 1 < 2)
複製代碼
可能有這樣一個場景,定義一個Food
的枚舉,包含了一些食物,同時還支持基於這些食物能夠兩兩混合成新食物
enum Food {
case beef
case potato
case mix(Food, Food)
}
複製代碼
此時編譯器會提示你Recursive enum 'Food' is not marked 'indirect'
,緣由是由於枚舉成員裏出現了遞歸調用。所以咱們須要在枚舉成員前加上indirect
來表示該成員可遞歸。
// 標記整個枚舉是遞歸枚舉
indirect enum Food {
case beef
case potato
case mix(Food, Food)
}
// 僅標記存在遞歸的枚舉成員
enum Food {
case beef
case potato
indirect case mix(Food, Food)
}
複製代碼
更推薦第二種寫法,由於使用遞歸枚舉時,編譯器會插入一個間接層。僅標記枚舉成員,可以減小沒必要要的消耗。
存儲屬性
簡單來講,一個存儲屬性就是存儲在特定類或結構體實例裏的一個常量或變量。存儲屬性能夠是變量存儲屬性(用關鍵字 var
定義),也能夠是常量存儲屬性(用關鍵字 let
定義)。
struct Person {
var name: String
var height: CGFloat
}
複製代碼
同時咱們還能夠經過Lazy
來標示該屬性爲延遲存儲屬性,相似於ObjC常說的的懶加載。
lazy var fileName: String = "data.txt"
複製代碼
若是一個被標記爲 lazy 的屬性在沒有初始化時就同時被多個線程訪問,則沒法保證該屬性只會被初始化一次,也就是說它是非線程安全的。
複製代碼
計算屬性
除存儲屬性外,類、結構體和枚舉能夠定義計算屬性。計算屬性不直接存儲值,而是提供一個 getter 和一個可選的 setter,來間接獲取和設置其餘屬性或變量的值。
struct Rect {
var origin = CGPoint.zero
var size = CGSize.zero
var center: CGPoint {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set {
origin.x = newValue.x - (size.width / 2)
origin.y = newValue.y - (size.height / 2)
}
}
}
複製代碼
若是咱們只但願可讀而不可寫時,setter
方法不提供便可,能夠簡寫爲
var center: CGPoint {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
複製代碼
觀察器
Swift提供了很是方便的觀察屬性變化的方法,每次屬性被設置值的時候都會調用屬性觀察器,即便新值和當前值相同的時候也不例外。
var origin: CGPoint {
willSet {
print("\(newValue)")
}
didSet {
print("\(oldValue)")
}
}
複製代碼
調用時序
調用 number
的 set
方法能夠看到工做的順序
let b = B()
b.number = 0
// 輸出
// get
// willSet
// set
// didSet
複製代碼
爲何有個get
?
這是由於咱們實現了 didSet
,didSet
中會用到 oldValue
,而這個值須要在整個 set
動做以前進行獲取並存儲待用,不然將沒法確保正確性。若是咱們不實現 didSet
的話,此次 get
操做也將不存在。
你能夠在聲明屬性或者變量時,在前面加上關鍵unowned`表示這是一個無主引用。使用無主引用,你必須確保引用始終指向一個未銷燬的實例。
和weak相似,unowned不會緊緊保持住引用的實例。它也被用於解決可能存在循環引用,且對象是非可選類型的場景。
例如在這樣的設計中:一個客戶可能有或者沒有信用卡,可是一張信用卡老是關聯着一個客戶。
class Customer {
let name: String
var card: CreditCard?
}
class CreditCard {
let number: UInt64
unowned let customer: Customer // 因爲始終有值,沒法使用weak
}
複製代碼
is
在功能上至關於ObjC的 isKindOfClass
,能夠檢查一個對象是否屬於某類型或其子類型。is
和原來的區別主要在於亮點,首先它不只能夠用於 class
類型上,也能夠對 Swift 的其餘像是 struct
或 enum
類型進行判斷。
class ClassA { }
class ClassB: ClassA { }
let obj: AnyObject = ClassB()
if (obj is ClassA) {
print("屬於 ClassA")
}
if (obj is ClassB) {
print("屬於 ClassB")
}
複製代碼
某類型的一個常量或變量可能在幕後實際上屬於一個子類。當肯定是這種狀況時,你能夠嘗試向下轉到它的子類型,用類型轉換操做符(as?
或 as!
)。
class Media {}
class Movie: Media {}
class Song: Media {}
for item in medias {
if let movie = item as? Movie {
print("It's Movie")
} else if let song = item as? Song {
print("It's Song")
}
}
複製代碼
as?
返回一個你試圖向下轉成的類型的可選值。 as!
把試圖向下轉型和強制解包轉換結果結合爲一個操做。只有你能夠肯定向下轉型必定會成功時,才使用as!
。
如同隱式解析可選類型同樣,as!一樣具備崩潰的高風險,咱們通常不容許使用。
複製代碼
Swift支持經過Extension爲已有類型添加新下標,例如
extension Int {
subscript(digitIndex: Int) -> Int {
var decimalBase = 1
for _ in 0..<digitIndex {
decimalBase *= 10
}
return (self / decimalBase) % 10
}
}
746381295[0]
// 返回 5
746381295[1]
// 返回 9
746381295[2]
// 返回 2
746381295[8]
// 返回 7
複製代碼
若是該 Int
值沒有足夠的位數,即下標越界,那麼上述下標實現會返回 0
,猶如在數字左邊自動補 0
:
746381295[9]
// 返回 0,即等同於:
0746381295[9]
複製代碼
結構體和枚舉類型中修改 self
或其屬性的方法必須將該實例方法標註爲 mutating
,不然沒法在方法裏改變本身的變量。
struct MyCar {
var color = UIColor.blue
mutating func changeColor() {
color = UIColor.red
}
}
複製代碼
因爲Swift 的 protocol 不只能夠被 class
類型實現,也適用於 struct
和 enum
,所以咱們在寫給別人用的接口時須要多考慮是否使用 mutating
來修飾方法。
實現協議中的 mutating 方法時,如果類類型,則不用寫 mutating 關鍵字。而對於結構體和枚舉,則必須寫 mutating 關鍵字。
複製代碼
有時候須要同時遵循多個協議,例如一個函數但願某個參數同時知足ProtocolA和ProtocolB,咱們能夠採用 ProtocolA & ProtocolB
這樣的格式進行組合,稱爲 協議合成(protocol composition)。
func testComposition(protocols: ProtocolA & ProtocolB) {
}
複製代碼
在開發中經常有面臨這樣的代碼
btn.addTarget(self, action: #selector(onClick(_:)), for: .touchUpInside)
@objc func onClick(_ sender: UIButton) {
}
複製代碼
爲何要使用@objc?
由於Swift 中的 #selector
是從暴露給 ObjC 的代碼中獲取一個 selector,因此它仍然是 ObjC runtime 的概念,若是你的 selector 對應的方法只在 Swift 中可見的話 (也就是說它是一個 Swift 中的 private 方法),在調用這個 selector 時你會遇到一個 unrecognized selector 錯誤。
有些時候咱們會但願在方法內部直接修改輸入的值,這時候咱們可使用 inout
來對參數進行修飾:
func addOne(_ variable: inout Int) {
variable += 1
}
複製代碼
由於在函數內部就更改了值,因此也不須要返回了。調用也要改變爲相應的形式,在前面加上 &
符號:
incrementor(&luckyNumber)
複製代碼
在 ObjC 中單例通常寫成:
@implementation MyManager
+ (id)sharedManager {
static MyManager *staticInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
staticInstance = [[self alloc] init];
});
return staticInstance;
}
@end
複製代碼
但在Swift中變得很是簡潔:
static let sharedInstance = MyManager()
複製代碼
咱們經常可能使用這樣的方式來獲取隨機數,好比100之內的:
let randomNum: Int = arc4random() % 100
複製代碼
此時編譯器會提示error,由於arc4random()
返回UInt32,須要作類型轉換,有時候咱們可能就直接:
let randomNum: Int = Int(arc4random()) % 100
複製代碼
結果測試的時候發如今有些機型上就崩潰了。
這是由於Int在32位機器上(iPhone5及如下)至關於Int32,64位機器上至關於Int64,表現上與ObjC中的NSInteger一致,而arc4random()
始終返回UInt32,因此在32位機器上就可能越界崩潰了。
最快捷的方式能夠先取餘以後再類型轉換:
let randomNum: Int = Int(arc4random() % 100)
複製代碼
若是想要一個可變參數的函數只須要在聲明參數時在類型後面加上 ...
就能夠了。
func sum(input: Int...) -> Int {
return input.reduce(0, combine: +)
}
print(sum(1,2,3,4,5))
複製代碼
Swift protocol
自己不容許可選項,要求全部方法都是必須得實現的。可是因爲Swift和ObjC能夠混編,那麼爲了方便和ObjC打交道,Swift支持在 protocol
中使用 optional
關鍵字做爲前綴來定義可選要求,且協議和可選要求都必須帶上@objc
屬性。
@objc protocol CounterDataSource {
@objc optional func incrementForCount(count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}
複製代碼
可是標記 @objc
特性的協議只能被繼承自 ObjC 類的類或者 @objc
類遵循,其餘類以及結構體和枚舉均不能遵循這種協議。這對於Swift protocol
是一個很大的限制。
因爲 protocol
支持可擴展,那麼咱們能夠在聲明一個 protocol
以後再用extension
的方式給出部分方法默認的實現,這樣這些方法在實際的類中就是可選實現的了。
protocol CounterDataSource {
func incrementForCount(count: Int) -> Int
var fixedIncrement: Int { get }
}
extension CounterDataSource {
func incrementForCount(count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}
class Counter: CounterDataSource {
var fixedIncrement: Int = 0
}
複製代碼
有這麼一個例子:
protocol A2 {
func method1()
}
extension A2 {
func method1() {
return print("hi")
}
func method2() {
return print("hi")
}
}
struct B2: A2 {
func method1() {
return print("hello")
}
func method2() {
return print("hello")
}
}
let b2 = B2()
b2.method1()
b2.method2()
複製代碼
打印的結果以下:
hello
hello
複製代碼
結果看起來在乎料之中,那若是咱們稍做改動:
let a2 = b2 as A2
a2.method1()
a2.method2()
複製代碼
此時結果是什麼呢?還與以前同樣麼?打印結果以下:
hello
hi
複製代碼
對於 method1
,由於它在 protocol
中被定義了,所以對於一個被聲明爲遵照接口的類型的實例 (也就是對於 a2
) 來講,能夠肯定實例必然實現了 method1
,咱們能夠放心大膽地用動態派發的方式使用最終的實現 (不論它是在類型中的具體實現,仍是在接口擴展中的默認實現);可是對於 method2
來講,咱們只是在接口擴展中進行了定義,沒有任何規定說它必須在最終的類型中被實現。在使用時,由於 a2
只是一個符合 A2
接口的實例,編譯器對 method2
惟一能肯定的只是在接口擴展中有一個默認實現,所以在調用時,沒法肯定安全,也就不會去進行動態派發,而是轉而編譯期間就肯定的默認實現。
Swift 的類型分爲值類型和引用類型兩種,值類型在傳遞和賦值時將進行復制,而引用類型則只會使用引用對象的一個 "指向"。
值類型有哪些?
Swift 中的 struct
和 enum
定義的類型是值類型,使用 class
定義的爲引用類型。頗有意思的是,Swift 中的全部的內建類型都是值類型,不只包括了傳統意義像 Int
,Bool
這些,甚至連 String
,Array
以及 Dictionary
都是值類型的。
值類型的好處?
相較於傳統的引用類型來講,一個很顯而易見的優點就是減小了堆上內存分配和回收的次數。值類型的一個特色是在傳遞和賦值時進行復制,每次複製確定會產生額外開銷,可是在 Swift 中這個消耗被控制在了最小範圍內,在沒有必要複製的時候,值類型的複製都是不會發生的。
var a = [1,2,3]
var b = a
let c = b
b.append(5) // 此時 a,c 和 b 的內存地址再也不相同
複製代碼
只有當值類型的內容發生改變時,值類型被纔會複製。
值類型的弊端?
在少數狀況下,咱們顯然也可能會在數組或者字典中存儲很是多的東西,而且還要對其中的內容進行添加或者刪除。在這時,Swift 內建的值類型的容器類型在每次操做時都須要複製一遍,即便是存儲的都是引用類型,在複製時咱們仍是須要存儲大量的引用,這個開銷就變得不容忽視了。
最佳實踐
針對上述問題,咱們能夠經過 Cocoa 中的引用類型的容器類來對應這種狀況,那就是 NSMutableArray
和 NSMutableDictionary
。
因此,在使用數組合字典時的最佳實踐應該是,按照具體的數據規模和操做特色來決定到時是使用值類型的容器仍是引用類型的容器:在須要處理大量數據而且頻繁操做 (增減) 其中元素時,選擇 NSMutableArray
和 NSMutableDictionary
會更好,而對於容器內條目小而容器自己數目多的狀況,應該使用 Swift 語言內建的 Array
和 Dictionary
。
let str = "Hello"
print("\(type(of: str))")
print("\(String(describing: object_getClass(str)))")
// String
// Optional(NSTaggedPointerString)
複製代碼
KVO在Cocoa中是很是強大的特性,在ObjC中有很是多的應用,以前在《iOS開發小記-基礎篇》中有相關介紹,感興趣的同窗能夠移步這篇文章。
在 Swift 中咱們也是可使用 KVO 的,可是僅限於在 NSObject
的子類中。這是能夠理解的,由於 KVO 是基於 KVC (Key-Value Coding) 以及動態派發技術實現的,而這些東西都是 ObjC 運行時的概念。另外因爲 Swift 爲了效率,默認禁用了動態派發,所以想用 Swift 來實現 KVO,咱們還須要作額外的工做,那就是將想要觀測的對象標記爲 @objc dynamic`。
例如,按照ObjC的使用習慣,咱們每每會這麼實現:
class Person: NSObject {
@objc dynamic var isHealth = true
}
private var familyContext = 0
class Family: NSObject {
var grandpa: Person
override init() {
grandpa = Person()
super.init()
print("爺爺身體情況: \(grandpa.isHealth ? "健康" : "不健康")")
grandpa.addObserver(self, forKeyPath: "isHealth", options: [.new], context: &familyContext)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.grandpa.isHealth = false
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if let isHealth = change?[.newKey] as? Bool,
context == &familyContext {
print("爺爺身體情況發生了變化:\(isHealth ? "健康" : "不健康")")
}
}
}
複製代碼
但實際上Swift 4經過閉包優化了KVO的實現,咱們能夠將上述例子改成:
class Person: NSObject {
@objc dynamic var isHealth = true
}
class Family: NSObject {
var grandpa: Person
var observation: NSKeyValueObservation?
override init() {
grandpa = Person()
super.init()
print("爺爺身體情況: \(grandpa.isHealth ? "健康" : "不健康")")
observation = grandpa.observe(\.isHealth, options: .new) { (object, change) in
if let isHealth = change.newValue {
print("爺爺身體情況發生了變化:\(isHealth ? "健康" : "不健康")")
}
}
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 3) {
self.grandpa.isHealth = false
}
}
}
複製代碼
這樣看上去是否是友好了許多?
在《WWDC-What's New in Foundation》專題上介紹 KVO
時,提到了observe
會返回一個 NSKeyValueObservation
對象,開發者只須要管理它的生命週期,而再也不須要移除觀察者,所以不用擔憂忘記移除而致使crash。
你能夠試試將
observation = grandpa.observe
改成let observation = grandpa.observe
,看看有什麼不一樣?
開發上的弊端
在 ObjC 中咱們幾乎能夠沒有限制地對全部知足 KVC 的屬性進行監聽,而如今咱們須要屬性有 @objc dynamic
進行修飾。但在不少狀況下,監聽的類屬性並不知足這個條件且沒法修改。目前可行的一個方案是經過屬性觀察器來實現一套本身的相似替代。
喵神在關於這一節的講述,因爲Swift版本較早,提出「一個可能可行的方案是繼承這個類而且將須要觀察的屬性使用 `dynamic` 進行重寫。」
但實際上僅使用`dynamic`修飾是不夠的,Swift 4開始還得配合`@objc`使用,可是繼承後再添加`@objc`是沒法編譯經過的。(這一點也容易理解,由於父類對於ObjC來講,已經不是`NSObject`的子類了)
複製代碼
前面提到過lazy
能夠用來標示屬性延遲加載,它還能夠配合像 map
或是 filter
這類接受閉包並進行運行的方法一塊兒,讓整個行爲變成延時進行的。在某些狀況下這麼作也對性能會有不小的幫助。
例如,直接使用 map
時:
let data = 1...3
let result = data.map {
(i: Int) -> Int in
print("正在處理 \(i)")
return i * 2
}
print("準備訪問結果")
for i in result {
print("操做後結果爲 \(i)")
}
print("操做完畢")
複製代碼
其輸出爲:
// 正在處理 1
// 正在處理 2
// 正在處理 3
// 準備訪問結果
// 操做後結果爲 2
// 操做後結果爲 4
// 操做後結果爲 6
// 操做完畢
複製代碼
而若是咱們先進行一次 lazy
操做的話,咱們就能獲得延時運行版本的容器:
let data = 1...3
let result = data.lazy.map {
(i: Int) -> Int in
print("正在處理 \(i)")
return i * 2
}
print("準備訪問結果")
for i in result {
print("操做後結果爲 \(i)")
}
print("操做完畢")
複製代碼
此時的運行結果:
// 準備訪問結果
// 正在處理 1
// 操做後結果爲 2
// 正在處理 2
// 操做後結果爲 4
// 正在處理 3
// 操做後結果爲 6
// 操做完畢
複製代碼
對於那些不須要徹底運行,可能提早退出的狀況,使用 lazy
來進行性能優化效果會很是有效。
有時候咱們會想要將當前的文件名字和那些必要的信息做爲參數一塊兒打印出來,Swift 爲咱們準備了幾個頗有用的編譯符號,用來處理相似這樣的需求,它們分別是:
符號 | 類型 | 描述 |
---|---|---|
#file | String | 包含這個符號的文件的路徑 |
#line | Int | 符號出現處的行號 |
#column | Int | 符號出現處的列 |
#function | String | 包含這個符號的方法名字 |
這樣,咱們能夠經過使用這些符號來寫一個好一些的 Log 輸出方法:
override func viewDidLoad() {
super.viewDidLoad()
detailLog(message: "嘿,這裏有問題")
}
func detailLog<T>(message: T, file: String = #file, method: String = #function, line: Int = #line) {
#if DEBUG
print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
#endif
}
複製代碼
咱們經常會對數組使用 map
方法,這個方法能對數組中的全部元素應用某個規則,而後返回一個新的數組。
例如但願將數組中的全部數字乘2:
let nums = [1, 2, 3]
let result = nums.map{ $0 * 2 }
print("\(result)")
// 輸出:[2, 4, 6]
複製代碼
但若是改爲對某個Int?
乘2呢?指望若是這個 Int?
有值的話,就取出值進行乘 2 的操做;若是是 nil
的話就直接將 nil
賦給結果。
let num: Int? = 3
// let num: Int? = nil
var result: Int?
if let num = num {
result = num * 2
}
print("\(String(describing: result))")
// num = 3時,打印Optional(6)
// num = nil時,打印nil
複製代碼
但其實有更優雅簡潔的寫法,那就是Optional Map
。不只在 Array
或者說 CollectionType
裏能夠用 map
,在 Optional
的聲明的話,會發現它也有一個 map
方法:
/// Evaluates the given closure when this `Optional` instance is not `nil`,
/// passing the unwrapped value as a parameter.
///
/// Use the `map` method with a closure that returns a non-optional value.
/// This example performs an arithmetic operation on an
/// optional integer.
///
/// let possibleNumber: Int? = Int("42")
/// let possibleSquare = possibleNumber.map { $0 * $0 }
/// print(possibleSquare)
/// // Prints "Optional(1764)"
///
/// let noNumber: Int? = nil
/// let noSquare = noNumber.map { $0 * $0 }
/// print(noSquare)
/// // Prints "nil"
///
/// - Parameter transform: A closure that takes the unwrapped value
/// of the instance.
/// - Returns: The result of the given closure. If this instance is `nil`,
/// returns `nil`.
@inlinable public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
複製代碼
如同函數說明描述,這個方法能讓咱們很方便地對一個 Optional 值作變化和操做,而沒必要進行手動的解包工做。輸入會被自動用相似 Optinal Binding 的方式進行判斷,若是有值,則進入 transform
的閉包進行變換,並返回一個 U?
;若是輸入就是 nil
的話,則直接返回值爲 nil
的 U?
。
所以,剛纔的例子能夠改成:
let num: Int? = 3
// let num: Int? = nil
let result = num.map { $0 * 2 }
print("\(String(describing: result))")
// num = 3時,打印Optional(6)
// num = nil時,打印nil
複製代碼
在一開始在Swift寫代理時,咱們可能會這麼寫:
protocol MyProyocol {
func method()
}
class MyClass: NSObject {
weak var delegate: MyProyocol?
}
複製代碼
而後就會發現編譯器會提示錯誤'weak' must not be applied to non-class-bound 'MyProyocol'; consider adding a protocol conformance that has a class bound
。
這是由於 Swift 的 protocol
是能夠被除了 class
之外的其餘類型遵照的,而對於像 struct
或是 enum
這樣的類型,自己就不經過引用計數來管理內存,因此也不可能用 weak
這樣的 ARC 的概念來進行修飾。
所以想要在 Swift 中使用 weak delegate
,咱們就須要將 protocol 限制在 class 內,例如
protocol MyProyocol: class {
func method()
}
protocol MyProyocol: NSObjectProtocol {
func method()
}
複製代碼
class
限制協議只用在class
中,NSObjectProtocol
限制只用在NSObject
中,明顯class
的範圍更廣,平常開發中均可以使用。
在ObjC平常開發中, @synchronized
接觸會比較多,這個關鍵字能夠用來修飾一個變量,併爲其自動加上和解除互斥鎖,用以保證變量在做用範圍內不會被其餘線程改變。
但不幸的是Swift 中它已經不存在了。其實 @synchronized
在幕後作的事情是調用了 objc_sync
中的 objc_sync_enter
和 objc_sync_exit
方法,而且加入了一些異常判斷。所以,在 Swift 中,若是咱們忽略掉那些異常的話,咱們想要 lock 一個變量的話,能夠這樣寫:
private var isResponse: Bool {
get {
objc_sync_enter(lockObj)
let result = _isResponse
objc_sync_exit(lockObj)
return result
}
set {
objc_sync_enter(lockObj)
_isResponse = newValue
objc_sync_exit(lockObj)
}
}
複製代碼
所謂字面量,就是指像特定的數字,字符串或者是布爾值這樣,可以直截了當地指出本身的類型併爲變量進行賦值的值。好比在下面:
let aNumber = 3
let aString = "Hello"
let aBool = true
複製代碼
在開發中咱們可能會遇到下面這種狀況:
public struct Thermometer {
var temperature: Double
public init(temperature: Double) {
self.temperature = temperature
}
}
複製代碼
想要建立一個Thermometer
對象,可使用以下代碼:
let t: Thermometer = Thermometer(temperature: 20.0)
複製代碼
可是實際上Thermometer
的初始化僅僅只須要一個Double
類型的基礎數據,若是能經過字面量來賦值該多好,好比:
let t: Thermometer = 20.0
複製代碼
其實Swift 爲咱們提供了一組很是有意思的接口,用來將字面量轉換爲特定的類型。對於那些實現了字面量轉換接口的類型,在提供字面量賦值的時候,就能夠簡單地按照接口方法中定義的規則「無縫對應」地經過賦值的方式將值轉換爲對應類型。這些接口包括了各個原生的字面量,在實際開發中咱們常常可能用到的有:
ExpressibleByNilLiteral
ExpressibleByIntegerLiteral
ExpressibleByFloatLiteral
ExpressibleByBooleanLiteral
ExpressibleByStringLiteral
ExpressibleByArrayLiteral
ExpressibleByDictionaryLiteral
這樣,咱們就能夠實現剛纔的設想啦:
extension Thermometer: ExpressibleByFloatLiteral {
public typealias FloatLiteralType = Double
public init(floatLiteral value: Self.FloatLiteralType) {
self.temperature = value
}
}
let t: Thermometer = 20.0
複製代碼
struct
是值類型,class
是引用類型。struct
有一個自動生成的成員逐一構造器,用於初始化新結構體實例中成員的屬性;而class
沒有。struct
中修改 self
或其屬性的方法必須將該實例方法標註爲 mutating
;而class
並不須要。struct
不能夠繼承,class
能夠繼承。struct
賦值是值拷貝,拷貝的是內容;class
是引用拷貝,拷貝的是指針。struct
是自動線程安全的;而class
不是。struct
存儲在stack
中,class
存儲在heap
中,struct
更快。通常的建議是使用最小的工具來完成你的目標,若是struct
可以徹底知足你的預期要求,能夠多使用struct
。
Currying就是把接受多個參數的方法進行一些變形,使其更加靈活的方法。函數式的編程思想貫穿於 Swift 中,而函數的柯里化正是這門語言函數式特色的重要表現。
例若有這樣的一個題目:實現一個函數,輸入是任一整數,輸出要返回輸入的整數 + 2。通常的實現爲:
func addTwo(_ num: Int) -> Int {
return num + 2
}
複製代碼
若是實現+3,+4,+5呢?是否須要將上面的函數依次增長一遍?咱們其實能夠定義一個通用的函數,它將接受須要與輸入數字相加的數,並返回一個函數:
func add(_ num: Int) -> (Int) -> Int {
return { (val) in
return val + num
}
}
let addTwo = add(2)
let addThree = add(3)
print("\(addTwo(1)) \(addThree(1))")
複製代碼
這樣咱們就能夠經過Curring來輸出模版來避免寫重複方法,從而達到量產類似方法的目的。
Swift中使用let
關鍵字來定義常量,let
只是標識這是一個常量,它是在runtime時肯定的,此後沒法再修改;ObjC中使用const
關鍵字來定義常量,在編譯時或者編譯解析時就須要肯定值。
func min<T : Comparable>(_ a : T , b : T) -> T {
return a < b ? a : b
}
複製代碼
常見的一種寫法是:
func swap<T>(_ a: inout T, _ b: inout T) {
let tempA = a
a = b
b = tempA
}
複製代碼
但若是使用多元組的話,咱們能夠這麼寫:
func swap<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
複製代碼
這樣一下變得簡潔起來,而且沒有顯示的增長額外空間。
爲何要說沒有顯示的增長額外空間呢?
喵神在說多元組交換時,提到了沒有使用額外空間。但有一些開發者認爲多元組交換將會複製兩個值,致使多餘內存消耗,這種說法有些道理,但蝸牛並無找到實質性的證據,若是有同窗瞭解,能夠評論補充。
另外蝸牛在[Leetcode-交換數字](https://leetcode-cn.com/problems/swap-numbers-lcci/)中測試了兩種寫法的執行耗時和內存消耗,基本上多元組交換執行速度上要優於普通交換,但前者的內存消耗要更高些,感興趣的同窗能夠試試。
複製代碼
都會對數組中的每個元素調用一次閉包函數,並返回該元素所映射的值,最終返回一個新數組。但flatmap
更進一步,多作了一些事情:
nil
,而且會解包Optional
類型。defer
所聲明的 block 會在當前代碼執行退出後被調用。正由於它提供了一種延時調用的方式,因此通常會被用來作資源釋放或者銷燬,這在某個函數有多個返回出口的時候特別有用。
func testDefer() {
print("開始持有資源")
defer {
print("結束持有資源")
}
print("程序運行ing")
}
// 開始持有資源
// 程序運行ing
// 結束持有資源
複製代碼
使用defer
會方便的將先後必要邏輯放在一塊兒,加強可讀性和維護,可是不正確的使用也會致使問題。例如上面的例子,在持有以前先判斷是否資源已經被其餘持有:
func testDefer(isLock: Bool) {
if !isLock {
print("開始持有資源")
defer {
print("結束持有資源")
}
}
print("程序運行ing")
}
// 開始持有資源
// 結束持有資源
// 程序運行ing
複製代碼
咱們要注意到defer
的做用域不是整個函數,而是當前的scope
。那若是有多份defer
呢?
func testDefer() {
print("開始持有資源")
defer {
print("結束持有資源A")
}
defer {
print("結束持有資源B")
}
print("程序運行ing")
}
// 開始持有資源
// 程序運行ing
// 結束持有資源B
// 結束持有資源A
複製代碼
當有多個defer
時,後加入的先執行,能夠猜想Swift使用了stack
來管理defer
。
String
是Swift類型,NSString
是Foundation
中的類,二者能夠無縫轉換。String
是值類型,NSString
是引用類型,前者更切合字符串的 "不變" 這一特性,而且值類型是自動多線程安全的,在使用上性能也有提高。
除非須要一些NSString
特有的方法,不然使用String
便可。
僅僅是獲取字符串的字符個數,可使用count
直接獲取:
let str = "Hello你好"
print("\(str.count)") // 7
複製代碼
若是想獲取字符串佔用的字節數,能夠根據具體的編碼環境來獲取:
print("\(str.lengthOfBytes(using: .utf8))") // 11
print("\(str.lengthOfBytes(using: .unicode))") // 14
複製代碼
Array
實現了ExpressibleByArrayLiteral
協議。return
,自動將這一句的結果做爲返回值。$0
在未顯式聲明參數列表時,表明第一個參數,以此類推。var mutableArray = [1,2,3]
for i in mutableArray {
mutableArray.removeAll()
print("\(i)")
}
print(mutableArray)
複製代碼
能夠正常運行,結果以下:
1
2
3
[]
複製代碼
爲何會調用三次?
由於Array
是個值類型,它是寫時賦值的,循環中mutableArray
值一旦改變,for in
上的mutableArray
會產生拷貝,後者的值仍然是[1, 2, 3]
,所以會循環三次。
原創不易,文章有任何錯誤,歡迎批(feng)評(kuang)指(diao)教(wo),順手點個贊👍,不甚感激!