SwiftBlogiOS技能Swift相关

Swift 面试题

2016-01-17  本文已影响4434人  TedX

Swift语言至今诞生有一年多的时间了,已经成为当前最流行语言之一。虽然它的语法简单好用,但实际上Swift是一门非常复杂的语言。因为它不仅是面向对象的同时又是函数式编程语言。本文主要介绍Swift常见的一些面试问题,你可以用这些问题向面试者提问,也可以用来测试你自己目前所掌握的Swift知识,如果你不清楚问题答案的话也不用太担心,因为每个问题下面都有相应的答案。

问题主要分为两个部分,笔试题和口头问题。

笔试题:可以通过发送Email方式进行编程测试,问题可以包含一小段代码测试。
口头问题:可以通过手机等方式进行面对面的交流,因为这些问题更适合用语言进行交流。

而且每个部分又分为三个等级:

初级:适用于刚接触Swift的学习者,已经读过一本或者两本Swift相关书籍,开始准备在App中使用Swift语言。
中级:适用于对Swift语言概念非常感兴趣的学习者,已经读过许多Swift博客文章并且想进一步深入学习Swift语言。
高级:适用于高级学者,已经对Swift语言很熟悉,喜欢探寻语言,想进一步挑战自我,喜欢高级技术者。

如果你想知道这些问题的答案,建议你最好打开Playground,亲自实现编码。下面这些问题的答案都在Xcode 7.0 beta 6上测试过。

笔试题

初级

你好,现在开始基础测试。
问题 #1 - Swift1.0或者更高版本
用下面方法写一个for循环是最好的方法吗?

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

答案:

for _ in 0...4 {
  print("Hello!")
}

Swift实现了两种范围操作符,分别为闭区间操作符和半开区间操作符。前者包括范围内的所有元素。例如下面例子包含了0到4所有元素。

0...4

而半开区间操作符不会包括最后一个元素。下面的例子同样包括了0到4所有元素

0..<5

问题 #2 - Swift1.0或者更高版本
考虑以下这段代码

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中,结构体是值类型而不是引用类型,它是值copy。
下面这行代码会首先创建一份tutorial1的copy,然后再赋值给tutorial2

var tutorial2 = tutorial1

从这行代码可以看出,tutorial2 的变化不会影响tutorial1
如果Tutorial是一个类,tutorial1.difficulty和tutorial2.difficulty的值都是2。
在Swift中,类是引用类型,tutorial1的变化会影响到tutorial2,反之亦然。

问题 #3 - Swift1.0或者更高版本
用var声明view1和用let声明view2,在下面的例子中有什么不同?,最后一行Code能否编译通过?

import UIKit
 
var view1 = UIView()
view1.alpha = 0.5
 
let view2 = UIView()
view2.alpha = 0.5 // Will this line compile?

答案

因为view1是一个变量,所以可以用UIView的实例进行赋值,用let关键字只能赋值一次,所以下面代码不能编译成功。

view2 = view1 // 错误:view2是不可变的

但是,UIView是基于类引用的,所以view2的属性是可以改变的(最后一行代码可以编译通过)

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

问题 #4 - Swift1.0或者更高版本
下面Code是把数组按字母顺序进行排序,看起来有些复杂,你能用闭包简化它吗?

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

答案

首先是可以简化闭包的参数,因为在闭包中,系统是可以通过类型推断方式推算出参数的类型。所以你可以去掉参数的类型:

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

返回类型也可以推算出来,所以可以去掉闭包的返回类型:

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

可以用$i符号替换掉参数的名字,代码然后就变成这样:

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

在单语句的闭包中,关键字return也可以省略。最后一条语句的返回值就变成闭包的返回值:

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

Oops, 到目前,是不是非常简单了,但实际上并非如此。
对字符串来说,有一个字符串比较函数,定义如下:

func <(lhs: String, rhs: String) -> Bool

使用这个函数可以让你的Code更加简洁, 如下:

let sortedAnimals = animals.sort(<)

注意:以上每一步的代码编译运行后都会输出同样的结果,你可以选择使用单字节的闭包。

问题 #5 - Swift1.0或者更高版本
下面代码定义AddressPerson两个类,创建Ray和Brian两个实例。

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)

假如Brian要搬迁到新的地址居住,所以你会这样更新他的住址:

brian.address.fullAddress = "148 Tutorial Street"

这样做的话会发生什么?错在哪个地方?

I know it
答案

Ray也会更新地址。因为类Address是引用类型,headquarters是同一个实例,不论你是修改ray的地址还是brian的地址,都会改变headquarters地址。
解决方法是为brian新创建一个地址。或者声明Address为结构体而不是类。

中级

下面提升一下难度的等级
问题 #1 - Swift2.0或者更高版本
思考一下以下代码:

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

nil.None有什么不同?变量optional1和optional2有什么不同?
答案:

nil.None是一样的。当可选变量没有值时,Optional.None(.None for short)是一般初始化可选变量的方式,而nil则是另一种写法。
事实上,下面条件语句输出是true:

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

记着下面代码说明enumeration是一个可选类型:

enum Optional<T> {
  case None
  case Some(T)
}

问题 #2 - Swift1.0或者更高版本
下面是用类和结构体实现温度计的例子:

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声明一个可变的函数修改内部变量 temperature,但是registerTemperature却被一个用let创建的实例所调用,用let定义的变量是不可变的,所以编译通不过。
在结构体中,改变内部状态的方法必须用mutating声明,而且不允许用不可变的实例调用它。

问题 #3 - Swift1.0或者更高版本
下面的代码打印输出是什么?为什么?

var thing = "cars"
let closure = { [thing] in
  print("I love \(thing)")
}
thing = "airplanes"
closure()

答案

结果会打印出“I love cars”。当声明闭包的时候,捕获列表会创建一份thing的copy,所以被捕获到的值是不会改变的,即使你改变thing的值。
如果你去掉闭包中的捕获列表,编译器会使用引用代替copy。在这种情况下,当闭包被调用时,变量的值是可以改变的。示例如下:

var thing = "cars"
let closure = {    
  print("I love \(thing)")
}
thing = "airplanes"
closure() // Prints "I love airplanes"

问题 #4 - Swift2.0或者更高版本
这是一个全局函数,用来记录数组中唯一值的数量。

func countUniques<T: Comparable>(array: Array<T>) -> 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类型需要遵循Comparable协议。
可以像下面这样调用它:

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

重写这个函数,让它作为数组Array的方法,使得可以这样调用:

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

答案

在Swift2.0中,泛型是可以被扩展的,但需要类型约束限制,如果泛型不满足约束,那么扩展也是不可见或者不可访问的。
因此,全局函数countUniques可以被重写为数组Array的扩展:

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
  }
}

注意这个被重写的方法只有当泛型的类型Element实现了Comparable协议才是有效的。例如,当你用装满UIView的数组调用这个方法时编译器会提示错误:

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

问题 #5 - Swift2.0或者更高版本
下面是一个计算两个可选类型除法的函数。在计算之前满足以下三个条件:

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!
}

这段代码能够按照预期的那样进行工作,但同时存在两个问题:

在Swift2.0中引用guard语句关键字,如果不满足时,guard会提供一个退出路径。这个关键字在判断预置条件时非常有用,能更加清晰表达条件,不需要采用金字塔似的多重嵌套if语句,下面是一个例子:

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

它还可以用在可选绑定上,能够访问解包后的变量:

guard let dividend = dividend else { return .None }

因此重写后的divide函数如下:

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条件,可以让函数变得更加简单:

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 - Swift1.0或者更高版本
思考以下用结构体定义的温度计:

public struct Thermometer {
  public var temperature: Double
  public init(temperature: Double) {
    self.temperature = temperature
  }
}

创建一个实例,可以用下面的方式:

var t: Thermometer = Thermometer(temperature:56.8)

但最好是使用下面方式进行初始化:

var thermometer: Thermometer = 56.8

这可以做到吗?应该怎样做?
答案

Swift定义了以下协议,通过使用赋值操作符,用字面值直接初始化:

extension Thermometer : FloatLiteralConvertible {
  public init(floatLiteral value: FloatLiteralType) {
    self.init(temperature: value)
  }
}

现在可以使用float值创建一个实例了:

var thermometer: Thermometer = 56.8

问题 #2 - Swift1.0或者更高版本
Swift中定义了运算操作符和逻辑操作符,用于操作不同的类型。当然,你可以自定义一些运算操作符,比如一元或者二元的。
定义一个^^幂操作符,并且同时满足以下要求:

答案

创建一个自定义运算符需要两步:声明和实现。
声明使用关键字operator,用于指定类型(一元或者二元),然后指定运算符的结合性和优先级。
在本例中,运算符是^^,类型是infix,结合性是右结合,优先级设置为155,鉴于乘法和除法的优先级是150。以下是运算符的声明:

infix operator ^^ { associativity right precedence 155 }

实现如下:

func ^^(lhs: Int, rhs: Int) -> Int {
  let l = Double(lhs)
  let r = Double(rhs)
  let p = pow(l, r)
  return Int(p)
}

注意一点,实现没有考虑溢出的情况,如果运算结果超过Int.max,将会产生运行时错误。

问题 #3 - Swift1.0或者更高版本
你能像下面这样用原始值定义枚举类型吗?为什么?

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)
}

答案

不能,原始值的类型必须遵循以下条件:

在以上代码中,原始数值类型是元组类型,即使元组中的数值满足条件,也是不兼容的。

问题 #4 - Swift2.0或者更高版本
思考以下代码,定义了结构体Pizza,协议Pizzeria,在协议扩展中实现默认方法makeMargherita()。

struct Pizza {
  let ingredients: [String]
}
 
protocol Pizzeria {
  func makePizza(ingredients: [String]) -> Pizza
  func makeMargherita() -> Pizza
}
 
extension Pizzeria {
  func makeMargherita() -> Pizza {
    return makePizza(["tomato", "mozzarella"])
  }
}

然后定义一个餐馆Lombardis:

struct Lombardis: Pizzeria {
  func makePizza(ingredients: [String]) -> Pizza {
    return Pizza(ingredients: ingredients)
  }
  func makeMargherita() -> Pizza {
    return makePizza(["tomato", "basil", "mozzarella"])
  }
}

下面代码创建了Lombardis的两个实例。哪一个会使用“basil”来做披萨?

let lombardis1: Pizzeria = Lombardis()
let lombardis2: Lombardis = Lombardis()
 
lombardis1.makeMargherita()
lombardis2.makeMargherita()

答案

两个都会。协议Pizzeria声明了makeMargherita()方法,并且提供了一个默认的实现。在Lombardis实现中,这个方法被重写了。因为这个方法在协议中声明了,在Runtime时能正确的被调用。
如果协议中没有声明makeMargherita()方法,但是在协议的扩展中又默认实现了 这个方法会怎样?

protocol Pizzeria {
  func makePizza(ingredients: [String]) -> Pizza
}
extension Pizzeria {
  func makeMargherita() -> Pizza {
    return makePizza(["tomato", "mozzarella"])
  }
}

在这种情况下,只有lombardis2能用“basil”来做披萨,而lombardis1则不会,因为它调用的是扩展中默认的方法。

问题 #5 - Swift2.0或者更高版本
下面代码会有编译错误,你能指出在哪个地方吗?为什么?

struct Kitten {
}
 
func showKitten(kitten: Kitten?) {
  guard let k = kitten else {
    print("There is no kitten")
  }
 
  print(k)
}

提示:有三种方式可以fix它
答案

在guard中else语句体中需要有退出路径,用return返回,或者抛出一个异常或者调用@noreturn。最简单的方式是return语句:

func showKitten(kitten: Kitten?) {
  guard let k = kitten else {
    print("There is no kitten")
    return
  }
  print(k)
}

下面版本是抛出异常:

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():

struct Kitten {
}
func showKitten(kitten: Kitten?) {
  guard let k = kitten else {
    print("There is no kitten")
    fatalError()
  }
  print(k)
}

口头答疑

绝地战士

初级

问题 #1 - Swift1.0或者更高版本
可选是什么?可以解决什么问题?
答案

可选可以使得任何类型的变量都能表达缺省值。在Objective-C中,缺省值只适用于引用类型,通常被指定为nil。对于基础类型的变量(例如int, float)则没有这个功能,
而Swift把缺省值概念扩展到引用类型和值类型中。一个可选变量在任何时候可以有值或者为nil。

问题 #2 - Swift1.0或者更高版本
什么时候使用结构体?什么时候使用类?
答案

目前关于使用类还是结构体这个问题,有许多的争论。在函数式编程倾向于使用值类型,而面向对象编程中更喜欢用类。
在Swift中,类和结构体有很多不相同的功能,主要有下面几点:

并没有统一的规则决定孰好孰坏。通常推荐使用最适合的工具完成特定的目标,在swift中比较好的做法是使用结构体,除非你需要用到继承或者引用的时候才使用类。
主要因为在运行时,结构体的性能优于类的,结构体的方法调用是静态绑定的,而类是在Runtime时动态解析的。

问题 #3 - Swift1.0或者更高版本
泛型是什么?用来解决什么问题?
答案

泛型可以使某一个类型的算法能够更安全的工作。在Swift中泛型可以用在函数和数据类型上,如类,结构体和枚举类型。
泛型还能解决代码重复的问题。普遍现象是当你已经有一个带参数的方法,但你又不得不再重新写一遍有着类似类型的方法。
在下面的例子中,第二个函数就像是第一个函数的“clone”,它只是把传入参数的类型从字符串变为整型。

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可以解决这个问题:

import Foundation
func areTheyEqual(x: NSObject, _ y: NSObject) -> Bool {
  return x == y
}
areTheyEqual("ray", "ray") // true
areTheyEqual(1, 1) // true

虽然这种方式是能解决问题,但是在编译期间是不安全的。因为它会允许比较String类型和Integer类型,就像下面这样:

areTheyEqual(1, "ray")

虽然应用程序不会crash,但是允许字符串和整型进行比较,会出现让你想不到的结果。
使用泛型的话,可以把两个函数合成一个,同时又能保证类型是安全的。下面是一个具体实现:

func areTheyEqual<T: Equatable>(x: T, _ y: T) -> Bool {
  return x == y
}
areTheyEqual("ray", "ray")
areTheyEqual(1, 1)

这个例子是测试两个数是否相等,你可以限制传入参数类型是任意类型,只要这个类型实现了Equatable协议。这段代码可以得到你想要的结果并且能防止传入不同参数类型。

Two birds, one stone

问题 #4 - Swift1.0或者更高版本
在某些情况下,你不得不使用隐式解包可选?什么场合?为什么?
答案

下面情况下需要使用隐式解包可选:

  1. 当属性在初始化阶段不能为空的时候。一个典型的例子是IB的输出口,它总是要初始化的。使用它之前已经在IB中配置,outlet确保在使用之前值不为空。
  2. 解决强引用循环的问题,当两个实例相互引用,需要一个实例是非空引用。在这种情况下,一个实例可以标注unowned,另一个使用隐式解包可选。
    提示:不要使用隐式解包可选,除非你必须使用时。如果使用它不当时,会增加Runtime时crash的机率。

问题 #5 - Swift1.0或者更高版本
解包可选类型方法是什么?它们的安全性怎样?
提示:有7种方法
答案

  • forced unwrapping ! operator -- unsafe
Believe

中级

问题 #1 - Swift1.0或者更高版本
Swift是面向对象语言还是函数式语言?
答案

Swift是混合式语言,两种都支持。
它实现面向对象的三个基本特征:

和函数式语言相比,Swift有一些不同,虽然它满足函数式语言基本要求,但并不是完全成熟的函数式编程语言。

问题 #2 - Swift1.0或者更高版本
下面功能,哪一个在Swift中有?

  1. 泛型类
  2. 泛型数据结构
  3. 泛型协议

答案

1和2在Swift中有。泛型可以用在类,结构体,枚举类型,全局函数和方法中。
3可以通过关键字typealias部分实现。它并不是泛型,只是占位符名字而已。它常被看作是关联数据类型,采用协议时才会被定义。

问题 #3 - Swift1.0或者更高版本
在Objective-C中,一个常量可以像下面进行声明:

const int number = 0;

下面是Swift中的:

let number = 0

他们之间有什么不同吗?如果是的,你能解释一下吗?
答案

const是一个变量在编译期间被初始化值或者在编译期间表达式的值。
通过let关键字创建常量是在Runtime时初始化的,它能够用用静态的或者动态表达式的结果初始化。注意它的值只能被初始化一次。

问题 #4 - Swift1.0或者更高版本
声明一个静态属性或者函数,你可以使用关键字static来修饰值类型。以下是一个结构体的例子:

struct Sun {
  static func illuminate() {}
}

对于类来说,你可以使用static或者class修饰符。虽然它们完成同样的功能,但实际上是不同的。你能解释一下它们之间有什么不同吗?

答案:

使用static关键字,静态属性和静态函数是不能被重写的,但当使用class关键字,你可以重写属性和函数。
其实,对于类来说,static关键字是class final的别名而已。
例如,你编译下面这些code时,当你要重写illuminate()函数时,编译器提示错误:

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 - Swift1.0或者更高版本
使用extension可以增加存储属性吗?解释一下
答案

是不能的。extension是用来给存在的类型添加新行为的,并不能改变类型或者接口本身。如果你增加存储属性,你需要额外的内存空间存储新的值。extension是不能管理这样的任务的。

高级

问题 #1 - Swift1.2
在Swift1.2中,你能解释一下用泛型声明枚举类型的问题吗?以两个泛型T和V的枚举类型Either为例,Left为关联T类型,Right关联V类型。

enum Either<T, V> {
  case Left(T)
  case Right(V)
}

提示:检验这个问题要用Xcode工程,不要在Playground中。注意这个问题和Swift1.2有关,你需要使用Xcode6.4版本。

答案

编译的时候会提示以下问题:

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

问题在于不能提前知道T分配内存大小,主要取决于T类型本身。但是在枚举类型中需要知道固定大小。
最常用的解决方法是采用一个泛型Box,如下:

class Box<T> {
  let value: T
  init(_ value: T) {
    self.value = value
  }
}
enum Either<T, V> {
  case Left(Box<T>)
  case Right(Box<V>)
}

这个问题只会影响Swift1.0或之后版本,在Swift2.0中已经解决。

问题 #2 - Swift1.0或者更高版本
闭包是值类型还是引用类型?
答案

闭包是引用类型。如果一个闭包赋值给一个变量,这个变量又复制一份copy给另一个变量,那么变量所引用的闭包和捕获的列表也会copy一份。

问题 #3 - Swift1.0或者更高版本
UInt类型用来存储无符号整数。它实现如下一个用有符号的整数构造器:

init(_ value: Int)

但是如果你提供一个负整数的话,下面代码会产生编译错误。

let myNegative = UInt(-1)

知道计算机负数是用二进制补码作为一个正数进行表示,你怎样把Int类型的负整数转为UInt数?

答案

已经有一个初始化器可以完成:

UInt(bitPattern: Int)

问题 #4 - Swift1.0或者更高版本
你能描述一下在Swift中哪些地方会产生循环引用?有什么解决办法吗?

当两个实例之间相互进行强引用的时候,就会引起循环引用。两个实例都不会释放内存,就会造成内存泄露。可用weak或者unowned打破实例之间的强引用问题,这样两个实例才有机会释放内存空间。

问题 #5 - Swift1.0或者更高版本
Swift2.0引用了一个新关键字能产生递归枚举类型。下面是一个带有Node节点的枚举类型,Node关联值类型,T和list:

enum List<T> {
    case Node(T, List<T>)
}

那个关键字是什么?
答案

关键字indirect允许递归枚举类型,像下面这样:

enum List<T> {
    indirect case Cons(T, List<T>)
}

结束语

所有的Swfit资源都来自于官方文档《The Swift Programming Language》,学习一门语言最好的方法是使用它。所以可以在Playground使用它或者在实际项目中运用它,Swift能够和Objective-C进行无缝对接。本文是翻译[Ray](1. http://www.raywenderlich.com/110982/swift-interview-questions-answers)的博客。

上一篇下一篇

猜你喜欢

热点阅读