【面試必備】iOS-Swift 面試題及其答案

雖然Swift出生才一年,但是它已經成爲最流行的編程語言之一了。它的語法很簡單,以至於當它發佈的時候,JavaScript開發者感覺就像下圖一樣

blob.png

事實上,Swift是一種複雜的語言。它包含面向對象和函數方法這兩個方面,並且隨着新版本的發佈在一直進化。

Swift的知識浩如煙海——但是怎麼測試你掌握了多少?在這篇文章中,我和這個raywenderlich.com網站的教學團隊共同寫了一個Swift面試問題列表。

你可以用這些問題來測試應聘者關於Swift方面的知識水平,或者測試一下你自己。如果你不知道答案,沒關係,沒一個問題下面都有答案供你學習。

這些問題包含兩個方面:

  • 筆試問題:通過電子郵件做一個編程測試是極好的,因爲這涉及到寫大量的代碼,從代碼質量上可以看出一個人的水平。

  • 面試問題:電話面試或者面對面面試也是很好的,因爲對面試者來說口頭交流會更方面。

每個方面有分成三個等級:

  • 初級:適合讀了一到兩本有關Swift的書,並且已經開始用Swift開發應用程序的初學者。

  • 中級:適合那些對Swift語言的概念有深刻理解和強烈興趣的,並且一直在閱讀大量有關Swift的博客文章並進行實踐的中級工程師。

  • 高級:適合那些以探索Swift語言知識爲樂趣,挑戰自己,使用前言技術的人們。

假如你想回答這些問題,我建議你在回答這些問題之前,打開Playground運行一下這些問題的代碼。這些問題的答案都在Xcode 7.0 Beta 6 版本中測試過。

準備好了嗎?繫好安全帶,現在就開始!

編者注:特別感謝raywenderlich.com教學團隊成員Warren BurtonGreg HeoMikael KonutganTim MitraLuke ParhamRui Peres, 和 Ray Wenderlich ,他們幫我相處了下面問題中得一些,並且測試區分難度級別。

筆試問題

初學者

問題1、(Swift 1.0及其之後的版本的問題)有什麼更好的方法來寫下面的for循環?

1
2
3
for var i = 0; i < 5; i++ {
  print("Hello!")
}

答案:

1
2
3
for in 0...4 {
  print("Hello!")
}

Swift 實現了兩個數組運算符closed operator 和 half-operator.前者包含數組中得所有值。例如:下面的例子包含從0到4得所有整數:

1
0...4

half-operator不包含數組中的最後一個元素,下面的例子會得到的結果和上面的一樣:

1
0..<5

問題2– Swift 1.0 or later

思考下面的問題:

1
2
3
4
5
6
7
struct Tutorial {
  var difficulty: Int = 1
}
  
var tutorial1 = Tutorial()
var tutorial2 = tutorial1
tutorial2.difficulty = 2

tutorial1.difficulty 和 tutorial2.difficulty的值分別是多少?假如Tutorial是一個類,會有什麼不同?並說明原因。

答案:tutorial1.difficulty  的值是1,然而tutorial2.difficulty的值是2.

在Swift中結構體是值類型,他們的值是複製的而不是引用的。下面的一行代碼意思是複製了tutorial1的值並把它賦值給tutorial2:

1
var tutorial2 = tutorial1

從這一行開始,tutorial2值得改變並不影響tutorial1的值。

假如Tutorial是一個類,tutorial1.difficulty和tutorial2.difficulty的值將都會是2.在Swift中類對象都是引用類型。tutorial1屬性的任何改變將會反應到tutorial2上,反之亦然。

問題3 – Swift 1.0 or later

view1聲明成var類型,view2聲明let類型。這裏有什麼區別嗎?下面的最後一行代碼能編譯嗎?

1
2
3
4
5
6
7
import UIKit
  
var view1 = UIView()
view1.alpha = 0.5
  
let view2 = UIView()
view2.alpha = 0.5 // Will this line compile?

答案:view1是個變量可以重新賦值給一個新的實例化的UIView對象。使用let你只賦值一次,所以下面的代碼是不能編譯的:

1
view2 = view1 // Error: view2 is immutable

但是UIView是一個引用類型的類,所以你可以改變view2的屬性,也就是說最後一行代碼是可以編譯的:

1
2
let view2 = UIView()
view2.alpha = 0.5 // Yes!

問題4 – Swift 1.0 or later

下面的代碼是把數組裏面的名字按字母的順序排序,看上去比較複雜。盡最大的可能簡化閉包裏的代碼。

1
2
3
4
let animals = ["fish""cat""chicken""dog"]
let sortedAnimals = animals.sort { (one: String, two: String) -> Bool in
  return one < two
}

答案:

第一個簡化的是參數。系統的參數類型推斷功能,可以計算出閉包裏面參數的類型,所以你不必定義參數的類型:

1
let sortedAnimals = animals.sort { (one, two) -> Bool in return one < two }

函數返回值也可以被推斷出來,所以簡化掉,代碼變爲:

1
let sortedAnimals = animals.sort { (one, two) in return one < two }

這個$i 符號可以代替參數名字,代碼進一步簡化爲:

1
let sortedAnimals = animals.sort { return $0 < $1 }

在一個獨立的閉包內,return這個關鍵字是可以省略的。最後聲明的返回值就是閉包的返回值:

1
let sortedAnimals = animals.sort { $0 < $1 }

這簡化很多了,但是我們不能止步於此!

對於字符串,有一個定義如下的比較函數:

1
func  Bool

這個簡單的小函數可以使你的代碼簡潔如下:

1
let sortedAnimals = animals.sort(<)

注意每一步的編譯結果都相同,但是最後一步你的閉包裏只有一個字符。

問題5 – Swift 1.0 or later

下面的代碼創建了兩個類Address和Person,並且創建了兩個實例對象分別代表Ray和Brain.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Address {
  var fullAddress: String
  var city: String
  
  init(fullAddress: String, city: String) {
    self.fullAddress = fullAddress
    self.city = city
  }
}
  
class Person {
  var name: String
  var address: Address
  
  init(name: String, address: Address) {
    self.name = name
    self.address = address
  }
}
  
var headquarters = Address(fullAddress: "123 Tutorial Street", city: "Appletown")
var ray = Person(name: "Ray", address: headquarters)
var brian = Person(name: "Brian", address: headquarters)

假設Brain搬家到街對面的建築物裏,那麼你會這樣更新他的地址:

1
brian.address.fullAddress = "148 Tutorial Street"

這樣做將會發生什麼?錯誤出在什麼地方呢?

blob.png

答案:Ray同樣會搬家到新的建築物裏面。Address是一個引用類型類,所以無論你是通過ray或者brain訪問headquarters,訪問都是同一個實例化對象。headquarters對象的變化也會引起ray和brain的變化。你能想象如果Brain收到Ray的郵件或者相反Ray收到Brain的郵件,將會發生什麼?解決方案是創建一個新的Address對象賦值給Brain或者把Address聲明成爲結構體而不是一個類。

中級

問題1– Swift 2.0 or later

思考下面的代碼:

1
2
var optional1: String? = nil
var optional2: String? = .None

nil 和 .None有什麼不同?optional1和optional2有什麼不同?

答案:兩者沒有什麼不同。Optional.None(簡稱.None)是optional變量值初始化的標準方法,而nil只是.None語法的一種修飾。事實上下面語句輸出是正確的:

1
2
nil == .None // On Swift 1.x this doesn't compile. You need Optional
.None

記住枚舉類型的Optional下的None:

1
2
3
4
enum Optional{
  case None
  case Some(T)
}

問題2-Swift 1.0 or later

下面是thermometer作爲類和結構體的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ThermometerClass {
  private(set) var temperature: Double = 0.0
  public func registerTemperature(temperature: Double) {
    self.temperature = temperature
  }
}
  
let thermometerClass = ThermometerClass()
thermometerClass.registerTemperature(56.0)
  
public struct ThermometerStruct {
  private(set) var temperature: Double = 0.0
  public mutating func registerTemperature(temperature: Double) {
    self.temperature = temperature
  }
}
  
let thermometerStruct = ThermometerStruct()
thermometerStruct.registerTemperature(56.0)

但是這段代碼編譯失敗了,請問哪裏報錯,出錯的原因是什麼。

建議:在使用Playground之前,認真閱讀代碼並思考。

答案:代碼的最後一行不會被編譯通過。ThermometerStruct結構體中正確的聲明瞭一個mutating屬性函數,它是用來改變結構體內部temperature屬性的值的,但是編譯器不通過的原因是,通過let創建的不可變的registerTemperature結構體調用了registerTemperature函數。

問題3– Swift 1.0 or later

下面的代碼輸出是什麼?並說明理由。

1
2
3
4
5
6
7
8
9
var thing = "cars"
  
let closure = { [thing] in
  print("I love \(thing)")
}
  
thing = "airplanes"
  
closure()

答案:輸出的是:I love cars。當閉包被聲明的時候,抓捕列表就複製一份thing變量,所以被捕捉的值並沒有改變,即使你給thing賦了一個新值。

如果你要忽視閉包中捕捉列表的值,那麼編譯器引用那個值而不是複製。這種情況下,被引用變量的值的變化將會反映到閉包中,正如下面的代碼所示:

1
2
3
4
5
6
7
8
9
var thing = "cars"
  
let closure = {   
  print("I love \(thing)")
}
  
thing = "airplanes"
  
closure() // Prints "I love airplanes"

問題4– Swift 2.0 or later

下面是一個全局函數,這個函數的功能是計算數組中特殊值得個數。(待校驗)

1
2
3
4
5
6
func countUniques(array: Array) -> Int {
  let sorted = array.sort(<)
  let initial: (T?, Int) = (.None, 0)
  let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) }
  return reduced.1
}

它使用了< 和==運算符,他們限制着T(佔位類型)的實際類型,也就是說T必須遵循Comparable協議。你可以這樣使用它:

1
countUniques([1, 2, 3, 3]) // result is 3

現在要求你重寫上面的方法作爲Array的擴展方法,然後你就可以這樣寫代碼:

1
[1, 2, 3, 3].countUniques() // should print 3

如何實現?

答案:在Swift 2.0 中,泛類型可以使用類型約束條件被強制擴展。但是假如這個泛類型不滿足這個類型的約束條件,那麼這個擴展方法既不可見也無法調用。

所以countUniques全局函數可以作爲Array的擴展方法被重寫如下:

1
2
3
4
5
6
7
8
extension Array where Element: Comparable {
  func countUniques() -> Int {
    let sorted = sort(<)
    let initial: (Element?, Int) = (.None, 0)
    let reduced = sorted.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) }
    return reduced.1
  }
}

注意:只有元類型實現了Comparable協議新的方法纔可以被使用。例如,如果你在全部是UIView對象的數組中調用countUniques,編譯器將會報錯。

1
2
3
import UIKit
let a = [UIView(), UIView()]
a.countUniques() // compiler error here because UIView doesn't implement Comparable

問題5- Swift 2.0 or later

下面一個函數的功能是計算兩個double(optional)類型的數的相除的結果。在執行除法之前,必須提前滿足三個條件:

被除數必須包含nil值

除數必須爲包含nil值

除數不能爲零

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func divide(dividend: Double?, by divisor: Double?) -> Double? {
  if dividend == .None {
    return .None
  }
  
  if divisor == .None {
    return .None
  }
  
  if divisor == 0 {
    return .None
  }
  
  return dividend! / divisor!
}

上面的函數可以正常使用,但是會存在兩個問題:

那些前提條件可以利用guard語句。

使用了強制拆包。

請使用guard語句和避免使用強制拆包來優化這個函數。

答案:guard語句是在Swift 2.0中引進的,它是用途是在未滿足某個條件時,提供一個退出的路徑。對於檢查是否滿足先決條件來說,它是非常有用的。因爲它可以使你更清晰的表達邏輯——而不是像i各種f語句嵌套實現那麼複雜。下面就是一個例子:

1
guard dividend != .None else return .None }

它也可以在optional binding(可選綁定)中使用。使用guard語句之後,使拆包後的變量可以被訪問。

1
guard let dividend = dividend else return .None }

所以divide函數被重寫如下:

1
2
3
4
5
6
func divide(dividend: Double?, by divisor: Double?) -> Double? {
  guard let dividend = dividend else return .None }
  guard let divisor = divisor else return .None }
  guard divisor != 0 else return .None }
  return dividend / divisor
}

我們發現隱身的可拆包的運算在代碼的最後一行,因爲dividend和divisor這兩個參數已經被拆包並且以分別以一個常量來存儲。

因此,你可以再次使用guard語句,使上面的函數更簡潔:

1
2
3
4
func divide(dividend: Double?, by divisor: Double?) -> Double? {
  guard let dividend = dividend, divisor = divisor where divisor != 0 else return .None }
  return dividend / divisor
}

上面的函數中使用了兩個guard語句,因爲使用了where語句指定了divisor不能爲0的條件。

高級

問題1- Swift 1.0 or later

下面是thermometer作爲結構體的例子:

1
2
3
4
5
6
public struct Thermometer {
  public var temperature: Double
  public init(temperature: Double) {
    self.temperature = temperature
  }
}

創建一個thermometer實例對象,你可以使用下面的代碼:

1
var t: Thermometer = Thermometer(temperature:56.8)

但是像下面的代碼那樣初始化對象是否會更好:

1
var thermometer: Thermometer = 56.8

能這樣做嗎?如果可以,怎麼做?提示:it has to do with convertibles, but not convertibles like Camaros and Mustangs

答案:Swift 定義了下面的協議,這些協議可以使一種類型通過字面量的方式來初始化並賦值。

1
2
3
4
5
6
7
8
9
NilLiteralConvertible
BooleanLiteralConvertible
IntegerLiteralConvertible
FloatLiteralConvertible
UnicodeScalarLiteralConvertible
ExtendedGraphemeClusterLiteralConvertible
StringLiteralConvertible
ArrayLiteralConvertible
DictionaryLiteralConvertible

採用相應的協議並且提供一個允許字面量初始化的公用方法。在Thermometer類型的例子下,我們需要實現FloatLiteralConvertible協議,代碼如下:

1
2
3
4
5
extension Thermometer : FloatLiteralConvertible {
  public init(floatLiteral value: FloatLiteralType) {
    self.init(temperature: value)
  }
}

那麼現在,你就可以通過一個簡單的float數字創建一Thermometer對象,代碼如下:

1
var thermometer: Thermometer = 56.8

問題2 - Swift 1.0 or later

Swift 擁有一系列預定義的運算符,這些運算符執行不同類型的操作,例如算術運算符和邏輯運算符。它甚至允許創建自定義的運算符,無論是一元運算符還是二元運算符。自定義一個滿足一下規格的冪運算符:

以兩個整數作爲參數

返回第一個參數的第二個參數次方的值

忽略潛在溢出錯誤

答案:創建一個自定義的運算符需要兩個步驟:聲明它和實現它。

使用operator關鍵字來聲明指定的類型(一元或者二元)、組成這個運算符字符的順序已經它的優先級和關聯性。

在這中情況下,運算符是^^,類型是infix(二進制),關聯性是right,優先級設置成爲155,原因是乘法和除法的優先級是150.下面就是具體的聲明代碼:

1
infix operator ^^ { associativity right precedence 155 }

代碼實現如下:

1
2
3
4
5
6
func ^^(lhs: Int, rhs: Int) -> Int {
  let l = Double(lhs)
  let r = Double(rhs)
  let p = pow(l, r)
  return Int(p)
}

值得注意的是,它並不需要溢出考慮;如果操作產生的結果int不能代表,如大於int.max,就會發生運行時錯誤。

問題3 - Swift 1.0 or later

你能像下面的代碼一樣使用原始值定義一個枚舉類型嗎?如果不行,說明原因。

1
2
3
4
5
6
enum Edges : (Double, Double) {
  case TopLeft = (0.0, 0.0)
  case TopRight = (1.0, 0.0)
  case BottomLeft = (0.0, 1.0)
  case BottomRight = (1.0, 1.0)
}

答案:不行。原始值得類型必須滿足一下條件

遵守Equatable協議

滿足能轉換成下列類型中的任何一個類型:    

1
2
3
    a.Int
    b.String
    c. Character

在上面的代碼中,原始值即使是獨立的個體值,但是它仍然是一個不兼容的元組。 

問題4- Swift 2.0 or later

下面的代碼定義了一個結構體Pizza和一個協議Pizzeria,這個協議有一個包含makeMargherita()函數的擴展。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Pizza {
  let ingredients: [String]
}
  
protocol Pizzeria {
  func makePizza(ingredients: [String]) -> Pizza
  func makeMargherita() -> Pizza
}
  
extension Pizzeria {
  func makeMargherita() -> Pizza {
    return makePizza(["tomato""mozzarella"])
  }
}

現在你將要定義一個如下的Lombardi的餐館:

1
2
3
4
5
6
7
8
struct Lombardis: Pizzeria {
  func makePizza(ingredients: [String]) -> Pizza {
    return Pizza(ingredients: ingredients)
  }
  func makeMargherita() -> Pizza {
    return makePizza(["tomato""basil""mozzarella"])
  }
}

下面的代碼創建了Lombardis類型的兩個實例對象,哪一個對象會產生一個帶有basil的margherita披薩?

1
2
3
4
5
let lombardis1: Pizzeria = Lombardis()
let lombardis2: Lombardis = Lombardis()
  
lombardis1.makeMargherita()
lombardis2.makeMargherita()

答案:兩個都可以。Pizzeria協議聲明瞭makeMargherita()方法並且提供了一個默認的實現,而且它又在Lombardis的實現中被重寫。在這兩種情況下,由於這個方法在協議中被聲明,那麼在運行時相應的實現就會被調用。

假如Pizzeria協議沒有聲明makeMargherita()方法,但是擴展中仍然提供瞭如下的代碼的這個方法默認的實現,會發生什麼?

1
2
3
4
5
6
7
8
9
protocol Pizzeria {
  func makePizza(ingredients: [String]) -> Pizza
}
  
extension Pizzeria {
  func makeMargherita() -> Pizza {
    return makePizza(["tomato""mozzarella"])
  }
}

這種情況下,只有lombardis2會產生一個帶有basil的pizza,而lombardis1將會產生一個不帶basil的pizza,原因是它會調用擴展中定義的那個方法。

問題5- Swift 2.0 or later

下面的代碼有一個編譯錯誤,請指出來並說明原因。

1
2
3
4
5
6
7
8
9
10
struct Kitten {
}
  
func showKitten(kitten: Kitten?) {
  guard let k = kitten else {
    print("There is no kitten")
  }
  
  print(k)
}

提示:有三種方法修復它。

答案:guard語句中得else語句必須需要一個返回路徑,你可以使用return ,拋出異常或者調用@noreturn.最簡單的解決方法是添加一個return語句,代碼如下:

1
2
3
4
5
6
7
func showKitten(kitten: Kitten?) {
  guard let k = kitten else {
    print("There is no kitten")
    return
  }
  print(k)
}

下面是拋出異常的版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum KittenError: ErrorType {
  case NoKitten
}
  
struct Kitten {
}
  
func showKitten(kitten: Kitten?) throws {
  guard let k = kitten else {
    print("There is no kitten")
    throw KittenError.NoKitten
  }
  print(k)
}
  
try showKitten(nil)

最後,調用一個@noreturn功能函數 fatalError( ) 解決方案:

1
2
3
4
5
6
7
8
9
10
struct Kitten {
}
  
func showKitten(kitten: Kitten?) {
  guard let k = kitten else {
    print("There is no kitten")
    fatalError()
  }
  print(k)
}

面試試題

blob.png

你很棒,但是你還沒達到絕地武士的要求。任何人都可以寫出代碼,但是你如何處理理論和實踐相結合的開放式問題?

爲了解決這些問題,你仍然需要使用Playgroud寫代碼來驗證一些問題。

初級

問題1- Swift 1.0 or later

什麼是optional類型,它是用來解決什麼問題的?

答案:optional類型被用來表示任何類型的變量都可以表示缺少值。在Objective-C中,引用類型的變量是可以缺少值得,並且使用nil作爲缺少值。基本的數據類型如int 或者float沒有這種功能。

Swift用optional擴展了在基本數據類型和引用類型中缺少值的概念。一個optional類型的變量,在任何時候都可以保存一個值或者爲nil。

問題2- Swift 1.0 or later

在Swfit中,什麼時候用結構體,什麼時候用類?

答案:一直都有這樣的爭論:到底是用類的做法優於用結構體,還是用結構體的做法優於類。函數式編程傾向於值類型,面向對象編程更喜歡類。

在Swift 中,類和結構體有許多不同的特性。下面是兩者不同的總結:

類支持繼承,結構體不支持。

類是引用類型,結構體是值類型

並沒有通用的規則決定結構體和類哪一個更好用。一般的建議是使用最小的工具來完成你的目標,但是有一個好的經驗是多使用結構體,除非你用了繼承和引用語義。

想要了解更多,點擊這裏

注意:在運行時,結構體的在性能方面更優於類,原因是結構體的方法調用是靜態綁定,而類的方法調用是動態實現的。這就是儘可能得使用結構體代替類的又一個好的原因。

問題3- Swift 1.0 or later

什麼是泛型?泛型是用來解決什麼問題的?

答案:泛型是用來使類型和算法安全的工作的一種類型。在Swift中,在函數和數據結構中都可以使用泛型,例如類、結構體和枚舉。

泛型一般是用來解決代碼複用的問題。常見的一種情況是,你有一個函數,它帶有一個參數,參數類型是A,然而當參數類型改變成B的時候,你不得不復制這個函數。

例如,下面的代碼中第二個函數就是複製第一個函數——它僅僅是用String類型代替了Integer類型。

1
2
3
4
5
6
7
8
9
10
func areIntEqual(x: Int, _ y: Int) -> Bool {
  return x == y
}
  
func areStringsEqual(x: String, _ y: String) -> Bool {
  return x == y
}
  
areStringsEqual("ray""ray"// true
areIntEqual(1, 1) // true

Objective-C開發人員可能想到用NSObject類來解決這個問題,代碼如下:

1
2
3
4
5
6
7
8
import Foundation
  
func areTheyEqual(x: NSObject, _ y: NSObject) -> Bool {
  return x == y
}
  
areTheyEqual("ray""ray"// true
areTheyEqual(1, 1) // true

這個代碼會按照預期的方式工作,但是它在編譯時不安全。它允許字符串和整數相比較,像這樣:

1
areTheyEqual(1, "ray")

應用程序不會崩潰,但是允許字符串和整數相比較可能不是預想的結果。

通過採用泛型,可以合併這兩個函數爲一個並同時保持類型安全。下面是代碼實現:

1
2
3
4
5
6
func areTheyEqual(x: T, _ y: T) -> Bool {
  return x == y
}
  
areTheyEqual("ray""ray")
areTheyEqual(1, 1)

上面的例子是測試兩個參數是否相等,這兩個參數的類型受到約束都必須遵循Equatable協議。上面的代碼達到預想的結果,並且防止了傳遞不同類型的參數。

問題4- Swift 1.0 or later

哪些情況下你不得不使用隱式拆包?說明原因。

答案:對optional變量使用隱式拆包最常見的原因如下:

1、對象屬性在初始化的時候不能nil,否則不能被初始化。典型的例子是Interface Builder outlet類型的屬性,它總是在它的擁有者初始化之後再初始化。在這種特定的情況下,假設它在Interface Builder中被正確的配置——outlet被使用之前,保證它不爲nil。

2、解決強引用的循環問題——當兩個實例對象相互引用,並且對引用的實例對象的值要求不能爲nil時候。在這種情況下,引用的一方可以標記爲unowned,另一方使用隱式拆包。

建議:除非必要,不要對option類型使用隱式拆包。使用不當會增加運行時崩潰的可能性。在某些情況下,崩潰可能是有意的行爲,但有更好的方法來達到相同的結果,例如,通過使用fatalError( )函數。

問題5- Swift 1.0 or later

對一個optional變量拆包有多少種方法?並在安全方面進行評價。

答案:  

  • 強制拆包 !操作符——不安全

  • 隱式拆包變量聲明——大多數情況下不安全

  • 可選綁定——安全

  • 自判斷鏈接(optional chaining)——安全

  • nil coalescing 運算符(空值合併運算符)——安全

  • Swift 2.0 的新特性 guard 語句——安全

  • Swift 2.0 的新特性optional pattern(可選模式) ——安全(@Kametrixom支持)

blob.png

中級

問題1- Swift 1.0 or later

Swift 是面向對象編程語言還是函數式編程語言?

答案:Swift是一種混合編程語言,它包含這兩種編程模式。它實現了面向對象的三個基本原則:

  • 封裝

  • 繼承

  • 多態

說道Swift作爲一種函數式編程語言,我們就不得不說一下什麼是函數式編程。有很多不同的方法去定義函數式編程語言,但是他們表達的意義相同。

最常見的定義來自維基百科:...它是一種編程規範…它把電腦運算當做數學函數計算,避免狀態改變和數據改變。

很難說Swift是一個成熟的函數式語言,但是它已經具備了函數式語言的基礎。

問題2- Swift 1.0 or later

下面的功能特性都包含在Swift中嗎?

1、泛型類

2、泛型結構體

3、泛型協議

答案:

  • Swift 包含1和2特性。泛型可以在類、結構體、枚舉、全局函數或者方法中使用。

  • 3是通過typealias部分實現的。typealias不是一個泛型類型,它只是一個佔位符的名字。它通常是作爲關聯類型被引用,只有協議被一個類型引用的時候它才被定義。

問題3- Swift 1.0 or later

在Objective-C中,一個常量可以這樣定義:

1
const int number = 0;

類似的Swift是這樣定義的:

1
let number = 0

兩者之間有什麼不同嗎?如果有,請說明原因。

答案:const常量是一個在編譯時或者編譯解析時被初始化的變量。通過let創建的是一個運行時常量,是不可變得。它可以使用stattic 或者dynamic關鍵字來初始化。謹記它的的值只能被分配一次。

問題4- Swift 1.0 or later

聲明一個靜態屬性或者函數,我們常常使用值類型的static修飾符。下面就是一個結構體的例子:

1
2
3
struct Sun {
  static func illuminate() {}
}

對類來說,使用static 或者class修飾符,都是可以的。它們使用後的效果是一樣的,但是本質上是不同的。能解釋一下爲什麼不同嗎?

答案:

static修飾的屬性或者修飾的函數都不可以重寫。但是使用class修飾符,你可以重寫屬性或者函數。

當static在類中應用的時候,static就成爲class final的一個別名。

例如,在下面的代碼中,當你嘗試重寫illuminate()函數時,編譯器就會報錯:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Star {
  class func spin() {}
  static func illuminate() {}
}
  
class Sun : Star {
  override class func spin() {
    super.spin()
  }
  override static func illuminate() { // error: class method overrides a 'final' class method
    super.illuminate()
  }
}

問題5- Swift 1.0 or later

你能通過extension(擴展)保存一個屬性嗎?請解釋一下原因。

答案:不能。擴展可以給當前的類型添加新的行爲,但是不能改變本身的類型或者本身的接口。如果你添加一個新的可存儲的屬性,你需要額外的內存來存儲新的值。擴展並不能實現這樣的任務。

高級

問題1- Swift 1.2

在Swift1.2版本中,你能解釋一下用泛型來聲明枚舉的問題嗎?拿下面代碼中Either枚舉來舉例說明吧,它有兩個泛型類型的參數T和V,參數T在關聯值類型爲left情況下使用,參數V在關聯值爲rihgt情況下使用,代碼如下:

1
2
3
4
enum Either{
  case Left(T)
  case Right(V)
}

提示:驗證上面的條件,需要在Xcode工程裏面,而不是在Playgroud中。同時注意,這個問題跟Swift1.2相關,所以Xcode的版本必須是6.4以上。

答案:上面的代碼會出現編譯錯誤:

1
unimplemented IR generation feature non-fixed multi-payload enum layout

問題是T的內存大小不能確定前期,因爲它依賴於T類型本身,但enum情況下需要一個固定大小的有效載荷。

最常用的解決方法是講泛類型用引用類型包裝起來,通常稱爲box,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
class Box{
  let value: T
  init(_ value: T) {
    self.value = value
  }
}
  
enum Either{
  case Left(Box)
  case Right(Box)
}

這個問題在Swift1.0及之後的版本出現,但是Swift2.0的時候,被解決了。

問題2- Swift 1.0 or later

閉包是引用類型嗎?

答案:閉包是引用類型。如果一個閉包被分配給一個變量,這個變量複製給另一個變量,那麼他們引用的是同一個閉包,他們的捕捉列表也會被複制。

問題3- Swift 1.0 or later

UInt類型是用來存儲無符號整型的。下面的代碼實現了一個有符號整型轉換的初始化方法:

init(_ value: Int)

然而,在下面的代碼中,當你給一個負值的時候,它會產生一個編譯時錯誤:

let myNegative = UInt(-1)

我們知道負數的內部結構是使用二進制補碼的正數,在保持這個負數內存地址不變的情況下,如何把一個負整數轉換成一個無符號的整數?

答案:使用下面的初始化方法:

UInt(bitPattern: Int)

問題4- Swift 1.0 or later

描述一種在Swift中出現循環引用的情況,並說明怎麼解決。

答案:循環引用出現在當兩個實例對象相互擁有強引用關係的時候,這會造成內存泄露,原因是這兩個對像都不會被釋放。只要一個對象被另一個對象強引用,那麼該對象就不能被釋放,由於強引用的存在,每個對象都會保持對方存在。

解決這個問題的方法是,用weak或者unowned引用代替其中一個的強引用,來打破循環引用。

問題5- Swift 2.0 or later

Swift2.0 增加了一個新的關鍵字來實現遞歸枚舉。下面的例子是一個枚舉類型,它在Node條件下有兩個相關聯的值類型T和List:

1
2
3
enum List{
    case Node(T, List)
}

什麼關鍵字可以實現遞歸枚舉?

答案:indirect 關鍵值可以允許遞歸枚舉,代碼如下:

1
2
3
enum List{
    indirect case Cons(T, List)
}

Where To Go From Here?

恭喜你到了文章的最後,如果你不知道所有問題的答案,也不要感到沮喪。

因爲上面中得有些問題還是比較複雜的,並且Swift是一門富有表現力的語言,還有很多需要我們學。此外,蘋果公司一直改善Swift的新特性,所以即使學的最好的人也不可能知道所有的一切。

在你現有的Swift基礎知識之上,要深入瞭解Swift ,你就的看看Swift by Tutorials這本書,或者加入我們實踐教學協會RWDevCon

當然,關於Swift所有方面的資源都來是蘋果公司官方文檔The Swift Programming Language

事實上,學習一門語言最好的方式是用它。在你的工程裏或者Plaugroud裏面使用Swift編程。Swift幾乎可以無縫銜接Object-C,所以在你現有的工程中使用Swift是一個學習Swift的很好的方法。

謝謝你的訪問和回答這些問題。在下面你可以隨意提問交流。我也不介意你在下面貼上自己遇見的難題和挑戰,我們可以相互學習。論壇上再見!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章