http://www.cocoachina.com/swift/20150918/13499.html
-
原文:Swift Interview Questions and Answers
-
原作者:Antonio Bello
-
原作者介绍: Antonio 拥有丰富的编程经验。他开始编程的时候,内存单位还是 byte 而不是 gigabyte,存储空间还是可选的附加物,最常用的语言还是BASIC。现在,Antonio 的兴趣在于 iOS 应用开发、node.js 后端开发,而且他从来不会放过学习新东西的机会。他认为 Swift 是一门很有表现力的语言,不过 Objective-C 仍然是伟大而不同寻常的。
-
译者:本文由CocoaChina译者 浅夏@旧时光 翻译
虽然Swift出生才一年,但是它已经成为最流行的编程语言之一了。它的语法很简单,以至于当它发布的时候,JavaScript开发者感觉就像下图一样
事实上,Swift是一种复杂的语言。它包含面向对象和函数方法这两个方面,并且随着新版本的发布在一直进化。
Swift的知识浩如烟海——但是怎么测试你掌握了多少?在这篇文章中,我和这个raywenderlich.com网站的教学团队共同写了一个Swift面试问题列表。
你可以用这些问题来测试应聘者关于Swift方面的知识水平,或者测试一下你自己。如果你不知道答案,没关系,没一个问题下面都有答案供你学习。
这些问题包含两个方面:
每个方面有分成三个等级:
-
初级:适合读了一到两本有关Swift的书,并且已经开始用Swift开发应用程序的初学者。
-
中级:适合那些对Swift语言的概念有深刻理解和强烈兴趣的,并且一直在阅读大量有关Swift的博客文章并进行实践的中级工程师。
-
高级:适合那些以探索Swift语言知识为乐趣,挑战自己,使用前言技术的人们。
假如你想回答这些问题,我建议你在回答这些问题之前,打开Playground运行一下这些问题的代码。这些问题的答案都在Xcode 7.0 Beta 6 版本中测试过。
准备好了吗?系好安全带,现在就开始!
编者注:特别感谢raywenderlich.com教学团队成员Warren Burton, Greg Heo, Mikael Konutgan, Tim Mitra, Luke Parham, Rui 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得所有整数:
half-operator不包含数组中的最后一个元素,下面的例子会得到的结果和上面的一样:
问题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
|
答案:view1是个变量可以重新赋值给一个新的实例化的UIView对象。使用let你只赋值一次,所以下面的代码是不能编译的:
但是UIView是一个引用类型的类,所以你可以改变view2的属性,也就是说最后一行代码是可以编译的:
1
2
|
let view2 = UIView()
view2.alpha = 0.5
|
问题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
|
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"
|
这样做将会发生什么?错误出在什么地方呢?
答案: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语法的一种修饰。事实上下面语句输出是正确的:
记住枚举类型的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()
|
问题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])
|
现在要求你重写上面的方法作为Array的扩展方法,然后你就可以这样写代码:
1
|
[1, 2, 3, 3].countUniques()
|
如何实现?
答案:在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()
|
问题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" ])
}
}
|
这种情况 |
请发表评论