结构体和类(一)

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

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

本篇开始学习第一部分,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<A> {
    var unbox: A
    init(_ value: A) { unbox = value }
}


struct GaussianBlur {

  private var boxedFilter: Box<CIFilter> = {
    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
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容