结构体和类(一)

结构体和类模块分两篇笔记来学习:

  • 第一篇:
  • 结构体和类的区别
  • 分析类和结构体可变性
  • 以一个具体的例子来学习使用类和结构体的区别,以及如何使用写时复制来解决结构体内部引用类型的复制
  • 最后学习函数闭包的可变性
  • 第二篇:
    • 类和结构体的内存分析
    • 何时使用类何时使用结构体

本篇开始学习第一部分,go!

1.结构体(和枚举)和类的区别

  • 结构体 (和枚举) 是值类型,而类是引用类型。在设计结构体时,我们可以要求编译器保证不可变性。而对于类来说,我们就得自己来确保这件事情。

  • 内存的管理方式有所不同。结构体可以被直接持有及访问,但是类的实例只能通过引用来间接地访问。结构体不会被引用,但是会被复制。也就是说,结构体的持有者是唯一的,但是类却能有很多个持有者。

  • 除非一个类被标记为 final,否则它都是可以被继承的。而结构体 (以及枚举) 是不能被继承的。想要在不同的结构体或者枚举之间共享代码,我们需要使用不同的技术,比如像是组合,泛型,以及协议扩展等。

2.实体和值

  • 实际开发中有些类型的对象我们需要控制它们的生命周期,或者比较两个对象是否指向同一个内存地址,比如文件句柄、通知中心、网络接口、数据库连接、ViewContrller等,这些都是引用类型。有些类型我们不需要声明周期,比较的时候也只是需要比较所对应的值是否相等,比如URL,二进制数据,日期,错误,字符串,通知以及数字等,这些类型都是值类型,可以使用结构体实现。

  • 引用语义和值语义:当我们将结构体变量传递给一个函数时,函数将接收到结构体的复制,它也只能改变它自己的这份复制。这叫做值语义 (value semantics),有时候也被叫做复制语义。而对于对象来说,它们是通过传递引用来工作的,因此类对象会拥有很多持有者,这被叫做引用语义

  • 值类型的复制优化和值语义的写时复制:编译器对结构体的复制进行优化,只是按照字节进行浅复制。写时复制由开发者实现,想要实现写时复制,需要检测所包含的类是否有共享的引用。

3.不可控制的可变性 (为何不用类)

  • Objective-C 的很多面向对象编程语言种,数据默认是可变的,这在多线程编程里会造成非常大的麻烦,而解决的方法就是通过不可变性的值类型来保障线程安全。

  • 类型的可变性无论是在oc还是swift中都是无法彻底控制的。

    class File {
      let data: NSMutableData
      init(data: NSMutableData) {
            self.data = data
      }
    }
    

    虽然使用了let来控制不能改变属性所指向的对象,但是可以修改当前所指向对象的值。

4.值类型

引用一个例子做对比引用类型和值类型:

 let inputParameters = [
    kCIInputRadiusKey: 10,
    kCIInputImageKey: image
 ]

let blurFilter = CIFilter(name: "CIGaussianBlur",
withInputParameters: inputParameters)!
let secondBlurFilter = blurFilter
secondBlurFilter.setValue(20, forKey: kCIInputRadiusKey)

CIFilter是CoreData里的一个滤镜类,第二个滤镜的配置变动会影响第一个滤镜的配置。可以通过复制时手动复制来避免影响:

let otherBlurFilter = blurFilter.copy() as! CIFilter
otherBlurFilter.setValue(20, forKey: kCIInputRadiusKey)”

接下来使用结构体来实现:

struct GaussianBlur {
  var inputImage: CIImage
  var radius: Double
}
var blur1 = GaussianBlur(inputImage: image, radius: 10)
blur1.radius = 20
var blur2 = blur1
blur2.radius = 30”

这种复制听上去有点浪费,但是编译器会为我们优化,此外如果如果使用写时复制的话,实际对数据的复制只会在其中某个值实际改变的时候才会发生。其实这也是 Swift 数组的工作方式。接下来使用扩展对GaussianBlur 结构体上添加一个 outputImage 属性:

  extension GaussianBlur {
    var outputImage: CIImage {
        let filter = CIFilter(name: "CIGaussianBlur", withInputParameters: [kCIInputImageKey: inputImage,kCIInputRadiusKey: radius])!
        return filter.outputImage!
    }

上面的代码在每次 outputImage 被访问时都会创建一个新的滤镜。这并不是很高效的做法,如果我们多次访问这个属性,会有好多新的滤镜被创建出来。

为了避免这种性能的浪费,采用另一种高效的做法,不像上面那样将值存在结构体中,这次采用直接存储CIFilter实例,然后通过自定义属性进行修改:

struct GaussianBlur {
  private var filter: CIFilter
  init(inputImage: CIImage, radius: Double) {
    filter = CIFilter(name: "CIGaussianBlur", withInputParameters: [
      kCIInputImageKey: inputImage,
      kCIInputRadiusKey: radius
    ])!
  }
 }

接下来,我们实现 inputImage 和 radius 属性,来直接访问和修改滤镜的属性:

extension GaussianBlur {
  var inputImage: CIImage {
    get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
    set { filter.setValue(newValue, forKey: kCIInputImageKey) }
  }

  var radius: Double {
    get { return filter.valueForKey(kCIInputRadiusKey) as! Double }
    set { filter.setValue(newValue, forKey: kCIInputRadiusKey) }
  }
}

最后,要取出输出图像,我们可以直接使用滤镜上的 outputImage 属性:

extension GaussianBlur {
    var outputImage: CIImage {
      return filter.outputImage!
    }
}

此时可以正常的满足需求了:

var blur = GaussianBlur(inputImage: image, radius: 25)
blur.outputImage

但是如果复制结构体的话就会有个问题:其中的所有值类型将被复制,而引用类型却只有对于对象的引用会被复制,对象本身不被复制。如果对blur进行复制产生otherBlur时,blur和otherBlur中的滤镜将指向同一个CIFilter实例!

5.写时复制

Swift 在实现值语义的结构体时,其底层使用了可变的对象,这也正是 Swift 中最强大的特性之一。我们从值语义里获益良多,同时又可以保持代码高效。然而,正如我们所看到的,这种做法可能导致意外的数据共享。

通过写时复制这种技术可以实现:在每次结构体被改变时,去复制一个封装后的对象,以此来避免共享。其实Swift 的很多数据结构都是以这种方式工作的。

配合上一节的滤镜例子,我们可以通过写时复制来避免滤镜的共享:

extension GaussianBlur {
  private var filterForWriting: CIFilter {
    mutating get {
      filter = filter.copy() as! CIFilter
      return filter
    }
   }
  }

mutating修饰符作用是使方法能够修改结构体的变量。这让我们得以更改滤镜的设置方法,在改变值之前先进行复制:

extension GaussianBlur {
  var inputImage: CIImage {
    get { return filter.valueForKey(kCIInputImageKey) as! CIImage }
    set {
        filterForWriting.setValue(newValue, forKey: kCIInputImageKey)
    }
  }

  var radius: Double {
    get { return filter.valueForKey(kCIInputRadiusKey) as! Double }
    set {
      filterForWriting.setValue(newValue, forKey: kCIInputRadiusKey)
    }
  }
}

上面这种方式可以按照预想工作,但是却带来了性能上的代价:每次我们改变滤镜值的时候,滤镜都会被复制,即便是我们只是做一些本地修改,而没有共享结构体时,这个复制也会发生”

6.高效的写时复制

在 Swift 中有一个方法,isUniquelyReferencedNonObjC,它会检查一个类的实例是不是唯一的引用。我们可以使用它来在保证结构体的值语义的同时,避免不必要的复制。

不幸的是,isUniquelyReferencedNonObjC 函数只对 Swift 对象有用,而 CIFilter 是一个 Objective-C 类。想要绕过这个限制,我们可以创建一个简单的 Box 封装类型,来把任意的 Swift 类封装进去:

final class Box {
    var unbox: A
    init(_ value: A) { unbox = value }
}


struct GaussianBlur {

  private var boxedFilter: Box = {
    var filter = CIFilter(name: "CIGaussianBlur",withInputParameters: [:])!
    filter.setDefaults()
    return Box(filter)
   }()

  var filter: CIFilter {
    get { return boxedFilter.unbox }
    set { boxedFilter = Box(newValue) }
  }
  
  private var filterForWriting: CIFilter {
    mutating get {
      if !isUniquelyReferencedNonObjC(&boxedFilter) {
        filter = filter.copy() as! CIFilter
        }
      return filter
    }
   }  

 }

这正是 Swift 数组内部的工作方式。当你创建一个新的数组的复制时,它背后的数据是一样的。只有当你要修改这个数组时,才会进行复制,这样一来,对该数组的更改不会影响到其他的数组。当在一个结构体中使用类时,我们需要保证它确实是不可变的。如果办不到这一点的话,我们就需要 (像上面那样的) 额外的步骤。或者就干脆使用一个类,这样我们的数据的使用者就不会期望它表现得像一个值。

7.闭包的可变性

闭包和函数也是引用类型的,如果进行复制,两个函数或闭包对象将共享同样的状态:

var i = 0
func uniqueInteger() -> Int {
    i += 1
    return i
}

let otherFunction: () -> Int = uniqueInteger

此时uniqueInteger和otherFunction将共同享用i变量。使用对闭包的再次封装,使得返回享用共同变量的闭包的方式来解决问题:

func uniqueIntegerProvider() -> () -> Int {
    var i = 0
    return {
        i += 1
        return i
    }
}

你可能感兴趣的:(结构体和类(一))