Swift 面试题及答案

一、swift访问级别

Swift 为代码中的实体提供了五种不同的访问级别。这些访问级别不仅与源文件中定义的实体相关,同时也与源文件所属的模块相关。

1、访问权限修饰

Open 和 Public 级别可以让实体被同一模块源文件中的所有实体访问,在模块外也可以通过导入该模块来访问源文件里的所有实体。通常情况下,你会使用 Open 或 Public 级别来指定框架的外部接口。Open 和 Public 的区别在后面会提到。
Internal 级别让实体被同一模块源文件中的任何实体访问,但是不能被模块外的实体访问。通常情况下,如果某个接口只在应用程序或框架内部使用,就可以将其设置为 Internal 级别。
File-private 限制实体只能在其定义的文件内部访问。如果功能的部分细节只需要在文件内使用时,可以使用 File-private 来将其隐藏。
Private 限制实体只能在其定义的作用域,以及同一文件内的 extension 访问。如果功能的部分细节只需要在当前作用域内使用时,可以使用 Private 来将其隐藏。
Open 为最高访问级别(限制最少),Private 为最低访问级别(限制最多)。

Open 只能作用于类和类的成员,它和 Public 的区别如下:

Public 或者其它更严访问级别的类,只能在其定义的模块内部被继承。
Public 或者其它更严访问级别的类成员,只能在其定义的模块内部的子类中重写。
Open 的类,可以在其定义的模块中被继承,也可以在引用它的模块中被继承。
Open 的类成员,可以在其定义的模块中子类中重写,也可以在引用它的模块中的子类重写。
把一个类标记为 open,明确的表示你已经充分考虑过外部模块使用此类作为父类的影响,并且设计好了你的类的代码了。

2、访问级别基本原则

Swift 中的访问级别遵循一个基本原则:实体不能定义在具有更低访问级别(更严格)的实体中。

例如:

一个 Public 的变量,其类型的访问级别不能是 Internal,File-private 或是 Private。因为无法保证变量的类型在使用变量的地方也具有访问权限。
函数的访问级别不能高于它的参数类型和返回类型的访问级别。因为这样就会出现函数可以在任何地方被访问,但是它的参数类型和返回类型却不可以的情况。
关于此原则在各种情况下的具体表现,将在下文有所体现。

3、默认访问级别

如果你没有为代码中的实体显式指定访问级别,那么它们默认为 internal 级别(有一些例外情况,稍后会进行说明)。因此,在大多数情况下,我们不需要显式指定实体的访问级别。

二、swift 与oc的区别

swift和OC的主要区别:

1.base class

Swift中可以定义不继承于其它类的类,称之为基类(base class),而OC的类都是继承自NSObject。

2.switch

在OC中的Switch:

    不能判断对象类型, 只能判断基本类型中的整数;可以穿透;default位置可以随便放。

在Swift中的Switch:

    可以判断对象类型,支持对象类型、元组、区间、double类型等等;默认不穿透,可以写fallthough穿透;可以不写break,如果写了default,default得放在最后。

3.final关键字

Swift中的final修饰符可以防止类被继承,还可以防止子类重写父类的属性、方法以及下标。

4.guard、inout

Swift中,guard关键字只能作用在函数中,guard关键字必须和else同时出现,guard关键字只有在条件为false的时候会走else语句,反之执行后边语句。

Swift中,inout是修饰函数参数类型,表示该参数在函数内修改后(即函数返回后),其值为修改后的值。

5.运算符重载

OC是不支持重载的,Swift是支持重载的。

6.命名空间

OC没有命名空间,但是在为了区分在不同框架中可能会使用相同名字的变量冲突的情况,OC使用前缀来区别。

Swift有命名空间,默认就是项目名称,同一个命名空间全局共享,这也是Swift不需要在import头文件。

7.swift类型安全,oc更灵活

类型安全语言需要代码里值的类型非常明确。

8.可选类型

Swift引入了可选项类型,用于处理变量值不存在的情况。Optionals类似于OC中指向nil的指针,但是适用于所有数据类型,而非仅仅局限于类,Optionals相比于OC中的nil指针,更加安全和简明。

9.字符串插值、String.Index

OC:[NSStringstringWithFormat:@"%d",10];

Swift: "\(10)"

10.OC是动态、Swift是静态(函数内联)

Swift 编译时就确定了调用的方法,OC是调用之前通过虚函数表来确定调用的方法。

11.泛型

泛型是用来使代码能安全工作,Swift中的范型可以在函数数据和普通数据类型中使用,例如类、结构体或枚举。范型可以解决代码复用的问题。

12.访问权限

Swift又新增了两种访问控制权限 fileprivate和 open。Swift中访问权限由大到小依次为:open > public > internal(默认)> fileprivate >private。

    open:可以在任何地方访问,包括override和继承。

    public:可以在任何地方访问,但其他module中不可以被override和继承,而在本module内可以被override和继承。

    internal:修饰的属性或方法在整个模块内都可以访问,系统默认。

    fileprivate:在当前文件内可以被访问。

    private:在当前类中能被访问。  

13.enum

Swift 的枚举成员在被创建时本身就是完备的值,这些值的类型是已经明确定义好的 Direction 类型. 不会像 Objective-C 一样被赋予一个默认的整型值。

14.swift 反射机制

所谓反射就是可以动态获取类型、成员信息,同时在运行时(而非编译时)可以动态调用任意方法、属性等行为的特性。反射最重要的目的,就是为了解耦。

在Swift中并不提倡使用Runtime,而是像其他语言一样使用反射(Reflect)。Swift的反射机制是基于一个叫 Mirror 的 struct 来实现的。

Swift其他功能:

1.范围运算符:

a...b 表示 [a,b] 包括a和b 。 (如3...5  就是范围取3,4,5)

a..<b 表示 [a,b) 包括a,不包括b 。 (如3..<5  就是范围取3,4)

常见的如for循环:for i in 0...9{}

2.独有的元组类型

元组(tuples)把多个值组合成一个复合值。元组内的值可以使任意类型,并不要求是相同类型。

eg: var value = (Int,String) = (x:15,y:"abc")

3.swift中使用let定义常量,var定义变量

使用常量,更加安全,不能够被修改,在需要对对象进行修改的时候只能用var修饰**。**

4.if let 、 guard let

缩减代码量,安全处理数据逻辑,作用是解包。

5.defer

**defer **block 里的代码会在函数 return 之前执行,无论函数是从哪个分支 return 的,还是有 throw,还是自然而然走到最后一行。    

6.高阶函数

高阶函数仅仅只是一个函数,其可以接收函数作为参数,或者返回一个函数来操作其他函数。Swift的集合类型中就有这些高阶函数:Map, FlatMap, Filter, 和Reduce。

Swift优点:

1.Swift容易阅读,语法和文件结构简易化。

2.Swift更易于维护,文件分离后结构更清晰。

3.Swift更加安全,它是类型安全的语言。

4.Swift代码更少,简洁的语法,可以省去大量冗余代码。

5.Swift速度更快,运算性能更高。

6.值类型优化:如写时拷贝

三、swift 闭包函数

1、闭包表达式种类

逃逸闭包

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @escaping,用来指明这个闭包是允许“逃逸”出这个函数的。

一种能使闭包“逃逸”出函数的方法是,将这个闭包保存在一个函数外部定义的变量中。举个例子,很多启动异步操作的函数接受一个闭包参数作为 completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。在这种情况下,闭包需要“逃逸”出函数,因为闭包需要在函数返回之后被调用。例如:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 函数接受一个闭包作为参数,该闭包被添加到一个函数外定义的数组中。如果你不将这个参数标记为 @escaping,就会得到一个编译错误。

将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。比如说,在下面的代码中,传递到 someFunctionWithEscapingClosure(_:) 中的闭包是一个逃逸闭包,这意味着它需要显式地引用 self。相对的,传递到 someFunctionWithNonescapingClosure(_:) 中的闭包是一个非逃逸闭包,这意味着它可以隐式引用 self

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出“200”

completionHandlers.first?()
print(instance.x)
// 打印出“100”

自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message 参数;它的 condition 参数仅会在 debug 模式下被求值,它的 message 参数仅当 condition 参数为 false 时被计算求值。

自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// 打印出“5”

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出“5”

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出“4”

尽管在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。请注意,customerProvider 的类型不是 String,而是 () -> String,一个没有参数且返回值为 String 的函数。

将闭包作为参数传递给函数时,你能获得同样的延时求值行为。

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出“Now serving Alex!”

上面的 serve(customer:) 函数接受一个返回顾客名字的显式的闭包。下面这个版本的 serve(customer:) 完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 @autoclosure 来接收一个自动闭包。现在你可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。customerProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure 特性。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”

注意

过度使用 autoclosures 会让你的代码变得难以理解。上下文和函数名应该能够清晰地表明求值是被延迟执行的。

如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure@escaping 属性。@escaping 属性的讲解见上面的 逃逸闭包

// customersInLine i= ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// 打印“Collected 2 closures.”
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// 打印“Now serving Barry!”
// 打印“Now serving Daniella!”

在上面的代码中,collectCustomerProviders(_:) 函数并没有调用传入的 customerProvider 闭包,而是将闭包追加到了 customerProviders 数组中。这个数组定义在函数作用域范围外,这意味着数组内的闭包能够在函数返回之后被调用。因此,customerProvider 参数必须允许“逃逸”出函数作用域。

四、swift 结构体与类的区别

Swift 类和结构体的定义方式是比较相似的,从下面代码可以直观的看在Swift的类和结构体格式上的区别。

//类定义
class LGPerson {
    var name: String;
    var age: Int;
    
    init(_ age: Int, _ name: String) {
        self.age = age;
        self.name = name;
    }
    deinit{}
}
// 结构体定义
struct LGPerson {
    var name: String;
    var age: Int;
    
    init(_ age: Int, _ name: String) {
        self.age = age;
        self.name = name;
    }
}

类与结构体的相同点与不同点

相同点

定义存储值的属性
定义方法
定义下标以使用下标语法提供对其值的访问
定义初始化器
使用 extension 来拓展功能
遵循协议来提供某种功能

不同点

类有继承的特性,而结构体没有
类型转换使您能够在运行时检查和解释类实例的类型
类有析构函数用来释放其分配的资源
引用计数允许对一个类实例有多个引用
在内存上:

    类是引用类型,是一个类类型的变量存储的是当前实例内存地址的引用;

    结构体是一个典型的值类型,是存储的具体的实例,可以理解成具体的值。

类与结构体最本质的区别

类是引用类型、结构体是值类型

1、通过修改属性字段的结果来看,我们可以把引用类型可以理解成一个在线共享的Excel,其他人对其修改,都是针对的同一个Excel进行修改(同一个源文件);而值类型就相当于本地的Excel,其他人对其修改,都是从本地Excel拷贝出一份,这样别人修改了什么,本地的不会感知的(不同的源文件)。

2、存储位置不同:值类型存储在栈上,引用类型存储在堆上。

了解这些之后,我们可以分析出开发过程中一些不需要继承特性的、不需要再运行时检查和解释实例的,都尽量使用Struct(结构体),因为计算机的读写存储速度在栈上是大于堆上的读写速度,这样就可以针对开发过程中的性能优化有了一个解决的方向。

x/8g : 可以读取内存中的值(8g表示8字节格式的输出)

栈区Stack、堆区Heap、全局区Global

栈主要存放局部变量、方法参数,由系统分配,快速高效
堆区主要存放对象(内存地址)
全局区存储全局变量、常量、代码区
了解这些之后,我们怎么去得知我们想要的对象是存放在在栈区还是堆区呢?这时候我们就可以使用一些llbd的命令去查看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容