版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.04.24 星期三 |
前言
Swift作为一门开发语言,它到目前为止也四岁了,接下来这个专题主要收集一下Swift面试相关的问题。感兴趣的看下面几篇文章。
1. Swift面试资料 (一) —— Questions 和 Answers(一)
Intermediate Written Questions
现在,加快一点难度。 你准备好了吗?
1. Question #1
nil
和.none
之间有什么区别?
没有区别,因为
Optional.none
(简称.none
)和nil
是等价的。实际上,这句话输出为真:
nil == .none
使用
nil
更常见,是推荐的惯例。
2. Question #2
这是thermometer
作为类和结构体的模型。 编译器会报错最后一行。 为什么编译失败?
提示:在
playground
上测试之前,请仔细阅读代码并考虑一下。
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)
使用可变函数正确声明
ThermometerStruct
以更改其内部变量temperature
。 编译器报错是因为你在通过let
创建的实例上调用了registerTemperature
,因此它是不可变的。 将let
改为var
以使示例编译。对于结构体,您必须将将内部状态更改为
mutating
的方法标记,但不能从不可变变量中调用它们。
3. Question #3
这段代码会打印什么?为什么?
var thing = "cars"
let closure = { [thing] in
print("I love \(thing)")
}
thing = "airplanes"
closure()
它会打印:
I love cars
。 声明闭包时,捕获列表会创建一个thing
副本。 这意味着即使为thing
分配新值,捕获的值也不会改变。如果省略闭包中的捕获列表,则编译器使用引用而不是副本。 因此,当您调用闭包时,它会反映对变量的任何更改。 您可以在以下代码中看到:
var thing = "cars" let closure = { print("I love \(thing)") } thing = "airplanes" closure() // Prints: "I love airplanes" `
4. Question #4
这是一个全局函数,用于计算数组中唯一值的数量:
func countUniques(_ array: Array) -> Int {
let sorted = array.sorted()
let initial: (T?, Int) = (.none, 0)
let reduced = sorted.reduce(initial) {
($1, $0.0 == $1 ? $0.1 : $0.1 + 1)
}
return reduced.1
}
它使用sorted
,因此它将T
限制为符合Comparable
的类型。
你这样调用它:
countUniques([1, 2, 3, 3]) // result is 3
将此函数重写为Array
上的扩展方法,以便您可以编写如下内容:
[1, 2, 3, 3].countUniques() // should print 3
您可以将全局
countUniques(_ :)
重写为Array
扩展:extension Array where Element: Comparable { func countUniques() -> Int { let sortedValues = sorted() let initial: (Element?, Int) = (.none, 0) let reduced = sortedValues.reduce(initial) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) } return reduced.1 } }
请注意,仅当泛型
Element
类型符合Comparable
时,新方法才可用。
5. Question #5
这是一个两个可选值相除的函数。 在执行实际相除之前,有三个先决条件需要验证:
- 被除数必须包含非
nil
值。 - 除数必须包含非
nil
值。 - 除数不能为零。
func divide(_ dividend: Double?, by divisor: Double?) -> Double? {
if dividend == nil {
return nil
}
if divisor == nil {
return nil
}
if divisor == 0 {
return nil
}
return dividend! / divisor!
}
使用guard
语句并且不使用强制解包来改进此函数。
Swift 2.0
中引入的guard
语句在不满足条件时提供退出路径。 检查前置条件时它非常有用,因为它可以让您以清晰的方式表达它们 - 没有嵌套if
语句的厄运金字塔。 这是一个例子:guard dividend != nil else { return nil }
您还可以使用
guard
语句进行可选绑定,这使得在guard
语句之后可以访问unwrapped
变量:guard let dividend = dividend else { return .none }
因此,您可以将
divide
函数重写为:func divide(_ dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend else { return nil } guard let divisor = divisor else { return nil } guard divisor != 0 else { return nil } return dividend / divisor }
注意在最后一行没有隐式解包的运算符,因为你已经解包了
dividend
和divisor
并将它们存储在非可选的不可变变量中。请注意,
guard
语句中未解包的选项的结果可用于语句出现的其余代码块。您可以通过对
guard
语句进行分组来进一步简化此操作:func divide(_ dividend: Double?, by divisor: Double?) -> Double? { guard let dividend = dividend, let divisor = divisor, divisor != 0 else { return nil } return dividend / divisor }
6. Question #6
使用if let
语句重写问题5中的方法。
if let
语句允许您解包选项并使用该代码块中的值。 请注意,您无法访问块外的未包装选项。 您可以使用if let
语句编写函数,例如:func divide(_ dividend: Double?, by divisor: Double?) -> Double? { if let dividend = dividend, let divisor = divisor, divisor != 0 { return dividend / divisor } else { return nil } }
Intermediate Verbal Questions
1. Question #1
在Objective-C
中,您声明一个这样的常量:
const int number = 0;
这是Swift的对应的:
let number = 0
它们之间有什么区别?
const
是在编译时使用值或表达式初始化的变量,必须在编译时解析。使用
let
创建的不可变是在运行时确定的常量。 您可以使用静态或动态表达式对其进行初始化。 这允许声明如下:let higherNumber = number + 5
请注意,您只能分配一次其值。
2. Question #2
要声明静态属性或函数,请在值类型上使用static
修饰符。 这是一个结构体的例子:
struct Sun {
static func illuminate() {}
}
对于类,可以使用static
或class
修饰符。 他们实现了相同的目标,但方式不同。 你能解释一下它们有何不同?
static
使属性或函数静态static
而不可覆盖overridable
。 使用class
可以覆盖属性或函数。应用于类时,
static
将成为class final
的别名。例如,在此代码中,当您尝试覆盖
illuminate()
时,编译器会报错:class Star { class func spin() {} static func illuminate() {} } class Sun : Star { override class func spin() { super.spin() } // error: class method overrides a 'final' class method override static func illuminate() { super.illuminate() } }
3. Question #3
您可以使用扩展名extension
将存储的属性添加到类型中吗? 怎么做或为什么不可以这么做呢?
不,这是不可能的。 您可以使用扩展来向现有类型添加新行为,但不能更改类型本身或其接口。 如果添加存储的属性,则需要额外的内存来存储新值。 扩展程序无法管理此类任务。
4. Question #4
Swift中的协议是什么?
协议是一种定义方法,属性和其他要求的
blueprint
的类型。 然后,类,结构或枚举可以采用协议来实现这些要求。采用协议要求的类型符合该协议。 协议本身不实现任何函数,而是定义函数。 您可以扩展协议以提供某些要求的默认实现或符合类型可以利用的其他函数。
Advanced Written Questions
1. Question #1
考虑以下模拟thermometer
的结构:
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
定义了一些协议,使您可以使用赋值运算符初始化具有文字值的类型。 采用相应的协议并提供公共初始化器允许特定类型的文字初始化。 对于Thermometer
,您可以按如下方式实现ExpressibleByFloatLiteral
:extension Thermometer: ExpressibleByFloatLiteral { public init(floatLiteral value: FloatLiteralType) { self.init(temperature: value) } }
现在,您可以使用
float
创建实例。var thermometer: Thermometer = 56.8
2. Question #2
Swift
有一组预定义的运算符来执行算术或逻辑运算。 它还允许创建自定义运算符,一元或二元。
使用以下规范定义和实现自定义^^
幂运算符:
- 取两个
Int
作为参数。 - 返回第一个引用第二个参数的幂。
- 使用标准的代数运算顺序正确评估方程。
- 忽略溢出错误的可能性。
您可以通过两个步骤创建新的自定义运算符:声明和实现。
声明使用
operator
关键字指定类型(一元或二元),组成运算符的字符序列,其关联性和优先级。 Swift 3.0改变了优先级的实现以使用优先级组。这里,运算符是
^^
,类型是infix
(二进制)。 相关性是right
;换句话说,相等的优先级^^
运算符应该从右到左评估等式。Swift中的指数运算没有预定义的标准优先级 predefined standard precedence。 在代数的标准操作顺序中,指数应在乘法/除法之前计算。 因此,您需要创建一个自定义优先级,使其高于乘法。
这是声明:
precedencegroup ExponentPrecedence { higherThan: MultiplicationPrecedence associativity: right } infix operator ^^: ExponentPrecedence
具体实现如下所示:
func ^^(base: Int, exponent: Int) -> Int { let l = Double(base) let r = Double(exponent) let p = pow(l, r) return Int(p) }
请注意,由于代码不考虑溢出,如果操作产生
Int
无法表示的结果(例如大于Int.max
的值),则会发生运行时错误。
3. Question #3
请考虑以下代码,将Pizza
定义为struct
,将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"])
}
}
您现在将Lombardi
餐厅定义如下:
struct Lombardis: Pizzeria {
func makePizza(_ ingredients: [String]) -> Pizza {
return Pizza(ingredients: ingredients)
}
func makeMargherita() -> Pizza {
return makePizza(["tomato", "basil", "mozzarella"])
}
}
以下代码创建了Lombardi
的两个实例。 这两个中的哪一个会用罗勒制作margherita
?
let lombardis1: Pizzeria = Lombardis()
let lombardis2: Lombardis = Lombardis()
lombardis1.makeMargherita()
lombardis2.makeMargherita()
他们都可以做到。
Pizzeria
协议声明了makeMargherita()
方法并提供了默认实现。Lombardis
实现会覆盖默认方法。 由于在两种情况下都在协议中声明方法,因此您将在运行时调用正确的实现。如果协议没有声明
makeMargherita()
方法但扩展仍然提供默认实现,如下所示怎么办?protocol Pizzeria { func makePizza(_ ingredients: [String]) -> Pizza } extension Pizzeria { func makeMargherita() -> Pizza { return makePizza(["tomato", "mozzarella"]) } }
在这里,只有
lombardis2
会用basil
制作披萨,而lombardis1
会在没有它的情况下制作披萨,因为它会使用扩展中定义的方法。
4. Question #4
以下代码具有编译时错误。 你能发现它并解释它为什么会发生吗? 有什么方法可以解决它?
struct Kitten {
}
func showKitten(kitten: Kitten?) {
guard let k = kitten else {
print("There is no kitten")
}
print(k)
}
提示:有三种方法可以解决错误。
guard
的else
块需要退出路径,可以使用return
,抛出异常或调用@noreturn
。 最简单的解决方案是添加一个return
语句。func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") return } print(k) }
这是一个抛出异常的版本。
enum KittenError: Error { 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(kitten: nil)
最后,这是一个调用
fatalError()
的实现,它是一个@noreturn
函数。struct Kitten { } func showKitten(kitten: Kitten?) { guard let k = kitten else { print("There is no kitten") fatalError() } print(k) }
Advanced Verbal Questions
1. Question #1
闭包是值还是引用类型?
闭包是引用类型。 如果为变量分配闭包并将变量复制到另一个变量,则还要复制对同一闭包及其捕获列表的引用。
2. Question #2
您使用UInt
类型来存储无符号整数。 它实现了以下初始化程序以从有符号整数转换:
init(_ value: Int)
但是,如果您提供负值,则以下代码会生成编译时错误异常:
let myNegative = UInt(-1)
根据定义,无符号整数不能为负数。 但是,可以使用负数的内存表示转换为无符号整数。 如何在保持内存表示的同时将Int
负数转换为UInt
?
有一个初始化器:
UInt(bitPattern: Int)
下面就是实现:
let myNegative = UInt(bitPattern: -1)
3. Question #3
你能描述Swift中的循环引用吗? 你怎么解决它?
当两个实例彼此持有强引用时会发生循环引用,从而导致内存泄漏,因为这两个实例都不会被释放。 原因是只要有一个强引用就不能解除分配
deallocate
实例,但是每个实例由于其强引用而使另一个实例保持活动状态。您可以通过使用弱
weak
引用或无主unowned
引用替换其中一个强引用来打破强循环引用来解决问题。
4. Question #4
Swift允许创建递归枚举。 下面是一个带有Node
case的枚举示例,它采用两个相关的值类型T
和List
:
enum List {
case node(T, List)
}
这将返回编译错误。 丢失的关键字是什么?
它是允许递归枚举情况的
indirect
关键字,如下所示:enum List
{ indirect case node(T, List ) }
恭喜你在Q&A结束之前,如果你不知道所有的答案,不要感到难过!
其中一些问题非常复杂。 Swift是一种丰富,富有表现力的语言,有很多值得学习的东西。 Apple不断改进 - 并且正在改变 - 所以即使是最有经验的开发人员也不知道一切。
Swift各方面的最终资源是Apple的官方文档The Swift Programming Language。
在一天结束时,使用语言是学习它的最佳方式。 在playground
玩Swift
或在真实项目中使用它。 Swift(几乎)与Objective-C无缝地工作,因此将它添加到您已经知道的现有项目中是学习其来龙去脉的绝佳方式。
后记
本篇主要讲述了Swift面试资料的Questions 和 Answers,感兴趣的给个赞或者关注~~~