Swift面试资料 (二) —— Questions 和 Answers(二)

前言

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<T: Comparable>(_ array: Array<T>) -> 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
}

注意在最后一行没有隐式解包的运算符,因为你已经解包了dividenddivisor并将它们存储在非可选的不可变变量中。

请注意,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() {}
}

对于类,可以使用staticclass修饰符。 他们实现了相同的目标,但方式不同。 你能解释一下它们有何不同?

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

提示:有三种方法可以解决错误。

guardelse块需要退出路径,可以使用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的枚举示例,它采用两个相关的值类型TList

 

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

这将返回编译错误。 丢失的关键字是什么?

它是允许递归枚举情况的indirect关键字,如下所示:

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

恭喜你在Q&A结束之前,如果你不知道所有的答案,不要感到难过!

其中一些问题非常复杂。 Swift是一种丰富,富有表现力的语言,有很多值得学习的东西。 Apple不断改进 - 并且正在改变 - 所以即使是最有经验的开发人员也不知道一切。

Swift各方面的最终资源是Apple的官方文档The Swift Programming Language

在一天结束时,使用语言是学习它的最佳方式。 在playgroundSwift或在真实项目中使用它。 Swift(几乎)与Objective-C无缝地工作,因此将它添加到您已经知道的现有项目中是学习其来龙去脉的绝佳方式。


 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值