Swift —— 属性

  • 1. 存储属性
    • 1.1 let 和 var 的区别
      • 代码角度
      • 汇编角度
      • SIL角度
  • 2. 计算属性
  • 3. 属性观察者
  • 4. 延迟存储属性
  • 5. 类型属性
  • 6. 属性在Mahco文件的位置信息

1. 存储属性

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性 (由 var 关键字引入)要么是常量存储属性(由 let 关键字引入)。存储属性这里没有什么特 别要强调的,因为随处可⻅。 比如这里的 age 和 name 就是我们所说的存储属性,这里我们需要加以区分的是 let 和 var 两者的区别:从定义上: let 用来声明常量,常量的值一旦设置好便不能再被更改; var 用来声明变量,变量的值可以在将来设置为不同的值。

1.1 let 和 var 的区别

代码角度

class LGTeacher{var age: Intvar name: String
}

这里我们来看几个案例:

class LGTeacher{let age: Intvar name: Stringinit( age: Int,  name: String){self.age = ageself.name = name }
}
struct LGStudent{let age: Intvar name: String}
let t = LGTeacher(age: 18, name: "Hello")
t.age = 20
t.name = "Logic"
t = LGTeacher(age: 30, name: "Kody")
var t1 = LGTeacher(age: 18, name: "Hello")
t.age = 20
t.name = "Logic"
t = LGTeacher(age: 30, name: "Kody")
let s = LGStudent(age: 30, name: "Kody")
s.age = 25
s.name = "Doman"
s = LGStudent(age: 30, name: "Kody")
var s1 = LGStudent(age: 30, name: "Kody")
s.age = 25
s.name = "Doman"
s = LGStudent(age: 30, name: "Kody")

从这里可以看到,对于类t来说,t.age是不能被修改的,t.name是可以被修改的,t是不能被修改的。所以这里可以知道,let修饰的变量,只确保当前变量的值是不能够被修改的。这里t存的是实例对象的内存地址,这个地址是不能在变得,所以当在给t重新赋值一个实例对象的时候, 相当于修改了t的值,所以这里是不允许的。age是用let修饰的,所以这里也是不能修改的。

而对于结构体s来说,对于值类型来说,当前的age和name都是不能被修改的,因为s里面存储的就是age和name的值,修改age和name就相当于修改s,由于s是用let声明,所以都不能修改。

汇编角度

从汇编角度来看var 和 let。

  var age = 18let x = 20

从这里可以知道var 和 let在汇编角度是没有什么区别的。

SIL角度

再到SIL里面查看var和let 的区别。
这里可以看到两者都是存储属性,都有初始值。但是这里不同的是,age是有setter 的,而x是没有setter的。本质上,对于存储属性来说,当前的编译器都会给存储属性默认合成setter和getter,访问使用getter,赋值使用setter。而对于let来说,是不生成setter的,而无法对x进行赋值也是因为无法找到其的setter。所以从SIL角度来说,var 和 let 也是一种语法糖。

2. 计算属性

存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不 存储值,他们提供 getter 和 setter 来修改和获取值,计算属性的本质是getter和setter。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时候必须包含类型,因为编译器需要知道期望返回值是什么。

struct square{var width: Doublevar area: Double{get {return width * hegith}set {self.width = newValue}   }
}

在SIL中查看,发现setter中的 newValue是编译器自动生成的。

这里也可以修改newValue的名字。这里注意setter里面不能给self.area赋值,否则会造成死循环。

 set(newArea){self.width = newArea}

接下来看一下只读的计算属性和let属性的区别。

struct square{var width: Double = 30var area: Double{get {return width * width}}let height: Double = 20
}

这里明显看到area不是存储属性,area和height都没有setter。但是,area和height本质是不一样的,因为一个要存储值,一个本质是方法。

那么下列的代码是什么意思呢?这个代码说明将set方法私有了,只能在结构体的声明当中访问到。对于外部来说,是无法访问area的setter方法的。

    private(set) var area:Double


在SIL中查看,这个时候area是存储属性了,只不过setter对结构体外部是访问不到的,所以对外部来说是一个只读属性。

3. 属性观察者

属性观察者会观察用来观察属性值的变化,一个 willSet 当属性将被改变调用,即使这个值与 原有的值相同,而 didSet 在属性已经改变之后调用。它们的语法类似于 getter 和 setter。

class SubjectName{//存储属性var subjectName: String = "" {willSet{print("subjectName will set value \(newValue)")}didSet{print("subjectName has been changed \(oldValue)") }}
}
let s = SubjectName()
s.subjectName = "swift"

这个时候运行就会调用willSet和didSet,newValue是要设的值也就是swift,而oldValue是原来的值就是空字符串

再来看到SIL文件,这里可以看到,是在setter里面调用的willSet和didSet方法,willSet和didSet中间是赋值的过程。也就是说,在赋值之前,setter方法会先调用willSet方法,然后进行赋值操作,当赋值完成后,在调用didset方法。

需要注意的是,在初始化期间设置属性时不会调用 willSet 和 didSet 观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这段代码,你会发现当前并不会有任何的输出。

class SubjectName{//存储属性var subjectName: String = "" {willSet{print("subjectName will set value \(newValue)")}didSet{print("subjectName has been changed \(oldValue)")}}init(_ subjectName: String) {self.subjectName = subjectName}
}let s = SubjectName("swift")

从SIL角度来看,init里面的赋值是没有调用setter方法的,这里的赋值是直接赋值给属性的内存地址的,因为在初始化的时候属性可能还没有初始化完成,就会造成内存泄露。

上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需 将相关代码添加到属性的 setter。我们先来看这段代码

class Square{var width: Doublevar area: Double{get{return width * width}set{self.width = sqrt(newValue)}willSet{print("area will set value \(newValue)")}didSet{print("area has been changed \(oldValue)")}}init(width: Double) {self.width = width}
}

这里可以看到willSet和didSet是不能和getter放在一起的,因为这里不需要willSet和didSet,如果需要观察属性,可以直接在set里面添加观察代码。

那么在继承的时候,属性观察者的表现是什么样的呢?输入下面代码

class LGPerson {var age: Int {willSet{print("age will set value \(newValue)")}didSet{print("age has been changed \(oldValue)")}}var name: Stringinit (age:Int,name:String){self.age = ageself.name = name}}class LGTeacher:LGPerson {override var age: Int {willSet{print("child age will set value \(newValue)")}didSet{print("child age has been changed \(oldValue)")}}var subject:Stringinit (subject:String) {self.subject = subjectsuper.init(age: 18, name: "hihi")}
}

运行后发现,这里是先调用子类的willSet,然后调用父类的willSet和didSet,最后调用子类的didSet。

从SIL来看,这里是先调用子类的willSet,然后调用父类的setter方法,调用父类的setter方法就会调用父类的willSet和didSet,最后调用子类的didSet。

4. 延迟存储属性

延迟存储属性,也可以理解为懒加载,必须要有一个初始值,初始值在其第一次使用时才进行计算,在Swift中用关键字 lazy 来标识一个延迟存储属性。

class Subject {lazy var name:String = ""
}

那么如何理解初始值在其第一次使用时才进行计算呢?创建一个s对象,然后打印s.name

var s = Subject()
print(s.name)
print("end")

在print(s.name)打下断点后运行。这里可以知道前面十六个字节分别是metadata和refCount,然后0x10389ab60是没有东西的,

而当运行到print(“end”)之后重新打印,发现这里有值了。所以这里可以知道第一次使用的时候才进行初始化。


那么添加lazy是否影响实例对象大小呢?打开SIL文件查看,发现这里name变成了可选值,并且是一个final。

从这里可以看到,name的默认值为空是因为这里给了一个枚举值Optional.none。

当去访问name的时候,就需要调用name的getter方法,而这前面的class_method表面了是VTable的调用。

这里看到name的getter方法进行枚举的模式匹配,如果有值则调用bb1代码块,没值调用bb2代码块。第一次访问时没有值的,所以此时调用的是bb2代码块。bb2把String的值构建出来,然后把值给到枚举变量,在把枚举变量的值赋值给name属性的地址。 而下次进来的话就有值了,就会调用bb1代码段,bb1代码段就会直接返回属性的值。

延迟存储属性不能保证变量只被访问一次。当有两个线程访问这个属性的时候,那么两个线程访问的时候,有可能都是没有值的,那么都会调用到bb2代码块,那么就会做两次赋值操作。所以 延迟存储属性不是线程安全的。

延迟存储属性还可以使用闭包表达式赋值,这和原本的方式没有什么区别,只是需要调用闭包表达式把值给到name属性,需要用到函数调用。

class Subject {lazy var name:String {return "ssss"}()
}

5. 类型属性

类型属性其实就是一个全局变量,类型属性只会被初始化一次。

class LGTeacher {static var age: Int = 18
}LGTeacher.age  = 30

从SIL角度来看,其还是存储属性,前面多了个static。

看到这里多了两个东西,一个token以及变成了全局变量的age。所以这里默认在全局声明了age变量,所以static本质上是全局变量。

那么是如何访问age变量的呢?看到这里是访问内存地址。

接下来看到unsafeMutableAddressor方法,这里先拿到了token,拿到了内存地址。接下来做了一个指针类型的转换然后调用了global_init函数,然后返回全局变量的内存地址。

看到global_init函数,这里先是创建一个age的全部变量,然后拿到其地址,构建int结构体,然后把结构体值存入到全局变量age里面,也就是在初始化age变量。


而builtin "once"实际上调用了swift_once,而swift_once在源码中测试调用了dispatch_once_f,使用了GCD的单例写法确保全局变量只被初始化一次。

那么,我们就可以使用类型属性来创建单例实例对象,并且这里私有化初始化器使外界只能通过单例实例对象来访问LGTeacher。

class LGTeacher {static let sharedInstance = LGTeacher()static var age: Int = 18private init(){}
}

6. 属性在Mahco文件的位置信息

之前探索的 Metadata 的元数据结构

struct Metadata {var kind: Intvar superClass: Any.Typevar cacheData: (Int, Int)var data: Intvar classFlags: Int32var instanceAddressPoint: UInt32var instanceSize: UInt32var instanceAlignmentMask: UInt16var reserved: UInt16var classSize: UInt32var classAddressPoint: UInt32var typeDescriptor: UnsafeMutableRawPointervar iVarDestroyer: UnsafeRawPointer
}

之前方法调度的过程中我们认识了typeDescriptor,这里面记录了V-Table的相关信息,接下来我们需要认识一下typeDescriptor中的fieldDescriptor。

struct TargetClassDescriptor{var flags: UInt32var parent: UInt32var name: Int32var accessFunctionPointer: Int32var fieldDescriptor: Int32var superClassType: Int32var metadataNegativeSizeInWords: UInt32var metadataPositiveSizeInWords: UInt32var numImmediateMembers: UInt32var numFields: UInt32var fieldOffsetVectorOffset: UInt32var Offset: UInt32var size: UInt32//V-Table
}

fieldDescriptor记录了当前的属性信息,其中fieldDescriptor在源码中的结构如下:

struct FieldDescriptor {MangledTypeName int32Superclass               int32Kind                         uint16FieldRecordSize.     uint16NumFields               uint32FieldRecords           [FieldRecord]
}


其中NumFields 代表当前有多少个属性,FieldRecords:代表记录的每个属性的信息,FieldRecords的结构体如下:

  • Flags:标识位
  • MangledTypeName:当前属性类型信息
  • FieldName: 属性名称
struct FieldRecord {Flags                        uint32MangledTypeName  int32FieldName                int32
}

根据macho的swift5_types算出类的位置位0XFFFFFF2C + 0X3F4C = 0x100003E78

接着在const里面找到类的位置,然后根据fieldDescriptor在TargetClassDescriptor中的位置知道需要位移4个4字节,得到 9C 00 00 00,这个是FieldDescriptor的offset在machO文件中的偏移信息这里不直接存储地址。那么使用0x100003E88 + 9C 得到0x100003F24。

到swift5_fieldmd里面找到0x100003F24,那么根据FieldDescriptor结构知道要偏移4个4字节找到FieldRecords。那么3F34开始就是FieldRecords这个数组。根据FieldRecord结构就可以知道,02 00 00 00 是Flags, E0 FF FF FF 是MangledTypeName, DF FF FF FF 是FieldName,以此类推。这里的FieldName存储也是偏移信息。

然后使用 0X3F34 + 0X8 + 0X FFFFFFDF = 0x100003F1B,然后到reflstr里面找到FieldName,分别为age 和 age1.

Swift —— 属性相关推荐

  1. Swift 属性与方法

    Swift5 属性与方法 1. Swift 存储属性 // Swift5 存储属性 class Phone {var system = "iOS"// 常量存储属性,一旦实例化,不 ...

  2. 【转载】Swift属性Property

    本文系转载 原文链接 Swift的属性与Objective-C中的属性是一样的,不同的是Swift细化了属性的类型,另外除了类之外,结构体和枚举也可以有属性. Swift中有这么几种属性: 存储属性( ...

  3. swift属性观察者机智

    为了让程序能在属性被赋值时获得执行代码的机会.swift提供了属性观察者机智,属性观察者其实就两个特殊的回调方法 willSet:被观察的属性即将被赋值之前自动调用该方法 didSet:被观察的属性被 ...

  4. Swift - 属性观察者(willSet与didSet)

    属性观察者,类似于触发器.用来监视属性的除初始化之外的属性值变化,当属性值发生改变时可以对此作出响应.有如下特点: 1,不仅可以在属性值改变后触发didSet,也可以在属性值改变前触发willSet. ...

  5. swift_020(Swift 的属性)

    //***********swift学习之20--属性--*************************** // 属性比较属性,不需要介绍太多,只知道还可以定义属性观察器来监控属性值的变化,以此 ...

  6. swift面向对象之属性

    swift面向对象之属性 swift属性 存储属性 可以存储常量和变量 惰性存储属性 语法:lazy var 变量 惰性存储属性只有在用的时候才会完成真正的初始化,刚开始的初始化代码只是类型占位,并没 ...

  7. Swift中文教程(十) 属性

    属性是描述特定类.结构或者枚举的值.存储属性作为实例的一部分存储常量与变量的值,而计算属性计算他们的值(不只是存储).计算属性存在于类.结构与枚举中.存储属性仅仅只在类与结构中. 属性通常与特定类型实 ...

  8. Swift和Javascript的神奇魔法

    Swift和Javascript的神奇魔法 记录Swift和Javascript如何进行交互 前言 今天在网上看到了一篇介绍Swift和Javascript交互的文章,感觉作者写的很好,因此把作者文章 ...

  9. Realm Swift

    ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★ ➤微信公众号:山青咏芝(shanqingyongzhi) ➤博客园地址:山青咏芝(https://www.cnblog ...

最新文章

  1. 某县百姓百事110便民服务管理系统(1)——项目总结
  2. linux点阵ascii像素字体,点阵字体显示系列之一:ASCII码字库的显示 | 迟思堂工作室...
  3. 一起学习 网络规划设计师
  4. 有些窗口底部被任务栏挡住了_开始使用 Tint2 吧,一款 Linux 中的开源任务栏
  5. matplotlib.pyplot.figure
  6. 最短路径算法详细介绍
  7. java不能弹出打印窗口,java – 如何打印一个摆动窗口,使其非常适合一页
  8. python项目描述怎么写_个人项目(python)
  9. OpenCV图像直方图案例
  10. 最新四级联动数据json
  11. vue实现div高度可拖拽
  12. pythonlambda多行_Python中通过lambda抛异常的奇迹淫巧
  13. 大数据,先推广应用再谈“共享”
  14. 【论文复刻】高技术企业认证政策是否促进了中国创新?(heckman两阶段模型 PSM-DID)论文复现
  15. 在线JSON格式化美化
  16. 闲鱼一直不确认收货怎么办?
  17. informatica session中bulk和normal模式
  18. Java 计算日期差
  19. MacOS M1 安装riscv toolchain
  20. Splinter入门(十一) Screenshot 截图

热门文章

  1. 重装系统后QQ聊天记录恢复方法
  2. java 静态分析_静态代码分析与代码质量安全
  3. Visual C# 的DirectX开发系列一初识DirectX
  4. 橙瓜大数据发掘好书,25个网站小说排行榜,百万人打分
  5. CSS学习之position属性
  6. perl mysql 数据推拉_用perl 从mysql取出数据做统计分析代码
  7. JS中的$().each
  8. 教你如何更改windows10系统默认字体
  9. linux系统中怎样抓logo,linux启动成功修改logo
  10. C:\windows\system32文件