1、类(class)和结构体(struct)有什么区别?
在Swift
中,class
是引用类型,struct
是值类型。值类型在传递和赋值的过程中将进行复制,而引用类型则只会使用引用对象的一个指向,所以两者之间的主要区别还是类型的区别。
看个简单例子,代码如下:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
struct SomeStruct {
var name: String
}
简单使用
let person = Person(name: "Jack")
let anotherPerson = person
anotherPerson.name = "Justin"
print(person.name, anotherPerson.name) //Justin Justin
let structA = SomeStruct(name: "Lisa")
var structB = structA
structB.name = "Judy"
print(structA.name, structB.name) //Lisa Judy
由上面代码可知,Person
是class
类型即引用类型,所以当修改anotherPerson
对象的name
属性时候,原有person
对象的name
值也发生了改变,而SomeStruct
是struct
,是值类型,所以structB
对象的name
属性改变并不会影响structA
的name
值。
在内存中,引用类型,诸如类,是在堆上进行存储和操作的,而值类型,诸如结构体,则是在栈上存储和操作的。相比栈上的操作,堆上的操作更加复杂耗时,所以苹果公司官方推荐使用结构体,这样可以提高APP的运作效率。
两者的区别
- class的如下功能是struct没有的:
1、可以继承,这样子类可以使用父类的特性和方法
2、类型转换可以在运行时检查和解释一个实例的类型
3、可以用deinit
来释放资源
4、一个类可以被多次引用
- struct也有如下优势:
1、结构较小,适用于复制操作,相比一个class
的实例被多次引用,struct
更安全
2、无须担心内存泄露或者多线程冲突问题
什么时候使用类(class)什么时候使用结构体(struct)?
- 如果是封装一些简单数据请使用
struct
- 如果希望封装的值是被复制而不是引用请使用
struct
- 无须使用继承来使用相关属性和行为请使用
struct
除以上情况请使用class
对于class
和struct
的详细内容可以看这里
2、Swift是面向对象还是函数式的编程语言?
Swift
既是面向对象的编程语言,也是函数式的编程语言。
说Swift
是面向对象的编程语言,是因为Swift
支持类的封装、继承和多态,从这一点来看,Swift
与Java
这类纯面向对象的编程语言几乎没什么差别。
说Swift
是函数式的编程语言,那是因为Swift
支持map
,reduce
,filter
,flatmap
这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。
3、在Swift中,什么是可选类型(optionals)?
在Swift
中,可选型是为处理值可能缺失的情况,当一个变量值为空时,那么该值就是nil
。在Swift
中,无论变量是引用类型还是值类型,都可以是可选类型变量
例如:
var value: Float? = 37.0 //值类型为Float,value的默认值为37.0
var key: String? = nil //值类型为string,key的默认值为nil
let image: UIImage? //引用类型为UIImage,image的默认值为nil
在OC
中没有明确提出可选型的概念,然而,其引用类型却可以为nil
,以此来标志其变量值为空的情况,而在Swift
中这一理念扩大到值类型,并且明确提出来可选型的概念
4、在Swift中,什么是泛型?
在Swift中,泛型主要是为增加代码的灵活性而生的,它可以使对应的代码满足任意类型的变量或者方法。
举个简单例子:假如要写一个方法可以交换两个Int
类型变量的值,通常我们会这样实现:
func swap(_ a: inout Int, _ b: inout Int) {
(a, b) = (b, a)
}
上面的函数写法正确但是并不高效和通用,因为,假如需要实现一个方法交换两个Float
值,那么又得重新写一般。两个方法的差别仅仅是输入参数的类型不同。范型就是为来解决这类问题而来的,通过实现一个一般性的方法,可以交换任意类型的变量
func swap<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}
注意:Swift是类型安全的语言,所以这里交换两个变量的类型必须一致
5、关键字:open
,public
,internal
,fileprivate
,private
Swift
中有五个级别的访问控制权限,从高到低依次为open
,public
,internal
,fileprivate
,private
它们遵循的基本原则是:高级别的变量不允许被定义为低级别变量的成员变量。例如:一个private
的class中不能包含public
的String值。反之,低级别的变量却可以定义在高级别的变量中。比如,public
的class中可以含有private
的Int值
open
:具备最高的访问权限。其修饰的类和方法可以在任意的module中被访问和重写;它是Swift3中新添加的访问权限
public
:public
的权限仅次于open
。与open
唯一的区别在于,它修饰的对象可以在任意module
中被访问,但不能被重写
internal
:默认的权限。它表示只能在当前定义的module
中访问和重写,它可以被一个module
中的多个文件访问,但不可以被其他module
访问
fileprivate
:也是Swift3
新添加的权限,其修饰的对象只能在当前文件中使用。例如:它可以被一个文件中的class
、extension
、struct
共同使用
private
:最低级别的访问权限。它的对象只能在定义的作用域内使用,离开来这个作用域,即使是同一个文件中的其他作用域,也无法访问。
6、比较关键词:strong
、weak
、unonwed
Swift
的内存管理机制与OC
一样,都是ARC(Automatic Reference Counting)
自动引用计数。其基本原理是,一个对象在没有强引用指向它时,所占用的内存就会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中
strong
:强引用,是默认属性。当一个对象被声明为strong
时,表示对该对象进行强引用,此时,该对象的引用计数会增加1
weak
:弱引用,会被定义为可选类型变量。当一个对象被声明为weak
时,表示对该对象不会对其引用的实例保持强引用,该对象的引用计数不会增加1。在该对象被释放后,弱引用也随机消失。继续访问该对象,程序会得到nil
,并不会奔溃。另外注意:当 ARC
设置弱引用为 nil
时,属性观察不会被触发。
unowned
:无主引用,与弱引用的本质一样。唯一不同的是,对象被释放后,依然有一个无效的引用指向对象,它不是optional
,也不指向nil
。如果继续访问该对象,则程序就会奔溃
引入weak
和unowned
是为了解决由strong
带来的循环引用问题。简单来说,就是当两个对象互相有一个强引用指向对方时,就会导致两个对象在内存中无法释放。
weak
和unwoned
的使用场景有如下区别:
当访问对象可能已经被释放时,则使用weak
,比如:delegate
的修饰
当访问对象不可能被释放时,则用unwoned
,比如:self
的引用
实际上,为了安全,很多情况下基本使用weak
7、在Swift中,如何理解copy-on-write
当值类型(比如:struct
)在复制时,复制的对象和原对象实际上在内存中指向同一个对象。当前仅当修改复制后的对象时,才会在内存中重新创建一个新的对象。
let arrayA = [1,2,3]
var arrayB = arrayA // 这时arrayB和arrayA在内存中时同一个数组,内存中并没有生成新的数组
print("before: \(arrayA), b: \(arrayB)") //before: [1, 2, 3], b: [1, 2, 3]
arrayB.append(4) // arrayB被修改了,此时arrayB在内存中变成了一个新的数组,而不是原来的arrayA
print("after: \(arrayA), b: \(arrayB)") //after: [1, 2, 3], b: [1, 2, 3, 4]
从上面的代码可以看出,复制的数组和原数组共享同一个地址,直到其中之一发生改变。这样设计使得值类型可以被多次复制而无须耗费多余的内存,只有变化的时候才增加开销。因此内存的使用更加高效
8、属性观察(Property Observer)
属性观察是指在当前类型内对特定属性进行监听,并作出响应的行为。属性观察是Swift
的特性,具有两种:willSet
和didSet
.
var title: String {
willSet {
print("将标题从\(title)设置到\(newValue)")
}
didSet {
print("已将标题从\(oldValue)设置到\(title)")
}
}
上面代码都对title
进行了监听,在title
发生改变之前,willSet
对应的作用域将被执行,新的值是newValue
,在title
发生改变之后,didSet
对应的作用域将被执行,原来的值为oldValue
,这就是属性观察
注意:在初始化方法对属性的设定,以及在
willSet
和didSet
中对属性的再次设定,都不会触发调用属性观察
9、在结构体中如何修改成员变量的方法
下面代码存在什么问题
protocol Pet {
var name: String { get set }
}
struct MyDog: Pet {
var name: String
func changeName(name: String) {
self.name = name
}
}
一旦我们编写这样的代码,编译器就会告诉我们当前的self
是不可变的,我们不能为name
进行复制,如果想要修改name
属性,那么必须在方法前加上mutating
关键词,表示该方法会修改结构体中自己的成员变量
注意:
1、类不存在这样的问题,因为类可以随意修改自己的成员变量
2、在设计协议的时候,由于protocol
可以被class
和struct
以及enum
实现,所以需要考虑是否使用mutating
关键词来修饰方法
10、如何使用Swift实现或(||)操作
直接一点的解法:
func ||(left: Bool, right: Bool) -> Bool {
return left ? left: right
}
上面解法虽然正确,但是效率不高。或(||)操作的本质是,当表达式左边的值为真的时候,无须计算表达式右边的值。而上面的解法是将表达式右边的默认值准备好了,再传入进行操作。当表达式右边的值计算释放复杂时,会造成性能上的浪费,所以,上面这种做法违反了或(||)操作的本质。正确的实现方法如下:
func ||(left: Bool, right: @autoclosure () -> Bool) -> Bool {
return left ? left: right()
}
自动闭包(autoclosure
)可以将表达式右边的值的计算推迟到判定left
为false
时,这样就可以避免第一种方法带来的不必要的计算。
11、实现一个函数:输入是任意一个整数,输出为输入的整数+2
题目看起来很简单,直接写代码如下所示:
func addTwo(_ num: Int) -> Int {
return num + 2
}
但是接下来要实现输入的整数加4的功能,难道我们又写如下代码:
func addFour(_ num: Int) -> Int {
return num + 4
}
如果要是加8加10呢?能不能定义一个方法解决所有的问题呢?必须的,使用柯里化(currying)特性
func add(_ num: Int) -> (Int) -> Int {
return { num + $0 }
}
// 简单使用
let addTwo = add(2)
print(addTwo(10)) // 12
let addFour = add(4)
print(addFour(10)) // 14
let addSix = add(6)
print(addSix(10)) // 16
Swift
的柯里化特性是函数式编程思想的体现,它将接受多个参数的方法进行变形,并用高阶函数的方式进行处理,使整个代码更加灵活
12、求0-100(包括0和100)中为偶数并且恰好是其他数字平方的数字
题目也很简单,满足两个条件即可,实现如下:
func num(fromValue: Int, toValue: Int) -> [Int] {
var result = [Int]()
for num in fromValue...toValue where num % 2 == 0 {
if (fromValue...toValue).contains(num * num) {
result.append(num)
}
}
return result
}
上面的实现虽然正确,但是不够优雅,其实直接使用函数式编程思路实现简洁,如下:
(0...10).map { $0 * $0 }.filter { $0 % 2 == 0 }
13、Swift为什么将String,Array和Dictionary设计成为值类型
要了解Swift
为什么将String
,Array
和Dictionary
设计成为值类型,就要和OC
中相同的数据结构设计进行比较。在OC
中,String
,Array
和Dictionary
是皆为引用类型。
值类型相比引用类型,最大的优势在于可以高效地使用内存。值类型在栈上进行操作,引用类型在堆上操作。栈上的操作仅仅是单个指针的上下移动,而堆上的操作则牵涉合并、移位、重链接等。也就说,Swift
这样设计大幅度减少了堆上的内存分配和回收的次数。同时,copy-on-write
又将值传递和复制的开销降到最低。
Swift
将String
,Array
和Dictionary
设计成为值类型也是为了线程安全。通过Swift
的let
设置,使得这些数据达到真正意义上的“不变”,也从根本上解决了多线程中内存访问和操作顺序的问题。
Swift
将String
,Array
和Dictionary
设计成为值类型还可以提升API 的灵活度。例如,通过实现Collection
这样的协议,可以遍历String
,使得整个开发更加灵活、高效。
14、如何使用Swift将协议(protocol)中的部分方法设计成为可选
@optional
和@required
是OC
中特有的关键字。
在Swift
中,协议中所有的方法默认都必须实现,而且在协议中,方法是不能直接被定义为optional
。有两种解决方案:
1)在协议和方法前面都加@objc
关键字,然后再在方法前加上optional
关键字。该方法实际上把协议转换为OC
的方式,然后就可以进行可选定义了。如下:
@objc protocol SomeProtocol {
func run() // 必须实现方法
@objc optional func jump() //可选方法
}
2)使用扩展(extension
)来规定可选方法。在Swift中,协议扩展可以定义部分方法的默认实现,这样,这些方法在实际调用中就是可选实现来。如:
protocol SomeProtocol {
func run() // 必须实现方法
func jump() //可选方法
}
extension SomeProtocol {
func jump() {
print("jump")
}
}
class Person: SomeProtocol {
func run() { //只需要实现run方法
print("run")
}
}
15、协议的代码实战
下面代码有什么问题?
protocol SomeProtocol {
func doSomething()
}
class Person {
weak var delegate: SomeProtocol?
}
声明delegate
属性的时候错误,编译器会报错。
Swift
中协议不仅可以被class
这样的引用类型实现,也可以被struct
和enum
这样的值类型实现(这是和OC
的一大区别)。weak
关键词是ARC
环境下,为引用类型提供引用计数这样的内存管理,它是不能被用来修饰值类型的。
有两种方法解决这个问题:
1)在protocol
前面加上@objc
。在OC
中,协议只能由class
来实现,这样一来,weak
修饰的对象与OC
一样,只不过是class
类型。如下:
@objc protocol SomeProtocol {
func doSomething()
}
2)在SomeProtocol
之后添加class
关键词。如此一来就声明该协议只能由类(class
)来实现。如下:
protocol SomeProtocol: class {
func doSomething()
}
16、在Swift和OC的混合项目中,如何在Swift文件中调用OC文件中定义的方法?又如何在OC文件中调用Swift文件中定义的方法
在Swift
中,若要使用OC
代码,则可以在ProjectName-Bridging-Header.h
文件中添加OC
的头文件名称,这样在Swift
文件中即可调用相应的OC
代码。一般情况下,Xcode
会在Swift
项目中第一次创建OC
文件时,自动创建ProjectName-Bridging-Header.h
文件
在OC
中如果想要调用Swift
代码,则可以导入Swift
生成的头文件ProjectName-Swift.h
文件
在Swift
文件中,若要将固定的方法或属性暴露给OC
使用,则可以在方法或属性前加上@objc
。如果该类是NSObject
子类,那么Swift
会在非private
的方法或属性前自动加上@objc.
17、比较Swift和OC的初始化方法的异同
简单来说:Swift
的初始化方法更加严格和准确
在OC
中,初始化方法无法保证所有成员变量都完成初始化;编译器对属性甚至并无警告,但是,在实际操作中会出现初始化不完全的问题;初始化方法与普通方法并无实际差别,可以多次调用。
在Swift
中,初始化方法必须保证所有非optional
的成员变量都完成初始化;同时,新增convenience
和required
两个修饰初始化方法的关键词。
-
convenience
:只是提供一个方便的初始化方法,必须通过调用同一个类中的designated
初始化方法来完成 -
required
:强制子类重写父类中所修饰的初始化方法
18、比较Swift和OC中协议的不同
相同点:两者都可以被用作代理,OC
中的protocol
类似Java
中的Interface
,在实际开发中主要用于适配器模式
不同点:Swift
中的protocol
还可以对接口进行抽象,例如:sequence
,配合扩展(extension
),泛型、关联类型等可以实现面向协议编程,从而提高整个代码的灵活性。同时,Swift
中的protocol
还可以用于值类型,如:结构体和枚举
19、谈谈对OC和Swift动态性理解
Runtime
其实就是OC
的动态机制。Runtime
执行的是编译后的代码,这时它可以动态加载对象、添加方法、修改属性、传递信息等。具体过程是,在OC
中,对象调用方法时,如[self.tableView reloadData]
,经历了两个阶段:
-
编译阶段:编译器会把这句话翻译为
objc_msgSend(self.tableView, @selector(reloadData))
,把消息发送给tableView
-
运行阶段:接受者
self.tableView
会响应这个消息,期间可能会直接执行、转发消息,也可能会找不到方法导致程序奔溃。所以,整个流程是:编译器编译->给接收者发送消息->接收者响应消息
例如:[self.tableView reloadData]
中,self.tableView
就是接收者,reloadData
就是消息,所以,方法调用的格式在编译器看来是[receiver message]
。其中,接收者响应代码,就发生在运行时(Runtime
)。Runtime
的运行机制就是OC
的动态特性
Swift
目前被公认为是一门静态的语言,它的动态特性都是通过桥接OC
实现的。如果要把其动态特性写的更“Swift”一点,则可以用protocol
来处理,比如:可以将OC
中的reflection
这样写:
if ([someImage respondsToSelector: @selector(shake)]) {
[someImage performSelector: shake];
}
在Swift
中实现如下
if let shakeableImage = someImage as? Shakeable {
shakeableImage.shake()
}
20、语言特性代码实战
下面代码输出什么?
protocol Chef {
func makeFood()
}
extension Chef {
func makeFood() {
print("make food")
}
}
struct SeafoodChef: Chef {
func makeFood() {
print("make seafood")
}
}
let chefOne: Chef = SeafoodChef()
let chefTwo: SeafoodChef = SeafoodChef()
chefOne.makeFood()
chefTwo.makeFood()
代码运行输出:
make seafood
make seafood
在Swift
中,协议中是动态派发,扩展中是静态派发,也就是说,协议中如果有方法声明,那么会根据对象的实际类型进行调用。
上面makeFood()
方法在Chef
协议中已经声明了,而chefOne
虽然声明为Chef
,但实际实现为SeafoodChef
。所以,根据实际情况,makeFood()
会调用SeafoodChef
中的实现。chefTwo
也是同样的道理。
如果protocol
中没有声明makeFood()
方法,代码又会输出什么?
运行如下:
make food
make seafood
因为协议中没有声明makeFood()
方法,所以,此时只会按照扩展中的声明类型进行静态派发。也就是说,会根据对象的声明类型进行调用。chefone
被声明为Chef
,所以会调用扩展的实现,chefTwo
被声明为SeafoodChef
,则会调用SeafoodChef
中的实现。
21、Swift和OC的自省有什么不同
自省在OC中就是判断一个对象是否属于某个类。它有两种形式:
[object isKindOfClass: [SomeClass class]];
[object isMemberOfClass: [SomeClass class]];
isKindOfClass
是用来判断object
是否为SomeClass
或者其子类的实例对象。
isMemberOfClass
则是判断object
是SomeClass
(非子类)的实例对象时,才返回真。
注意:这两个方法都有一个前提,即object
必须是NSObject
或其子类
在Swift
中,由于很多class
并非继承自NSObject
,故而Swift
用is
函数来进行判断,它相当于isKindOfClass
。这样做的优点是is
函数不仅可以用于任何class
类型上,也可以用来判断enum
和struct
类型
自省经常是与动态一起使用,动态类型就是id类型
。任何类型的对象都可以用id
来代替,这个时候常常需要自省来判断对象的实际所属类。如下:
id vehicle = someCarInstance;
if ([vehicle isKindOfClass: [Car Class]]) {
NSLog(@"vehicle is a car");
} else if ([vehicle isKindOfClass: [Trunk class]]) {
NSLog(@"vehicle is a truck");
}
22、能否通过Category给已有的类添加属性
可以通过Category
给已有的类添加属性,无论是OC
还是Swift
。
在OC
中,正常情况下,在Category
添加属性会报错,提示找不到getter
和setter
方法,这是因为Category
不会自动生成这两个方法。解决的办法是引入运行时头文件,并配合关联对象的方法来实现。其中主要涉及的两个函数是objc_getAssociatedObject
和objc_setAssociatedObject
。在Swift
中,解决办法OC
是一样的,只是写法上不一样。
假如有一个class
叫做User
,我们在其Category
中添加name
属性,如下:
// .h
#import "User.h"
@interface User (Add)
@property (nonatomic, copy) NSString *name;
@end
// .m
#import "User+Add.h"
#import <objc/runtime.h>
static const void *nameKey = @"nameKey";
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, nameKey);
}
@end
代码分析:
1、在.h文件中添加name
属性,此属性为私有
2、在.m文件中引入运行时头文件 <objc/runtime.h>
,接着设置关联属性的key,最后实现setter
和getter
。
其中,objc_setAssociatedObject
这个方法的四个参数分别为原对象、关联属性key
、关联值、关联策略。
Swift
中的实现如下:
private var nameKey: Void?
class User: NSObject {}
extension User {
var name: String? {
get {
return objc_getAssociatedObject(self, &nameKey) as? String
}
set {
objc_setAssociatedObject(self, &nameKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}
23、OC和Swift在单例模式的创建上有什么区别
单例模式在创建过程中,要保证实例变量只被创建一次,在整个开发中需要特别注意线程安全,即使在多线程情况下,依然只初始化一次变量
+(instancetype)sharedManager {
static Manager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[Manager alloc]init];
});
return sharedManager;
}
在Swift
中,let
关键词已经保证了实例变量不会被修改,所以单例的创建就简单很多:
class Manager {
static let sharedManager = Manager()
private init() {}
}
24、解释Swift中的懒加载?
懒加载主要是为了延迟加载,保证数据在用到的时候才会被加载,这样可以减少内存消耗
如下:使用懒加载创建UITableView
,懒加载说白了就是使用闭包创建对象,当使用实例对象的时候会执行闭包
lazy var tableView: UITableView = {
let tableView = UITableView (frame: self.view.bounds,
style: UITableView.Style.plain)
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
与OC中懒加载的区别:
-
Swift
中懒加载只会在第一次调动的时候执行闭包, 然后将闭包的结果存在属性中,如果以后将属性置空,属性不在会有值,懒加载也不再执行 -
OC
中第一次使用点语法调用属性的时候,执行懒加载,置空以后再使用点语法调用,会再次执行懒加载
25、什么是OOP,它在iOS开发中有哪些优缺点?
OOP(Object Oritented Programming)
,即面向对象编程,是目前最主流的编程方式。在iOS中绝大多数运用的都是OOP
在iOS
开发中,OOP
有如下优点:
封装和权限控制
在Swift
中,相关属性和方法被放入一个类中,OC
中.h
文件负责声明公共变量和方法,.m
文件负责声明私有变量,并实现所有方法。在Swift
中也有public/internal/fileprivate/private/open
等权限控制
命名空间
在Swift
中,不同的class
即使命名相同,在不同的bundle
中,由于命名空间不同,它们依然可以和谐共存,毫无冲突。当App
体积很大、bundle
很多的时候,这一点非常有用。而OC
没有命名空间,所以很多类在命名时都加入类“驼峰式”的前缀
扩展性
在Swift
中,class
可以通过extension
来增加新方法,通过动态特性可以增加新变量。这样可以保证在不破坏原来代码的封装的情况下实现新的功能。而在OC
中,可以用category
来实现类似功能。另外,在Swift
和OC
中,还可以通过protocol
和代理模式来实现更加灵活的扩展
继承和多态
同其他语言一样,在iOS
开发中,可以将共同的方法和变量定义在父类中,在子类继承时再各自实现对应的功能,高效率实现代码复用。同时,针对不同情况,可以调用不同子类,从而大大增加代码的灵活性。
缺点
隐式共享
class
是引用类型,当在代码中的某处改变某个实例变量时,另外一处调用此变量时就会受此修改的影响。例如:
class Person {
var name: String = ""
}
let person1 = Person()
person1.name = "person1"
let peron2 = person1
peron2.name = "person2"
print(person1.name, peron2.name) // person2 person2
这很容易造成异常。尤其是在多线程时,我们经常遇到的资源竞争就属于这种情况。解决的方案是在多线程时加锁,当前,这个方案会引入死锁和代码复杂度剧增的问题。解决这个问题的最好方案是尽可能地用struct
这样的值类型替代class
冗杂的父类
想象一个场景:一个UIViewController
的子类和一个UITableViewController
中都需要加入handleSomething()
方法。OOP
的解决方案是直接在extension
中加入handleSomething()
方法。但是随着新方法越来越多,倒置UIViewController
会越来越冗杂。当然也可以引入一个专门的父类或工具类,但是依然有职责不明确、依赖、冗杂等多种问题
另外一个方面,父类中的handleSomething()
方法必须有具体的实现,因为它不能根据子类作出灵活调整。子类如果要特定操作,则必须重写方法来实现,那么父类中的实现又显得多此一举了。解决的方案是使用protocol
,这样它的方法就不需要具体的实现了,交给遵守它的类或结构体即可
多继承
Swift
和OC
是不支持多继承的,因为这会造成“菱形”问题,即多个父类实现了同一个方法,子类无法判断继承那个父类的情况。Swift
中又类似protocol
的解决方案
26、Swift为什么要推出POP?
POP
(Protocol Oriented Programming)面向协议编程
1、OOP
有自身的缺点。在继承、代码复用等方面,其灵活度不高。而POP
恰好解决来这些问题
2、POP
可以保证Swift作为静态语言的安全性,而OC
时代的OOP
,其动态性经常会倒置异常
3、OOP
无法应用于值类型,而POP
却可以将其优势扩展到结构体、枚举类型中
27、POP相比OOP有哪些优势
优势:
更加灵活
比如:可以使用协议来定义公共的方法,让所有的服从类都可以有默认的实现,也可以自己实现
protocol SomethingProtocol {
func handleSomething()
}
extension SomethingProtocol {
func handleSomething() {
print("handleSomething")
}
}
class TableViewController: UITableViewController, SomethingProtocol {}
class ViewController: UIViewController, SomethingProtocol {}
减少依赖
相对于传入具体的实例变量,我们可以传入protocol
来实现多态。同时,在测试时也可以利用protocol
来模拟真实的实例,减少对对象极其实现的依赖。
protocol Request {
func send(request: Info)
}
protocol Info {}
class UserInfo: Info {}
class UserRequest: Request {
// 这里传入Info是protocol,不需要是具体的UserInfo,这方便了我们之后的测试
func send(request: Info) {
// 实际实现,一般是吧info发送给server
}
}
如果需要测试,实现protocol即可
class MockUserRequest: Request {
func send(request: Info) {
// 这里进行测试,方便实现
}
}
func testUserRequest() {
let userRequest = MockUserRequest()
userRequest.send(request: UserInfo())
}
消除动态分发的风险
对遵守protocol
的类或结构体而言,它必须实现protocol
声明的所有方法。否则编译器报错,这杜绝了运行时程序锁具有的风险。
协议可用于值类型
相比OOP
只能用于class
,POP
可以用于struct
和enum
这样的值类型。
28、defer关键字的使用
工作原理:延迟执行,等当前范围内的陈述语句执行完成最后执行
defer
1、单个defer语句的执行
func updateImage() {
defer { print("Did update image") }
print("Will update image")
imageView.image = updatedImage
}
// Will update Image
// Did update image
2、多个defer语句的执行顺序
在相同的范围内有多个defer
语句,执行的顺序跟显示的顺序相反,即从最后的defer
语句开始执行
func printStringNumbers() {
defer { print("1") }
defer { print("2") }
defer { print("3") }
print("4")
}
/// Prints 4, 3, 2, 1
3、defer 和闭包
另一个比较有意思的事实是,虽然defer
后面跟了一个闭包,但是它更多地像是一个语法糖,和我们所熟知的闭包特性不一样,并不会持有里面的值。比如:
func foo() {
var number = 1
defer { print("Statement 2: \(number)") }
number = 100
print("Statement 1: \(number)")
}
将会输出:
Statement 1: 100
Statement 2: 100
在defer
中如果要依赖某个变量值时,需要自行进行复制:
func foo() {
var number = 1
var closureNumber = number
defer { print("Statement 2: \(closureNumber)") }
number = 100
print("Statement 1: \(number)")
}
// Statement 1: 100
// Statement 2: 1
使用场景
- 关闭文件句柄(FileHandle)
func writeFile() {
let file: FileHandle? = FileHandle(forReadingAtPath: filepath)
defer { file?.closeFile() }
// Write changes to the file
}
- 确保结果
确保回调闭包能够被执行
func getData(completion: (_ result: Result<String>) -> Void) {
var result: Result<String>?
defer {
guard let result = result else {
fatalError("We should always end with a result")
}
completion(result)
}
// Generate the result..
}
Defer usage in Swift
Swift defer 的正确使用
29、Static和Class的区别
在Swift
中static
和class
都表示“类型范围作用域”的关键字。在所有类型中(class、static、enum
)中,我们可以使用static
来描述类型作用域。class
是专门用于修饰class
类型的。
-
static
可以修饰属性和方法
class Person {
// 存储属性
static let age: Int = 20
// 计算属性
static var workTime: Int {
return 8
}
// 类方法
static func sleep() {
print("sleep")
}
}
但是所修饰的属性和方法不能够被重写,也就是说
static
修饰的类方法和属性包含了final
关键字的特性。
重写会报错:
20180301190454894.png
-
class
修饰方法和计算属性
20180301184734222.png我们同样可以使用
class
修饰方法和计算属性,但是不能够修饰存储属性。
对于 class
修饰的类方法和计算属性是可以被重写的,可以使用class
关键字也可以是static
关键字
class Person {
class var workTime: Int {
return 8
}
class func sleep() {
print("sleep")
}
}
class Student: Person {
// 使用class - ok
// override class func sleep() {
// }
// override class var workTime: Int {
// return 10
// }
// 使用static
override static func sleep() {
}
override static var workTime: Int {
return 10
}
}
-
static
和protocol
Swift
中的class,struct,enum
都可以实现某个指定的协议。如果我们想在protocol
中定义一个类型作用域上的方法或者计算属性,应该使用哪个关键字?答案显而易见,肯定是static
,因为static
是通用的。
注意:在使用
protocol
的时候,在enum
和struct
中仍然使用static
进行修饰,在class
中,class
和static
都可以使用。
protocol MyProtocol {
static func foo() -> String
static func bar() -> String
}
struct MyStruct: MyProtocol {
static func foo() -> String {
return "MyStruct.foo()"
}
static func bar() -> String {
return "MyStruct.bar()"
}
}
enum MyEnum: MyProtocol {
static func foo() -> String {
return "MyEnum.foo()"
}
static func bar() -> String {
return "MyEnum.bar()"
}
}
class MyClass: MyProtocol {
// 在 class 中可以使用 class
class func foo() -> String {
return "MyClass.foo()"
}
// 也可以使用 static
static func bar() -> String {
return "MyClass.bar()"
}
}
总结
1、用class
指定的类方法可以被子类重写,而static
指定的类方法是不能被子类重写的,因为static
修饰的类方法和属性包含了final
关键字的特性。
2、class
不能修饰存储属性,而static
可以
3、对protocol
而言,推荐使用static
关键字修饰类方法和属性
30、讲讲deinit函数
当一个类的实例被释放之前,析构器会被立即调用。析构器用关键字deinit
来标示,类似于构造器要用init
来标示。析构器只适用于类类型。在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数,如下所示:
deinit {
// perform the deinitialization
}
析构器是在实例释放发生前被自动调用。你不能主动调用析构器。子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用。即使子类没有提供自己的析构器,父类的析构器也同样会被调用。
因为直到实例的析构器被调用后,实例才会被释放,所以析构器可以访问实例的所有属性,并且可以根据那些属性可以修改它的行为。开发中,经常在deinit
函数中进行一些资源释放,如:去除通知、观察者等。
31、Swift编程风格指南
objc
出版的《Swift进阶》中,看到编程风格习惯的内容,觉得很不错。所以这里记录下来,希望我们在自己的项目中使用Swift
代码时,也应该尽量遵循如下的原则:
-
1、对于命名,在使用时能清晰表意是最重要。因为
API
被使用的次数要远远多于被声明的次数,所以我们应当从使用者的⻆度来考虑它们的名字。尽快熟悉Swift API
设计准则,并且在你自己的代码中坚持使用这些准则。 -
2、简洁经常有助于代码清晰,但是简洁本身不应该独自成为我们编码的目标。
-
3、务必为函数添加文档注释—特别是泛型函数。
-
4、类型使用大写字母开头,函数、变量和枚举成员使用小写字母开头,两者都使用驼峰式命名法。
-
5、使用类型推断,省略掉显而易⻅的类型会有助于提高可读性。
-
6、如果存在歧义或者在进行定义的时候不要使用类型推断。(比如func就需要显式地指定
返回类型) -
7、优先选择结构体,只在确实需要使用到类特有的特性或者是引用语义时才使用类。
-
8、除非你的设计就是希望某个类被继承使用,否则都应该将它们标记为
final
。 -
9、除非一个闭包后面立即跟随有左括号,否则都应该使用尾随闭包(trailingclosure)的语 法。
-
10、使用
guard
来提早退出方法。 -
11、避免对可选值进行强制解包和隐式强制解包。它们偶尔有用,但是经常需要使用它们的话往往意味着有其他不妥的地方。
-
12、不要写重复的代码。如果你发现你写了好几次类似的代码片段的话,试着将它们提取到 一个函数里,并且考虑将这个函数转化为协议扩展的可能性。
-
13、试着去使用
map和reduce
,但这不是强制的。当合适的时候,使用for
循环也无可厚 非。高阶函数的意义是让代码可读性更高。但是如果使用reduce
的场景难以理解的话, 强行使用往往事与愿违,这种时候简单的for
循环可能会更清晰。 -
14、试着去使用不可变值:除非你需要改变某个值,否则都应该使用
let
来声明变量。不过 如果能让代码更加清晰高效的话,也可以选择使用可变的版本。用函数将可变的部分封 装起来,可以把它带来的副作用进行隔离。 -
15、
Swift
的泛型可能会导致非常⻓的函数签名。坏消息是我们现在除了将函数声明强制写成几行以外,对此并没有什么好办法。我们会在示例代码中在这点上保持一贯性,这样 你能看到我们是如何处理这个问题的。 -
16、除非你确实需要,否则不要使用
self.
。在闭包表达式中,使用self
是一个清晰的信号, 表明闭包将会捕获self
。 -
17、尽可能地对现有的类型和协议进行扩展,而不是写一些全局函数。这有助于提高可读性, 让别人更容易发现你的代码。
参考
《iOS面试之道》
网友评论