Swift面试题

class 和 struct 的区别

1.struct是值类型,class是引用类型。

值类型的变量直接包含它们的数据,对于值类型都有它们自己的数据副本,因此对一个变量操作不可能影响另一个变量。

引用类型的变量存储对他们的数据引用,因此后者称为对象,因此对一个变量操作可能影响另一个变量所引用的对象。

2.二者的本质区别:

struct是深拷贝,拷贝的是内容;class是浅拷贝,拷贝的是指针。

3.property的初始化不同:

class 在初始化时不能直接把 property 放在 默认的constructor 的参数里,而是需要自己创建一个带参数的constructor;而struct可以,把属性放在默认的constructor 的参数里。

4.变量赋值方式不同:

struct是值拷贝;class是引用拷贝。

5.immutable变量:

swift的可变内容和不可变内容用var和let来甄别,如果初始为let的变量再去修改会发生编译错误。struct遵循这一特性;class不存在这样的问题。

6.mutating function: 

struct 和 class 的差別是 struct 的 function 要去改变 property 的值的时候要加上 mutating,而 class 不用。

7.继承: 

struct不可以继承,class可以继承。

8.struct比class更轻量:

struct分配在栈中,class分配在堆中。

open与public的区别

public:可以别任何人访问,但是不可以被其他module复写和继承。

open:可以被任何人访问,可以被继承和复写。

swift中struct作为数据模型

优点:

1.安全性:因为 Struct 是用值类型传递的,它们没有引用计数。

2.内存:由于他们没有引用数,他们不会因为循环引用导致内存泄漏。

    速度:值类型通常来说是以栈的形式分配的,而不是用堆。因此他们比 Class 要快很多!

3.拷贝:Objective-C 里拷贝一个对象,你必须选用正确的拷贝类型(深拷贝、浅拷贝),而值类型的拷贝则非常轻松!

4.线程安全:值类型是自动线程安全的。无论你从哪个线程去访问你的 Struct ,都非常简单。

缺点:

1.Objective-C与swift混合开发:OC调用的swift代码必须继承于NSObject。

2.继承:struct不能相互继承。

3.NSUserDefaults:Struct 不能被序列化成 NSData 对象

Swift代码复用的方式有哪些?

1.继承

2.在swift 文件里直接写方法,相当于一个全局函数

3.extension 给类直接扩展方法

Array、Set、Dictionary 三者的区别

Set:是用来存储相同类型、没有确定顺序、且不重复的值的集

Array:是有序数据的集

Dictionary:是无序的键值对的集

map、filter、reduce 的作用

map : 映射 ,将一个元素根据某个函数 映射 成另一个元素(可以是同类型,也可以是不同类型)

filter : 过滤 , 将一个元素传入闭包中,如果返回的是false , 就过滤掉

reduce :先映射后融合 , 将数组中的所有元素映射融合在一起。

map 与 flatmap 的区别

map:作用于数组中的每个元素,闭包返回一个变换后的元素,接着将所有这些变换后的元素组成一个新的数组。

flatMap:功能和map类似,区别在于flatMap可以去nil,能够将可选类型(optional)转换为非可选类型(non-optionals),把数组中存有数组的数组 一同打开变成一个新的数组(数组降维)

copy on write

写时复制:每当数组被改变,它首先检查它对存储缓冲区 的引用是否是唯一的,或者说,检查数组本身是不是这块缓冲区的唯一拥有者。如果是,那么 缓冲区可以进行原地变更;也不会有复制被进行。不过,如果缓冲区有一个以上的持有者 (如本 例中),那么数组就需要先进行复制,然后对复制的值进行变化,而保持其他的持有者不受影响。

在Swift提供一个函数isKnownUniquelyReferenced,能检查一个类的实例是不是唯一的引用,如果是,说明实例没有被共享

eg: isKnownUniquelyReferenced(&object)

获取当前代码的函数名和行号

函数名:#function 

行号:#line 

文件名:#file

guard、defer、where

defer:defer所声明的 block 会在当前代码执行退出后被调用,如果有多个 defer, 那么后加入的先执行

guard:可以理解为拦截,凡是不满足 guard 后面条件的,都不会再执行下面的代码。

where:在Swift语法里where关键字的作用跟SQL的where一样, 即附加条件判断。where关键字可以用在集合遍历、switch/case、协议中; Swift3时if let和guard场景的where已经被Swift4的逗号取代

String 与 NSString 的关系与区别

String为Swift的Struct结构,值类型;NSString为OC对象,引用类型,能够互相转换

throws 和 rethrows 的用法与作用

throws 用在函数上, 表示这个函数会抛出错误.

有两种情况会抛出错误, 一种是直接使用 throw 抛出, 另一种是调用其他抛出异常的函数时, 直接使用 try xx 没有处理异常.

enum DivideError: Error {
    case EqualZeroError;
}
func divide(_ a: Double, _ b: Double) throws -> Double {
    guard b != Double(0) else {
        throw DivideError.EqualZeroError
    }
    return a / b
}
func split(pieces: Int) throws -> Double {
    return try divide(1, Double(pieces))
}

rethrows 与 throws 类似, 不过只适用于参数中有函数, 且函数会抛出异常的情况, rethrows 可以用 throws 替换, 反过来不行

func processNumber(a: Double, b: Double, function: (Double, Double) throws -> Double) rethrows -> Double {
    return try function(a, b)
}

try?和 try!

这两个都用于处理可抛出异常的函数, 使用这两个关键字可以不用写 do catch.

区别在于, try? 在用于处理可抛出异常函数时, 如果函数抛出异常, 则返回 nil, 否则返回函数返回值的可选值

而 try! 则在函数抛出异常的时候崩溃, 否则则返会函数返回值, 相当于(try? xxx)!

associatedtype 的作用

简单来说就是 protocol 使用的泛型

Swift 下的 KVO , KVC

KVO, KVC 都是Objective-C 运行时的特性, Swift 是不具有的, 想要使用, 必须要继承 NSObject, 自然, 继承都没有的结构体也是没有 KVO, KVC 的.

KVC:Swift 下的 KVC 用起来很简单, 只要继承 NSObject 就行了

KVO:KVO 就稍微麻烦一些了,由于 Swift 为了效率, 默认禁用了动态派发, 因此想用 Swift 来实现 KVO, 除了继承NSObject,还需要将想要观测的对象标记为 dynamic

@objc

OC 是基于运行时,遵循了 KVC 和动态派发,而 Swift 是静态语言,为了追求性能,在编译时就已经确定,而不需要在运行时的,在 Swift 类型文件中,为了解决这个问题,需要暴露给 OC 使用的任何地方(类,属性,方法等)的生命前面加上 @objc 修饰符

如果用 Swift 写的 class 是继承 NSObject 的话, Swift 会默认自动为所有非 private 的类和成员加上@objc

自定义下标获取

实现 subscript 即可, 索引除了数字之外, 其他类型也是可以的

extension AnyList {
    subscript(index: Int) -> T{
        return self.list[index]
    }

    subscript(indexString: String) -> T?{
        guard let index = Int(indexString) else {
            return nil
        }
        return self.list[index]
    }
}

lazy 的作用

懒加载, 当属性要使用的时候, 才去完成初始化

一个类型表示选项,可以同时表示有几个选项选中(类似 UIViewAnimationOptions ),用什么类型表示

需要实现OptionSet, 一般使用 struct 实现. 由于 OptionSet 要求有一个不可失败的init(rawValue:) 构造器, 而 枚举无法做到这一点(枚举的原始值构造器是可失败的, 而且有些组合值, 是没办法用一个枚举值表示的)

struct SomeOption: OptionSet {
    let rawValue: Int
    static let option1 = SomeOption(rawValue: 1 << 0)
    static let option2 =  SomeOption(rawValue:1 << 1)
    static let option3 =  SomeOption(rawValue:1 << 2)
}
let options: SomeOption = [.option1, .option2]

static和class的区别

static 可以在类、结构体、或者枚举中使用。而 class 只能在类中使用。

static 可以修饰存储属性,static 修饰的存储属性称为静态变量(常量)。而 class 不能修饰存储属性。

static 修饰的计算属性不能被重写。而 class 修饰的可以被重写。

static 修饰的静态方法不能被重写。而 class 修饰的类方法可以被重写。

class 修饰的计算属性被重写时,可以使用 static 让其变为静态属性。

class 修饰的类方法被重写时,可以使用 static 让方法变为静态方法

class关键字指定的类方法可以被子类重写,但是用static关键字指定的类方法是不能被子类重写的

mutating

mutating用于函数的最前面,用于告诉编译器这个方法会改变自身.

swift增强了结构体和枚举的使用,结构体和枚举也可以有构造方法和实例方法,但结构体和枚举是值类型,如果我们要在实例方法中修改当前类型的实例属性的值或当前对象实例的值,必须在func前面添加mutating关键字,表示当前方法将会将会修改它相关联的对象实例的实例属性值.

协议中,当在结构体或者枚举实现协议方法时,若对自身属性作修改,需要将协议的方法声明为mutating,对类无影响.

在扩展中,同样若对自身属性作修改,需要将方法声明为mutating

swift多线程

1.Thread

//方式1:使用类方法,不需要手动启动线程
Thread.detachNewThreadSelector(#selector(ViewController.downloadImage), toTarget: self, with: nil)

//方式2:实例方法-便利构造器,需调用start()启动线程 let myThread = Thread(target: self, selector: #selector(thread), object: nil) myThread.start()

线程同步:线程同步方法通过锁来实现,每个线程都只用一个锁,这个锁与一个特定的线程关联。

//定义两个线程
var thread1:Thread?
var thread2:Thread?
//定义两个线程条件,用于锁住线程
let condition1 = NSCondition()
let condition2 = NSCondition()
     
override func viewDidLoad() {
   super.viewDidLoad()
   thread2 = Thread(target: self, selector: #selector(ViewController.method2), object: nil)
   thread1 = Thread(target: self, selector: #selector(ViewController.method1), object: nil)
   thread1?.start()
}
     
//定义两方法,用于两个线程调用
func method1(sender:AnyObject){
   for i in 0 ..< 10 {
      print("Thread 1 running (i)")
      sleep(1)
      if i == 2 {
         thread2?.start() //启动线程2
         //本线程(thread1)锁定
         condition1.lock()
         condition1.wait()
         condition1.unlock()
      }
  }
   print("Thread 1 over")
   //线程2激活
   condition2.signal()
}
//方法2 func method2(sender:AnyObject){ for i in 0 ..< 10 { print("Thread 2 running (i)") sleep(1) if i == 2 { //线程1激活 condition1.signal() //本线程(thread2)锁定 condition2.lock() condition2.wait() condition2.unlock() } } print("Thread 2 over") }

2.Operation和OperationQueue

2.1 直接用定义好的子类:BlockOperation。

let operation = BlockOperation(block: { [weak self] in
    // 执行代码return
})
//创建一个NSOperationQueue实例并添加operation
let queue = OperationQueue()
queue.addOperation(operation)

2.2 继承Operation 

  把Operation子类的对象放入OperationQueue队列中,一旦这个对象被加入到队列,队列就开始处理这个对象,直到这个对象的所有操作完成,然后它被队列释放。
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        //创建线程对象
        let downloadImageOperation = DownloadImageOperation()
        //创建一个OperationQueue实例并添加operation
        let queue = OperationQueue()
        queue.addOperation(downloadImageOperation)
    }
}
class DownloadImageOperation: Operation {
    override func main(){
        let imageUrl = "http://hangge.com/blog/images/logo.png"
        let data = try! Data(contentsOf: URL(string: imageUrl)!)
        print(data.count)
    }
}

2.3 其他方法

//队列设置并发数
queue.maxConcurrentOperationCount = 5
//队列取消所有线程操作
queue.cancelAllOperations()
//给operation设置回调
operation.completionBlock = { () -> Void in
    print("--- operation.completionBlock ---")
}

3.GCD

3.1 创建队列

//------自己创建队列
//创建串行队列
let serial = DispatchQueue(label: "serialQueue1")
//创建并行队列
let concurrent = DispatchQueue(label: "concurrentQueue1", attributes: .concurrent)

//------系统global队列
//Global Dispatch Queue有4个执行优先级:
//.userInitiated  高
//.default  正常
//.utility  低
//.background 非常低的优先级(这个优先级只用于不太关心完成时间的真正的后台任务)
let globalQueue = DispatchQueue.global(qos: .default)

//------系统主线程队列
//因为主线程只有一个,所有这自然是串行队列。一起跟UI有关的操作必须放在主线程中执行。
let mainQueue = DispatchQueue.main

3.2 添加任务到队列

async异步追加Block块(async函数不做任何等待)

DispatchQueue.global(qos: .default).async {
    //处理耗时操作的代码块...
     
    //操作完成,调用主线程来刷新界面
    DispatchQueue.main.async {
        print("main refresh")
    }
}

sync同步追加Block块 

//同步追加Block块,与上面相反。在追加Block结束之前,sync函数会一直等待,等待队列前面的所有任务完成后才能执行追加的任务。
//添加同步代码块到global队列
//不会造成死锁,但会一直等待代码块执行完毕
DispatchQueue.global(qos: .default).sync {
    print("sync1")
}
print("end1")
 
 
//添加同步代码块到main队列
//会引起死锁
//因为在主线程里面添加一个任务,因为是同步,所以要等添加的任务执行完毕后才能继续走下去。但是新添加的任务排在
//队列的末尾,要执行完成必须等前面的任务执行完成,由此又回到了第一步,程序卡死
DispatchQueue.main.sync {
    print("sync2")
}

3.3 暂停或者继续队列

  这两个函数是异步的,而且只在不同的blocks之间生效,对已经正在执行的任务没有影响。suspend()后,追加到Dispatch Queue中尚未执行的任务在此之后停止执行。而resume()则使得这些任务能够继续执行。

//创建并行队列
let conQueue = DispatchQueue(label: "concurrentQueue1", attributes: .concurrent)
//暂停一个队列
conQueue.suspend()
//继续队列
conQueue.resume()

3.4 asyncAfter 延迟调用

  asyncAfter 并不是在指定时间后执行任务处理,而是在指定时间后把任务追加到queue里面。因此会有少许延迟。注意,我们不能(直接)取消我们已经提交到 asyncAfter 里的代码。

//延时2秒执行
DispatchQueue.global(qos: .default).asyncAfter(deadline: DispatchTime.now() + 2.0) {
    print("after!")
}

  如果需要取消正在等待执行的Block操作,我们可以先将这个Block封装到DispatchWorkItem对象中,然后对其发送cancle,来取消一个正在等待执行的block。

//将要执行的操作封装到DispatchWorkItem中
let task = DispatchWorkItem { print("after!") }
//延时2秒执行
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2, execute: task)
//取消任务
task.cancel()

 3.5 DispatchWorkItem

  DispatchWorkItem可以将任务封装成DispatchWorkItem对象。

  可以调用workItem的perform()函数执行任务,也可以将workItem追加到DispatchQueue或DispatchGroup中。以上所有传block的地方都可换成DispatchWorkItem对象。
DispatchQueue还可以使用notify函数观察workItem中的任务执行结束,以及通过cancel()函数取消任务。

  另外,workItem也可以像DispatchGroup一样调用wait()函数等待任务完成。需要注意的是,追加workItem的队列或调用perform()所在的队列不能与调用workItem.wait()的队列是同一个队列,否则会出现线程死锁。

// 初始化方法
init(qos: DispatchQoS, flags: DispatchWorkItemFlags, block: () -> Void)

let workItem = DispatchWorkItem.init {
      print("执行任务")
}
3.6 DispatchQueue.concurrentPerform
  sync函数和Dispatch Group的关联API。会按指定次数异步执行任务,并且会等待指定次数的任务全部执行完成,即会阻塞线程。建议在子线程中使用。
DispatchQueue.global().async {
     DispatchQueue.concurrentPerform(iterations: 5) { (i) in
         print("执行任务(i+1)")
     }
     print("任务执行完成")
}
3.7 DispatchSemaphore

  信号量:用于控制访问资源的数量。比如系统有两个资源可以被利用,同时有三个线程要访问,只能允许两个线程访问,第三个会等待资源被释放后再访问。

  信号量的初始化方法:DispatchSemaphore.init(value: Int),value表示允许访问资源的线程数量,当value为0时对访问资源的线程没有限制。

  信号量配套使用wait()函数与signal()函数控制访问资源。

  wait函数会阻塞当前线程直到信号量计数大于或等于1,当信号量大于或等于1时,将信号量计数-1, 然后执行后面的代码。signal()函数会将信号量计数+1。

  信号量是GCD同步的一种方式。前面介绍过的DispatchWorkItemFlags.barrier是对queue中的任务进行批量同步处理,sync函数是对queue中的任务单个同步处理,而    

  DispatchSemaphore是对queue中的某个任务中的某部分(某段代码)同步处理。此时将DispatchSemaphore.init(value: Int)中的参数value传入1。

var arr = [Int]()
let semaphore = DispatchSemaphore.init(value: 1) // 创建信号量,控制同时访问资源的线程数为1
for i in 0...100 {
    DispatchQueue.global().async {         
        /*
        其他并发操作
        */    
        semaphore.wait() // 如果信号量计数>=1,将信号量计数减1;如果信号量计数<1,阻塞线程直到信号量计数>=1
        arr.append(i)
        semaphore.signal() // 信号量计加1  
        /*
        其他并发操作
        */
     }
}
原文地址:https://www.cnblogs.com/liuluoxing/p/11758053.html