本文翻译自 Mike Ash 的Friday Q&A 2015-07-17: When to Use Swift Structs and Classes

Swift 圈中有一个被反复讨论的话题是:何时使用struct,何时使用class.我觉得今天我也要给出我的个人观点.

值 VS 引用

答案真的很简单了:当你需要用值语义的时候使用class,需要用引用语义使用struct.就是这样!

我们下周再见…

等下干啥?

还没回答我的问题呢你啥意思?答案不明摆着么?

哦,但是…啥?

什么是值/引用语义?哦我明白了,我可能接下来会探讨下.

还有他们是如何关联到classstruct上的

所有都归根结底到数据以及数据存储的位置.我们把东西存在局部变量,参数,属性和全局变量中.从根本上又划分为两种不同的方式.

对于值语义来说,数据直接存在于存储单元中.对于引用语义,数据存在于其他地方,存储单元存储一个对数据的引用.当你存储数据的时候这种差异不一定明显.要注意的是拷贝存储的时候.对于值语义,你得到的是数据一份新拷贝.对于引用语义,你得到的是一份指向相同数据引用的新拷贝.

真是抽象,我们来看个例子吧.我们暂时先把 Swift 这茬放下,我们先看个 Objective-C 的例子:

@interfaceSomeClass:NSObject
@propertyintnumber;
@end
@implementationSomeClass
@end

structSomeStruct{
intnumber;
};

SomeClass*reference=[[SomeClassalloc]init];
reference.number=42;
SomeClass*reference2=reference;
reference.number=43;
NSLog(@"Thenumberinreference2is%d",reference2.number);

structSomeStructvalue={};
value.number=42;
structSomeStructvalue2=value;
value.number=43;
NSLog(@"Thenumberinvalue2is%d",value2.number);

打印结果:

Thenumberinreference2is43
Thenumberinvalue2is42

为啥不一样呢?

SomeClass *reference = [[SomeClass alloc] init]这行代码在内存中创建了一个SomeClass的新实例,然后在变量中放置了一个对那个实例的引用.reference2 = reference这行代码在新变量中放置了对相同对象的引用.reference.number = 43这行代码修改的是两个变量当前一起指向的对象中存储的数字.结果就是日志打印的是对象中的值,即43.

struct SomeStruct value = {}这行代码在变量中创建了一个SomeStruct的新实例.value2 = value将实例拷贝到第二个变量.每个变量有各自的数据块.

Swift 对应的例子:

classSomeClass{
varnumber:Int=0
}

structSomeStruct{
varnumber:Int=0
}

varreference=SomeClass()
reference.number=42
varreference2=reference
reference.number=43
print("Thenumberinreference2is\(reference2.number)")

varvalue=SomeStruct()
value.number=42
varvalue2=value
value.number=43
print("Thenumberinvalue2is\(value2.number)")

输出结果跟以前一样: 1 2 The number in reference2 is 43 The number in value2 is 42

值类型的经验

值类型并不是新鲜事物,但是对于很多人来说觉得它是新的.这是怎么回事呢?

struct在绝大部分 Objective-C 代码中并不是很常用.我们偶尔以CGRectCGPoint等方式接触到它们,但很少会自己去写.首先,它们不是很实用.用 Objective-C 在struct中正确地存储对象的引用的确很难,尤其是使用 ARC 的时候.

很多其他语言干脆没有类似struct的东东.许多语言如同 Python 和 JavaScript 一样”万物皆对象”,只有引用类型.如果你是从这类语言转型到 Swift 的,你可能对 struct 的概念就更陌生了.

等一下!有种情况下几乎所有语言都使用的值类型:数字!稍微有点编程经验的程序员都不会对下面的行为感到惊讶,这跟语言无关:

varx=42
varx2=x
x++
print("x=\(x)x2=\(x2)")
//prints:x=43x2=42

这对我们来说如此显而易见和自然以至于我们甚至没意识到结果的差异,但它就在我们眼前.你从开始编程之日起一直在使用值类型,即使你没意识到.

很多语言实际上将数字实现为引用类型,因为它们是”万物皆对象”哲学的死忠粉.然而,它们是不可变类型,值类型与不可变引用类型之间的差异很难察觉.它们表现得与值类型相同,尽管实现方式可能不同.

这是理解值和引用类型重要的一环.当数据变化时,差异主要关系到语法方面.假如数据是不可变的,那么值和引用的差别就消失了,或至少变成了仅是性能问题而不是语法差异.

实际上 Objective-C 的tagged pointers对此提到过.一个对象遇上了 tagged pointer 的处理,然后存储在指针的值中,就成了值类型.拷贝操作这时拷贝的就是对象内容了.表面上没差异,因为 Objective-C 函数库小心翼翼地仅将不可变类型放到 tagged pointer 中.有些 NSNumber 对象是引用类型而有些是值类型,但用起来没什么差别.

做出抉择

既然我们知道了值类型的工作原理,我们改为自己的数据类型选择那种方式呢? 这两种类型根本的区别就是当对其使用=时会发生什么.值类型是被拷贝,而引用类型只是得到另一个新引用.

因此在选择使用哪种类型时面对的根本问题是:拷贝它有意义么?拷贝操作是你想要变得简单,并经常使用的么?

我们先看些极端的,显而易见的例子.整型数明显是可以拷贝的,应该是值类型.网络套接字感觉是不能被拷贝,应该是引用类型.像是用 x,y 对儿的点坐标是可拷贝的,应该是值类型.用来表示磁盘的控制器感觉上不太容易被拷贝,应该是引用类型.

有些类型可以被拷贝,但它们不总是你希望的那样.建议把它们设为引用类型.比如屏幕上的一个按钮从概念上讲是可以拷贝的.副本按钮不会跟原来的按钮完全一样.点击副本按钮将不会激活原来的按钮.副本不会占用相同的屏幕位置.如果你将按钮传递到周围或放到一个新的变量里,你大概将想引用原本的按钮,除非明确被请求要做一份拷贝.这意味着你的按钮类型应该是一个引用类型.

视图和窗口控制器是类似的例子.它们可能想象上是能拷贝的,但它几乎从来都不是你想要的那样.它们应该是引用类型.

用于 Model 的类型该怎么搞?比方你有个User类型来表示系统中的用户,或者Crime类型来表示用户的活动.这些都是可完美拷贝的,所以它们或许应该是值类型.然而,你可能希望你程序中某处对UserCrime上的更新在程序的其他地方也可见.这就建议User应该被某种用户控制器来管理,而且它应该是引用类型.

集合是个有趣的例子.这包括比如数组和字典之类的东西,以及字符串.它们是可拷贝的么?显而易见.你想要做的拷贝操作是否易发生且经常发生呢?这不好说.

大多数语言对此说”不”,而是实现为引用类型. Objective-C,Java,Python,JavaScript 和几乎其他所有我能想到的语言都是这么干的.(一个主要的例外就是 C++ 的 STL 中的集合类型,但是 C++ 是语言世界中胡言乱语的疯子,它不走寻常路.)

Swift 说”不错”,这意味着如Array,DictionaryString都是struct而不是class. 它们在赋值和作为参数传递时被拷贝.只要拷贝的开销小,这就是个彻底明智的选择,而Swift费了很大力气去实现这点.

嵌套类型

嵌套使用值类型和引用类型会有四种组合方式.只是其中一个比较有趣.

如果一个引用类型包含了另一个引用类型,没有什么有趣的发生.任何指向其内部或外部值的引用通常都能修改它.每个人都会看到发生的变更.

如果一个值类型包含了另一个值类型,这实际上只是让其占用空间更多.内部值是外部值的一部分.如果你将外部值放进某个新的存储区,所有的值都会被拷贝,包括内部值.如果你将内部值放入某个新的存储,它会被拷贝.

一个引用类型包含了一个值类型实际上让被引用的值占用空间更大了.拥有对外部值的引用就可以操作全部值,包括被嵌入的值.被嵌入值的所有变更对指向外部值的引用是可见的.如果你将内部值放入某个新的存储区,它会被拷贝至那里.

一个值类型包含着一个引用类型那就不这么简单了.你实际上暗地里破坏了你想要用的值类型语义.这样做或好后坏,取决于你怎样去做.当你把一个引用类型放入到一个值类型中,当你把它放入新的存储区时外部值会被拷贝,但是拷贝后的副本有一个指向相同内嵌的原始对象的引用.这有个例子: class Inner { var value = 42 }

classInner{
varvalue=42
}

structOuter{
varvalue=42
varinner=Inner()
}

varouter=Outer()
varouter2=outer
outer.value=43
outer.inner.value=43
print("outer2.value=\(outer2.value)outer2.inner.value=\(outer2.inner.value)")

输出是:

outer2.value=42outer2.inner.value=43

虽然outer2得到一份value的拷贝,但它只拷贝了inner的*引用,于是这两个 struct 最终共享同一个Inner实例.因此对outer.inner.value的更新会影响到outer2.inner.value. 唉呀妈呀!

这种做法真的很方便.用这个方法可以创建个能够执行写时拷贝的struct,还能实现让值语义实际上不到处拷贝一坨坨的数据.这就是 Swift 中的集合的工作原理,你自己也可以实现自己的集合类型.想要知道更多详细信息,可以看看Let’s Build Swift.Array.

这么做也可能变得极其危险.比方说你创建了个Person类型.它被用作 Model 类当然也是可拷贝的,所以可以用struct实现咯.突发一阵对 OC 的怀旧,你决定用Nsstring作为Personname:

structPerson{
varname:Nsstring
}

然后你创建按了一对儿Person实例,拼接字符串构建出name:

letname=NSMutableString()
name.appendString("Bob")
name.appendString("")
name.appendString("Josephsonson")
letbob=Person(name:name)

name.appendString(",Jr.")
letbobjr=Person(name:name)

然后输出它们:

print(bob.name)
print(bobjr.name)

结果产生了:

BobJosephsonson,Jr.
BobJosephsonson,Jr.

靠! 发生了什么?区别于 Swift 的String类型,Nsstring是一个引用类型.它是不可变的,但它有个可变的子类,NSMutableString. 当 bob 创建时,它创建了一个对name字符串的引用.当那个字符串随后被修改时,变更会通过bob展现出来.要注意到即使bob是被let约束的值类型,但实际上改变了bob.这算不上真的修改了bob,只是修改了bob中引用的一个值,但因为那个值是bob数据的一部分,从语义上让人感到像是对bob作了修改.

这种事情在 Objective-C 中一直在发生.每个有经验的 Objective-C 程序员都有到处写防御拷贝的习惯.因为一个Nsstring实例可能实际上却是NSMutableString,为了避免灾难,你要将属性定义为copy,或者在初始化时显式调用copy方法.这同样适用于Cocoa中各种各样的集合类型.

在 Swift 中解决方案更简单些:使用值类型而不是引用类型.在这种情况下,让name成为String.再也不用担心无意中把引用共享咯.

在其他情况下,解决方案可能更简单.比如,你创建了一个包含视图的struct,而视图是引用类型且不能改成值类型.这或许是个好的迹象表明你不该用struct,因为你不管怎样都不能维持值语义.

结论

当移动值类型时它们会被拷贝,然而引用类型只是得到了一个对相同底层对象新引用.这意味着对引用类型的修改在每个引用上都看的到,然而对值类型的修改只会影响你修改的那块存储区.当选择使用哪种类型时,思考下如何拷贝你的类型比较恰当,如果需要深层拷贝就倾向于选择值类型.最后,谨防值类型中嵌入的引用类型,稍有不慎就会遭殃.

何时使用Swift Structs和Classes的更多相关文章

  1. ios – 声明NSDictionary并在Swift中添加键值对?

    我一直在尝试使用类类型键和值来声明一个NSDictionary,如下所示:这里,“Category”和“SubCategory”是全局类.我知道我不能将类类型用于关键字段.但是,无论如何,我应该做到这一点.有没有办法做到这一点?如何声明专门的NSDictionary或类似的东西来做到这一点?

  2. ios – 重新创建Persistant Store后的核心数据错误

    在我的应用程序中,我能够清除数据库中的所有数据.完成此操作后,将解析捆绑的JSON,然后将其保存到数据库(以便将数据库返回到默认状态).解析和保存此JSON的操作在任何情况下都可正常工作,除非在清除并重新创建持久性存储之后,在这种情况下我得到’NSinvalidargumentexception’,原因:’无法从此NSManagedobjectContext的协调器访问对象的持久存储’.在保存在后

  3. ios – 异常断点处于活动状态时,应用程序在启动时崩溃

    我刚开始继续开发一款适用于商店的传统iPad应用程序.我注意到项目中的异常断点未启用.当我启用它时,应用程序在启动时崩溃,但在输出窗口中没有给出任何信息,而在线程视图中只有相当无用的信息(见下文)我试着解决它..>将Autolayout设置为关闭.>通过编辑和重新保存故事板文件..但到目前为止没有运气.我的猜测是,故事板中的某些内容被破坏了,因为AppDelegates“确实完成了启动……”

  4. ios – Swift相当于`[NSDictionary initWithObjects:forKeys:]`

    Swift的原生字典是否与[NSDictionaryinitWithObjects:forKeys:]相当?假设我有两个带键和值的数组,并希望将它们放在字典中.在Objective-C中,我这样做:当然我可以通过两个数组迭代一个计数器,使用vardict:[String:Int]并逐步添加东西.但这似乎不是一个好的解决方案.使用zip和enumerate可能是同时迭代两者的更好方法.然而,这种方法

  5. ios – Swift 4添加手势:覆盖vs @objc

    我想在我的视图中添加一个手势,如下所示:但是,在Swift4中,我的编译器给出了以下错误:建议添加@objc以将此实例方法公开给Objective-C.实现此目的的另一个选项将覆盖touchesBegan()函数并使用它来处理点击.我试图以“Swift”的方式做到这一点,而不必带入Obj-C.有没有纯粹的Swift方式来添加这个轻击手势而不使用@objc?

  6. ios – Objective-C中“and”关键字的含义是什么?

    我在Xcode中输入了一条评论,但忘了领先//.我注意到了这一点并且突出显示为关键字.我做了一些谷歌搜索,但我似乎无法弄清楚它做了什么.这是什么意思?解决方法它是&&的同义词.见iso646.h.

  7. ios – 以编程方式在Swift中添加联系人

    我想在Swift中以编程方式添加联系人.我发现了一些Objective-C示例,但我没有让它们工作,甚至在Objective-C中也没有.我不希望这涉及到AddressBookUI,因为我想从我自己的UI中获取值.解决方法这是在Swift中添加联系人的快速方法.我在我的iPhone5iOS7.1上验证了它,因为我发现模拟器并不总是与我的手机对AB的东西相同.您可以添加一个按钮并指向此方法:顺便说一下–它假设你已经分配了一个地址簿var,你可以通过覆盖viewDidAppear来打开视图.它也会执行安全提示

  8. core-data – 错误: – [UIImage _deleteExternalReferenceFromPermanentLocation]无法识别的选择器发送到实例

    当我删除包含图像的托管对象时,在外部记录中存储为可转换值,然后我崩溃并出现此错误:解决方法我在AppleDeveloperforums回答了类似的事情.我猜你在数据建模器中的那个字段上选择了外部存储复选框.有一个bug可以解决.我是这样做的:一旦更新了数据并保存了上下文,任何删除它的尝试都会引发这个“无法识别的选择器”异常.要强制可以响应_deleteExternalReferenceFromPe

  9. ios – Objective-C中的Google用户serverAuthCode nil

    我正在尝试将GoogleSignIn框架集成到iOS应用程序中,并对服务器上的用户进行身份验证.我设法登录用户,但在–(void)signIn:(GIDSignIn*)signIndidSignInForUser:(GIDGoogleUser*)用户withError:(NSError*)错误委托方法,user.serverAuthCode为nil,我需要通过此服务器身份验证代码,嗯,验证服务器上

  10. ios – 无法识别的选择器发送到实例NSTimer Swift

    解决方法让updateTime成为一个类方法.如果它是在一个纯粹的Swift类中,你需要在@objc前面说明该方法的声明,如:

随机推荐

  1. Swift UITextField,UITextView,UISegmentedControl,UISwitch

    下面我们通过一个demo来简单的实现下这些控件的功能.首先,我们拖将这几个控件拖到storyboard,并关联上相应的属性和动作.如图:关联上属性和动作后,看看实现的代码:

  2. swift UISlider,UIStepper

    我们用两个label来显示slider和stepper的值.再用张图片来显示改变stepper值的效果.首先,这三个控件需要全局变量声明如下然后,我们对所有的控件做个简单的布局:最后,当slider的值改变时,我们用一个label来显示值的变化,同样,用另一个label来显示stepper值的变化,并改变图片的大小:实现效果如下:

  3. preferredFontForTextStyle字体设置之更改

    即:

  4. Swift没有异常处理,遇到功能性错误怎么办?

    本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请发送邮件至dio@foxmail.com举报,一经查实,本站将立刻删除。

  5. 字典实战和UIKit初探

    ios中数组和字典的应用Applicationschedule类别子项类别名称优先级数据包contactsentertainment接触UIKit学习用Swift调用CocoaTouchimportUIKitletcolors=[]varbackView=UIView(frame:CGRectMake(0.0,0.0,320.0,CGFloat(colors.count*50)))backView

  6. swift语言IOS8开发战记21 Core Data2

    上一话中我们简单地介绍了一些coredata的基本知识,这一话我们通过编程来实现coredata的使用。还记得我们在coredata中定义的那个Model么,上面这段代码会加载这个Model。定义完方法之后,我们对coredata的准备都已经完成了。最后强调一点,coredata并不是数据库,它只是一个框架,协助我们进行数据库操作,它并不关心我们把数据存到哪里。

  7. swift语言IOS8开发战记22 Core Data3

    上一话我们定义了与coredata有关的变量和方法,做足了准备工作,这一话我们来试试能不能成功。首先打开上一话中生成的Info类,在其中引用头文件的地方添加一个@objc,不然后面会报错,我也不知道为什么。

  8. swift实战小程序1天气预报

    在有一定swift基础的情况下,让我们来做一些小程序练练手,今天来试试做一个简单地天气预报。然后在btnpressed方法中依旧增加loadWeather方法.在loadWeather方法中加上信息的显示语句:运行一下看看效果,如图:虽然显示出来了,但是我们的text是可编辑状态的,在storyboard中勾选Editable,再次运行:大功告成,而且现在每次单击按钮,就会重新请求天气情况,大家也来试试吧。

  9. 【iOS学习01】swift ? and !  的学习

    如果不初始化就会报错。

  10. swift语言IOS8开发战记23 Core Data4

    接着我们需要把我们的Rest类变成一个被coredata管理的类,点开Rest类,作如下修改:关键字@NSManaged的作用是与实体中对应的属性通信,BinaryData对应的类型是NSData,CoreData没有布尔属性,只能用0和1来区分。进行如下操作,输入类名:建立好之后因为我们之前写的代码有些地方并不适用于coredata,所以编译器会报错,现在来一一解决。

返回
顶部