WWDC--Swift中怎样用好Value Type?

Value Type

亲,我的简书已不再维护和更新了,所有文章都迁移到了我的个人博客:https://mikefighting.github.io/,欢迎交流。

为什么要用Value Type?

首先我们要说明为什么我们需要Value Type。因为我们通常使用的Reference Type不能满足我们的需求,并且容易出Bug,什么是Reference Type?
Reference Type就是我们常说的Class,我们不是用的很好吗?我们举几个例子来看看

例一:但是现在又一种需求:进入用于信息页面编辑页面,如果用户的信息改变了,并且没有点击保存,这时如果点击导航栏的退出,要给用户提醒"您编辑了信息,确定不保存退出吗?",这时为了保存进入编辑页面的信息,我们一般这样做

  1. 增加一个originInfo的property。
  2. 在ViewDidLoad的时候赋值:self.originInfo = passedInfo;(这个passedInfo从上个页面传来)
  3. 在点击退出的时候对比passedInfo和self.originInfo是否相等,如果不相等则提醒用户,如果相等则返回。
    这时你会发现一个bug,这两个对象永远都是相同的,为什么?

例二:我们要从商品列表中进入商品编辑页面,这时,我们点击了编辑,将goodModel传到第二个页面,在第二个页面操作完之后,用户没有保存信息,返回了,这时你也可能会发现,你列表中的商品信息改变了。

例三:Cell复用造成的布局混乱。
上述两种情景是Reference Type,也就是说他们的都指向了堆上的相同对象,这种类型对对象的公用造成了一系列的bug。并且这种bug很难被发现,并且往往也不是必现的。
怎样解决呢?
这时我们就需要Copy原来的instance,在OC中我们需要遵守NSCopying,或者NSMutableCopying协议,因为这些,然后实现相应的协议,例如:

@interface HYLocationModel : NSObject<NSMutableCopying>
// cityDic{@"name":@"",@"id":@""}
@property (nonatomic, strong) NSDictionary *cityDic; //City
@property (nonatomic, strong) NSDictionary *areaDic; //
@property (nonatomic, strong) NSDictionary *districtDic;

@end
@implementation HYLocationModel
- (id)mutableCopyWithZone:(NSZone *)zone {
    
    HYLocationModel *locationModel = [[HYLocationModel alloc]init];
    locationModel.cityDic = self.cityDic;
    locationModel.areaDic = self.areaDic;
    locationModel.districtDic = self.districtDic;
    return locationModel;
}
@end

这样我们就可以Copy对象了,将Copy的对象赋值给self.originInfo,就可以解决上述的bug。
但是这会消耗性能,因为需要在堆里开辟内存空间。NSCopying协议在OC中很常见,比如NSString,NSArray,NSDictionary等都遵守NSCopying协议。其中NSDictionary的Key,默认是实现了NSCopying协议的,因为在给NSDictionary赋值的时候,系统默认是Copy了它的Key,因为如果不Copy它的Key,如果你给字典赋值之后改变了这个Key,那么它将会使整个NSDictionary混乱,出现意想不到的Bug。当然这种Copy也会消耗性能。

不可变对象是否可以解决上述问题呢?

在函数式编程中,我们会使用不可变的Reference Type来消除其可变所带来的问题,想象下如果你做数学题题目A中的X值被题目B改变了,那么会有怎样的结果?
在Swift中,我们可以使用let来使其不可变,但是这种不可变的数据结构有以下弊端:

  1. 可能导致很恶心的接口(见下文)。
  2. 不能有效地和机器模型相匹配(因为我们的寄存器,我们的Cache,Memory,Storage都是可变状态的)。

比如下面的代码

// With mutability 
home.oven.temperature.fahrenheit += 10.0

//Without mutability let temp = home.oven.temperaturehome.oven.temperature = Temperature(fahrenheit: temp.fahrenheit + 10.0) 

在上面的例子中,我们把Temperature类的某个属性改成了let,那么如果我们要更改这个数值,我们就需要在堆上开辟内存空间然后创建一个新的Temperature,最后更换掉整个Temperature类。
Cocoa[Touch]中有很多的不可变类比如:NSDate,NSURL,UIImage,NSNumber等
这更加安全了(不需要使用Copy),也不必担心接下来的程序会改变这个数值。

NSArray<NSString *> *array = [NSArray arrayWithObject: NSHomeDirectory()]; NSString *component; while ((component = getNextSubdir()) { 
array = [array arrayByAddingObject: component]; } 
url = [NSURL fileURLWithPathComponents: array];

Value Type将怎样解决这种问题呢?

Swift中的所有基础类型都是Value Type的,像:Int,Double,String ...
Swift中所有的Collection都是Value Type的,像:Array,Set,Dictianry...
Swift中的Tuples,Struct,Enums如果只包含Value Types那么他们自身也是Value Type的。
Value Type要是完全可以直接比较的,可以直接使用==自定义的Value Type并且其必须要遵守Equable协议,覆盖==方法才可以使用的

var a: [Int] = [1, 2, 3]
var b: [Int] = [3, 2, 1].sorted(by:<)
assert(a == b) // true

如果是自身的定义的Struct,那么需要遵守Equable协议,并且覆盖==方法来实现。比如:

struct Temperature: Equatable {
  var celsius: Double = 0
  var fahrenheit: Double {
    get { return celsius * 9 / 5 + 32 }
    set { celsius = (newValue - 32) * 5 / 9 }
  }
} 
func ==(lhs: Temperature, rhs: Temperature) -> Bool {
  return lhs.celsius == rhs.celsius
} 

使用Value Type不用担心竞争条件。也就是不用担心资源抢夺,加锁等。看下面的代码:

var numbers = [1, 2, 3, 4, 5]
scheduler.processNumbersAsynchronously(numbers) //异步处理numbers
for i in 0..<numbers.count { numbers[i] = numbers[i] * i }
scheduler.processNumbersAsynchronously(numbers) //异步处理numbers

在Reference Type中这个numbers将会发生资源抢夺,但在Swift中是Value Type的,在执行for i in 0..<numbers.count { numbers[i] = numbers[i] * i }的时候会发生Copy操作,所以不会发生资源抢夺。也就是说每次将Value Type赋值给其它的Value Type的时候会发生拷贝(逻辑拷贝)操作,但是这种Copy消耗的时间很微小,并且系统会将Copy推迟到写操作执行的时候,这就是Copy on Write。在Swift中可以利用Protocol将Struct等ValueType封装,类似于OOP中的多态

Swift中的Value Type和Reference Type混用会怎样呢?

我们来看Structh中含有结构体的情况:

   struct ButtonWrapper {
      var button: Button
   }

在这种情况下,复制ButtonWrapper的时候将会共享button这个Reference Type。这就违背了我们上文所说的Value Type在重新赋值的时候拷贝(深拷贝,和原来的Struct没有关系)。怎样才可以做到这一点呢?比如下面的

Value Type 中含有不可变的Reference Type

struct Image: Drawable {
  var topLeft:CGPoint
  var image: UIImage
}
var image = Image(topLeft:CGPoint(x:0,y:0),image:UIImage.init(named: "someImage.png"))
var image2 = image

这时image和image2将会公用一个UIImage:

ValueType Contains Reference Type

在实现Equatable协议的时候,我们这么做:

 extension Image: Equatable {}
 func == (left:Iamge, right:Image) -> Bool {
  return left.topLeft == right.topLeft && left.image === right.image
 }

但是由于UIImage是不可变的,所以我们不必担心image2的image对象的改变会影响到image的image对象。
注:上面===表示引用相同,但是不表示其指向Image是相同的,如果要表示其相同,需要使用==操作。

Value Type 中含有可变的Reference Type

下面我们来看一个可变的Reference Type。

struct BezierPath: Drawable {
  var path = UIBezierPath()
  var isEmpty: Bool {
    return path.empty
} 
  // **注意这种写法是错误的**
  func addLineToPoint(point: CGPoint) {
    path.addLineToPoint(point)
} } 

其内存结构是这样的:


Value Contains Reference Type

这时如果我们如果执行下面的代码

var bezierPath1 = bezierPath0

就会发现意想不到的Bug,因为你对bezierPath1的任何改动都将会显示到bezierPath0上。
怎样解决这样的问题呢?这时我们需要使用Copy On Write

对Value Type中的Reference Type做改动将会破坏Value Type的"完全独立"特性。
所以我们必须将可变的Reference Type和不可变的操作分开
不可变操作总是安全的
可变操作必须首先Copy

怎样做到Copy On Write呢?我们需要给BezierPath中加入如下代码:

struct BezierPath: Drawable { 
private var _path = UIBezierPath() var pathForReading: UIBezierPath { 
return _path 
} var pathForWriting: UIBezierPath { 
    mutating get {     _path = _path.copy() as! UIBezierPath 
    return _path 
} } 
} 

这样我们就可以将上述的错误代码改为:

extension BezierPath { 
var isEmpty: Bool { 
return pathForReading.empty 
} 
 mutating func addLineToPoint(point: CGPoint) {
    pathForWriting.addLineToPoint(point)
  }
} 

这样,我们在执行:

var path = BezierPath()
var path2 = path
if path.empty { print("Path is empty") }
var path2 = path
path.addLineToPoint(CGPoint(x: 10, y: 20))
path.addLineToPoint(CGPoint(x: 100, y: 125))

这段代码的时候就会在addLineToPoint的时候执行Copy,这就不会出现改动path而影响path2的现象了。
但是还有一个问题,每次执行addLineToPoint的时候都需要执行Copy操作,有时候如果这个对象只有一个引用那么就不需要这种操作,所以我们可以利用isUniquelyReferencedNonObjC()方法来判断时候需要Copy,如果返回true,说明只有一个对象在用,就不必Copy,如果返回false,说明很多对象在用,这个时候就需要执行Copy操作了。
用法如下:

struct MyWrapper {
  var _object: SomeSwiftObject
  var objectForWriting: SomeSwiftObject {
    mutating get {
     if !isUniquelyReferencedNonObjC(&_object)) {
        _object = _object.copy()
     }
     return _object
    }
} } 

注:

  1. 需要标示记忆过程的时候,比如实现撤销操作,需要恢复之前数值的时候。(备忘录模式)
  2. 比如需要对新的变化做特殊处理的时候,因为我们已经记忆了之前的过程,只需要对最新的Value Type改变,比如:本博客中的第一张图片,如果衣服颜色改变了,那么就只改变衣服颜色的那几个方格的值即可。

参考资料

https://developer.apple.com/videos/play/wwdc2015/414/

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,892评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,067评论 4 62
  • 认识金牛先生是个意外。 白羊小姐大概是单身太久了,觉得这是老天爷给她的恩赐,便无法自拔的深深迷恋上了他。金牛先生性...
    一只胡萝北_阅读 939评论 2 5
  • 没有老师到场的谢师宴应该不能叫做谢师宴吧。宁一花了一整个晚上的时间认真分析了这个问题,权衡再三为这场晚宴下了升学宴...
    王家人宁阅读 487评论 0 0
  • 今天早上本想着睡个懒觉儿子早早的就醒了,我说儿子你上学起不来一休息比妈妈起的都早。儿子说妈妈你快点起来做饭,吃完饭...
    放飞心情_a7c7阅读 115评论 0 0