王垠:Swift语言的设计错误,以及大家对王垠此论的评价

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友 微信微信
查看查看374 回复回复11 收藏收藏1 分享淘帖 转播转播 分享分享 微信
查看: 374|回复: 11
收起左侧

王垠:Swift语言的设计错误,以及大家对王垠此论的评价

[复制链接]
苏格拉没有底 发表于 2016-7-25 03:08:23 | 显示全部楼层 |阅读模式
快来登录
获取最新的苹果动态资讯
收藏热门的iOS等技术干货
拷贝下载Swift Demo源代码
本帖最后由 苏格拉没有底 于 2016-10-28 11:09 编辑

在《编程的智慧》一文中,我分析和肯定了 Swift 语言的 optional type 设计,但这并不等于 Swift 语言的整体设计是完美没有问题的。其实 Swift 1.0 刚出来的时候,我就发现它的 array 可变性设计存在严重的错误。Swift 2.0 修正了这个问题,然而他们的修正方法却没有击中要害,所以导致了其它的问题。这个错误一直延续到今天。

王垠:Swift语言的设计错误

王垠:Swift语言的设计错误,以及大家对王垠此论的评价 - 敏捷大拇指 - 王垠:Swift语言的设计错误


Swift 1.0 试图利用 var 和 let 的区别来指定 array 成员的可变性,然而其实 var 和 let 只能指定 array reference 的可变性,而不能指定 array 成员的可变性。举个例子,Swift 1.0 试图实现这样的语义:

[Swift] 纯文本查看 复制代码
var shoppingList = ["Eggs", "Milk"]

// 可以对 array 成员赋值
shoppingList[0] = "Salad"


[Swift] 纯文本查看 复制代码
let shoppingList = ["Eggs", "Milk"]

// 不能对 array 成员赋值,报错
shoppingList[0] = "Salad"


这是错误的。在 Swift 1.0 里面,array 像其它的 object 一样,是一种“reference type”。为了理解这个问题,你应该清晰地区分 array reference 和 array 成员的区别。在这个例子里,shoppingList 是一个 array reference,而 shoppingList[0] 是访问一个 array 成员,这两者有着非常大的不同。

var 和 let 本来是用于指定 shoppingList 这个 reference 是否可变,也就是决定 shoppingList 是否可以指向另一个 array 对象。正确的用法应该是这样:

[Swift] 纯文本查看 复制代码
var shoppingList = ["Eggs", "Milk"]

// 可以对 array reference 赋值
shoppingList = ["Salad", "Noodles"]

// 可以对 array 成员赋值
shoppingList[0] = "Salad"


[Swift] 纯文本查看 复制代码
let shoppingList = ["Eggs", "Milk"]

// 不能对 array reference 赋值,报错
shoppingList = ["Salad", "Noodles"]

// let 不能限制对 array 成员赋值,不报错
shoppingList[0] = "Salad"


也就是说你可以用 var 和 let 来限制 shoppingList 这个 reference 的可变性,而不能用来限制 shoppingList[0] 这样的成员访问的可变性。

var 和 let 一旦被用于指定 array reference 的可变性,就不再能用于指定 array 成员的可变性。实际上 var 和 let 用于局部变量定义的时候,只能指定栈上数据的可变性。如果你理解 reference 是放在栈(stack)上的,而 Swift 1.0 的 array 是放在堆(heap)上的,就会明白array 成员(一种堆数据)可变性,必须用另外的方式来指定,而不能用 var 和 let。

很多古老的语言都已经看清楚了这个问题,它们明确的用两种不同的方式来指定栈和堆数据的可变性。C++ 程序员都知道 int const * 和 int * const 的区别。Objective C 程序员都知道 NSArray 和 NSMutableArray 的区别。我不知道为什么 Swift 的设计者看不到这个问题,试图用同样的关键字(var 和 let)来指定栈和堆两种不同位置数据的可变性。实际上,不可变数组和可变数组,应该使用两种不同的类型来表示,就像 Objective C 的 NSArray 和 NSMutableArray 那样,而不应该使用 var 和 let 来区分。

Swift 2.0 修正了这个问题,然而可惜的是,它的修正方式是错误的。Swift 2.0 做出了一个离谱的改动,它把 array 从 reference type 变成了所谓 value type,也就是说把整个 array 放在栈上,而不是堆上。这貌似解决了以上的问题,由于 array 成了 value type,那么  shoppingList 就不是 reference,而代表整个 array 本身。所以在 array 是 value type 的情况下,你确实可以用 var 和 let 来决定它的成员是否可变。

[Swift] 纯文本查看 复制代码
let shoppingList = ["Eggs", "Milk"]

// 不能对 array 成员赋值,因为 shoppingList 是 value type
// 它表示整个 array 而不是一个指针
// 这个 array 的任何一部分都不可变
shoppingList[0] = "Salad"


这看似一个可行的解决方案,然而它却没有击中要害。这是一种削足适履的做法,它带来了另外的问题。把 array 作为 value type,使得每一次对 array 变量的赋值或者参数传递,都必须进行拷贝。你没法让两个变量指向同一个 array,也就是说 array 不再能被共享。比如:

[Swift] 纯文本查看 复制代码
var a = [1, 2, 3]

// a 的内容被拷贝给 b
// a 和 b 是两个不同的 array,有相同的内容
var b = a


这违反了程序员对于数组这种大型结构的心理模型,他们不再能清晰方便的对 array 进行思考。由于 array 会被不经意的自动拷贝,很容易犯错误。数组拷贝需要大量时间,就算接收者不修改它也必须拷贝,所以效率上有很大影响。不能共享同一个 array,在里面读写数据,是一个很大的功能缺失。由于这个原因,没有任何其它现代语言(Java,C#,……)把 array 作为 value type。

如果你看透了 value type 的实质,就会发现这整个概念的存在,在具有垃圾回收(GC)的现代语言里,几乎是没有意义的。有些新语言比如 Swift 和 Rust,试图利用 value type 来解决内存管理的效率问题,然而它带来的性能提升其实是微乎其微的,给程序员带来的麻烦和困扰却是有目共睹的。完全使用 reference type 的语言(比如 Java,Scheme,Python),程序员不需要思考 value type 和 reference type 的区别,大大简化和加速了编程的思维过程。Java 不但有非常高效的 GC,还可以利用 escape analysis 自动把某些堆数据放在栈上,程序员不需要思考就可以达到 value type 带来的那么一点点性能提升。相比之下,Swift,Rust 和 C# 的 value type 制造的更多是麻烦,而没有带来实在的性能优势。

Swift 1.0 犯下这种我一眼就看出来的低级错误,你也许从中发现了一个道理:编译器专家并不等于程序语言专家。很多经验老到的程序语言专家一看到 Swift 最初的 array 设计,就知道那是错的。只要团队里有一个语言专家指出了这个问题,就不需要这样反复的修改折腾。为什么 Swift 直到 1.0 发布都没有发现这个问题,到了 2.0 修正却仍然是错的?我猜这是因为 Apple 并没有聘请到合格的程序语言专家来进行 Swift 的设计,或者有合格的人,然而他们的建议却没有被领导采纳。Swift 的首席设计师是 Chris Lattner,也就是 LLVM 的设计者。他是不错的编译器专家,然而在程序语言设计方面,恐怕只能算业余水平。编译器和程序语言,真的是两个非常不同的领域。Apple 的领导们以为好的编译器作者就能设计出好的程序语言,以至于让 Chris Lattner 做了总设计师。

Swift 团队不像 Go 语言团队完全是一知半解的外行,他们在语言方面确实有一定的基础,所以 Swift 在大体上不会有特别严重的问题。然而可以看出来这些人功力还不够深厚,略带年轻人的自负,浮躁,盲目的创新和借鉴精神。有些设计并不是出自自己深入的见解,而只是“借鉴”其它语言的做法,所以可能犯下经验丰富的语言专家根本不会犯的错误。第一次就应该做对的事情,却需要经过多次返工。以至于每出一个新的版本,就出现一些“不兼容改动”,导致老版本语言写出来的代码不再能用。这个趋势在 Swift 3.0 还要继续。由于 Apple 的统治地位,这种情况对于 Swift 语言也许不是世界末日,然而它确实犯了语言设计的大忌。一个好的语言可以缺少一些特性,但它绝不应该加入错误的设计,导致日后出现不兼容的改变。我希望 Apple 能够早日招募到资深一些的语言设计专家,虚心采纳他们的建议。BTW,如果 Apple 支付足够多的费用,我倒可以考虑兼职做他们的语言设计顾问 ;-)




Java 有 value type 吗?

有人看了以上的内容,问我:“你说 Java 只有 reference type,但是根据 Java 的官方文档,Java 也有 value type 和 reference type 的区别的。” 由于这个问题相当的有趣,我另外写了一篇文章来回答这个问题。




相关内容:

王垠:编程的智慧【精品】Swift开发者/程序员/工程师必看

王垠:Swift语言的设计错误



都看到这里了,就把这篇资料推荐给您的好朋友吧,让他们也感受一下。

回帖是一种美德,也是对楼主发帖的尊重和支持。

*声明:敏捷大拇指是全球最大的Swift开发者社区、苹果粉丝家园、智能移动门户,所载内容仅限于传递更多最新信息,并不意味赞同其观点或证实其描述;内容仅供参考,并非绝对正确的建议。本站不对上述信息的真实性、合法性、完整性做出保证;转载请注明来源并加上本站链接,敏捷大拇指将保留所有法律权益。如有疑问或建议,邮件至marketing@swifthumb.com

*联系:微信公众平台:“swifthumb” / 腾讯微博:@swifthumb / 新浪微博:@swifthumb / 官方QQ一群:343549891(满) / 官方QQ二群:245285613 ,需要报上用户名才会被同意进群,请先注册敏捷大拇指

嗯,不错!期待更多好内容,支持一把:
支持敏捷大拇指,用支付宝支付10.24元 支持敏捷大拇指,用微信支付10.24元

评分

参与人数 1金钱 +10 贡献 +10 专家分 +10 收起 理由
Anewczs + 10 + 10 + 10 32个赞!专家给力!

查看全部评分

我是90后 发表于 2016-10-28 00:14:52 | 显示全部楼层

如何评价王垠的《Swift 语言的设计错误》?




1、

顶王垠, 不管王垠文章里面有多少错误, 有句话说得很对: 语言是给开发者用的, 不是给编译器专家用的. 你 blabla 搞这么语义, 这门语言的用户体验一坨屎一样. 扯那么实现有用吗?




2、

没有任何其它现代语言(Java,C#,……)把 array 作为 value type。


王垠这是在黑 php 不是现代语言吗?




3、

看着很厉害 别的我不懂,但是看到下面这段,我忍不住笑了:

“Java 不但有非常高效的 GC,还可以利用 escape analysis 自动把某些堆数据放在栈上,程序员不需要思考就可以达到 value type 带来的那么一点点性能提升。相比之下,Swift,Rust 和 C# 的 value type 制造的更多是麻烦,而没有带来实在的性能优势。”





4、

放在C++里,就是要实现const 引用不能调用非const成员函数。
也就是禁止const Class &b = a;b.set(X);这样的语句。
但是Swift里面let的语义是这样:Class& const b = a;//我故意把&跟Class写一起了,因为Swift里面没有显性的引用,或者说都是引用
这就尴尬了,因为被禁止的只是b=c这种语句,而b.set还是随便调用。这跟我预想不一样啊,怎么办呢?
于是苹果大神想了一种方法,对于某些类,它们比较常用,也经常用于存常量,就让他们作为值来传递,比如数组,把上面的Class&整个替换成Array,写下Array b = a;的时候b整个就是一个常量,不能调用任何非const方法([]=可以看作set(index, value)方法),当然b=c也是不允许的。
目的达到论了,似乎挺和谐。

但是这么做引来一些问题。
一是王神所说,数组这么大,靠值传递会引发严重的效率问题。这个Swift采取的解决方案是copy-on-write,简称cow(母牛)。线程安全与多核并行问题暂且不论,在C++里面,cow是非常蛋疼的,因为[]无法区分左右(可以参考下map里面的[]操作,不管是get还是set都会给你insert),导致c = str[5]这样的表达式也能触发cow。事实上string的复制效率非常高,一般都会用memcpy实现,并且如果我不想复制直接用const string &就行了,需要复制的时候早晚也要复制,cow完全是自作多情的做法。然而某些C++编译器似乎一意孤行的使用这项在VS2003时代就放弃了的“技术”,果然C++是装逼乐园。
TMD扯远了,一黑就收不住。总之cow是蛋疼的东西,没有任何效率优化,却增加了程序员的思考负担。王神在博客中用的一个词非常好——削足适履。

第二个问题是把Array当值传递使得语义不那么统一。类型分为两类:不可变类型,包括C中的int, double, Java/Python中的字符串,他们除了用赋值之外没有任何改变自身值的方法;可以改变的类型,包括各种自定义类,数组,它们可以通过某种操作改变自身,比如set方法,比如[]=的操作。如果前者按值传递,后者按引用传递,那么这种传递方式可以看成所有对象都是引用传递(赋值可以看成改变引用,参见Java 有值类型吗?)。无端的让数组值传递,破坏了上述统一性,也使得数组从“可改变对象”中割裂,为什么偏偏是数组,其他类对象怎么不这么搞?
PS:C的传递方式非常统一,都是值传递;C++的分为引用和值两种,不过都是显式声明的,也无可厚非;C#就不那么统一了,既有值又有引用,光看var b = a不知道是怎么传的。

问题之三还是王所说,value type并没有带来多少效率的提升,却大大增加麻烦。在这里不得不黑一波Rust,这货号称比C++安全……个屁!用了各种关键字,各种符号,各种库,就是为了实现C++里面的各类智能指针,同时各种指针本身的问题一个也没避免,租借move之后只能有一个在用(unique),RC不能循环引用(shared),非线程安全,再加上unsafe(raw pointer),根本就是比C++没有一丁点长进,完全是一厢情愿,倒也有mozilla的作风。

回到问题。有人可能会问,既然你说这么做不好,那应该怎么做?我的看法,一是引入新关键字,如C++的const,C#的readonly,禁止一切改变类成员的行为(数组元素可以看成数组类的成员);二是像Java/Python/C(89)那样,放着不管,不提供保护措施,你爱改就改出了问题自己负责。两种方法都能保证语义的统一性,前者稍微复杂,能满足强迫症需求;后者更加简洁,代价是几乎为0的出错几率。

iOS9变卡不是没有原因的。




5、

文章的核心主题:

把 array 作为 value type,使得每一次对 array 变量的赋值或者参数传递,都必须进行拷贝。你没法让两个变量指向同一个 array,也就是说 array 不再能被共享。

这违反了程序员对于数组这种大型结构的心理模型,他们不再能清晰方便的对 array 进行思考。由于 array 会被不经意的自动拷贝,很容易犯错误。


SO上的提问已具体说明问题:Swift: Pass array by reference?

王垠谈论的语义上的问题,而不是具体实现问题。




6、

各种平台、语言、环境、app等,我收集的bug有整整一堆,包括windows的一些从win7开始就没更正问题。

这些东西,我只用作装逼时的谈资罢了,把这些东西当回事拿出来说,那太low了,我丢不起这人。




7、

Swift的篇幅少点,说明还是设计得不错。

我发现王老师总拿Java的观点来看待其它语言,这是病得治。王老师无非就是认为值类型每次赋值必定发生复制,开销很大,而引用类型可以选择手动显式复制,或者仅仅将引用指向相同实例。但是值类型不意味着“数据一定放栈上”,它是根据对象赋值时的行为而不是储存位置来定义。有一类值类型底层实现上把数据放堆上,它就可以实现“写时复制”,避免不必要的开销。典型的例子就是C++的std::vector或者std::string。如果王老师要喷Swift.Array,把C++的容器也一起喷了吧。但是我从来没看过王老师喷C++,明明实现机制和类型都是一样的,说明还是欺软怕硬啊。

不过有一点王老师说的还是对的,就是对于imutable,值类型和引用类型无所谓,或者说这种二分法不适用于它们。比如Java的String和JS的string在行为上完全一致,但是JS标准中的string就是值类型。




8、

我对Swift了解还不是很深,但是比起王垠,我更相信喵神。

如何评价王垠的《Swift 语言的设计错误》? 1

王垠:Swift语言的设计错误,以及大家对王垠此论的评价 - 敏捷大拇指 - 如何评价王垠的《Swift 语言的设计错误》? 1


如何评价王垠的《Swift 语言的设计错误》? 2

王垠:Swift语言的设计错误,以及大家对王垠此论的评价 - 敏捷大拇指 - 如何评价王垠的《Swift 语言的设计错误》? 2





9、

毕竟是编译原理没考及格的水平。

编译原理考试第一题。对于业务逻辑狗来说,语义是strict, immutable的语言才是最好的。可是业界因为immutable更慢占用内存更多等偏见不愿意使用immutable的语言。业务逻辑狗有苦说不出啊。你学习了编译原理,现在是时候写一个编译器,驳斥各种不实的说法了。Tips: MLton通过whole-program analysis把高阶函数拍扁成一阶函数,你也可以用类似的方法,自动找出可以改成mutable而不影响语义的代码,并对其进行转换。




10、

我只能说

王博士程序写的太少了,才无法体会这个设计的必要性和简洁性,语言是给开发者开发使用的,开发者的用户体验才是更加重要的,而不是什么编译器专家或者语言设计专家

其他:最大的问题就是以偏概全,抓住一个问题(不知道算不算问题,就姑且按其观点算吧),然后极大的贬低一个新生知名语言(大概是为了突出自己牛B?)

恩,iPhone面世的时候,如果按这种标准来挑毛病,估计是百无是处吧?




11、

依稀记得许多年前看过一篇文章,讲的大概是copy-on-write在多核的新环境下已经落后、低性能、没人用了的事情,所以Delphi要跪。没想到Swift也(略

果然能用的语言来来回回都是这么几样东西,普通程序员已经有几十年没有被迫接触过新东西了。

=============

王垠写了篇新文章来澄清(Java 有值类型吗?),大概说的就是“我口中的值类型和引用类型跟你们口中的不一样,你们要按照我说的来讨论”。当然这篇文章不看“王垠的术语”和“大家的术语”的区别的话,讲的还是很有道理的。




12、

难道就我一个觉得他说得有道理吗?

容器可变性是否包含元素可变性本来就是一个见仁见智的问题。

我倾向于是不包含。

非要包含也行,但成本会很大。到时候各位别骂什么速度慢,占用内存多,不灵活什么的。




13、

文章的重点在最后一句啊。
数组什么的说得挺有道理,不过他对value type的看法倒是很符合他没有太多实际工程经验的背景,没有被GC坑过的人有这样的观点很正常,毕竟50行代码GC也没机会干什么。




14、

Swift 中 let 和 var 的语义,我也觉得确实难于理解。如果 let 作用于 Value Type 时能使其本身不可变而不仅仅是 Reference 不可变,就超出了 Type 跟着 Value 走的心智模型。你拿到一个东西,它的值是否可变,不在这个值的类型信息上,而是在这个 Name Binding 使用了 let/var上,Immutable 跟 Mutable 类型的接口往往也会设计得不一样,比如 Immutable Array append 方法返回值跟 Mutable Array 的 append 方法返回值保持一样合理吗?你会发现,同一个数据类型,它的方法因为let/var的区别语义完全变掉了。再一个,如果可变性是 Array 类型本身的信息,那就可以轻松理解 ImmutableArray of MutableArray 这样的类型,我不知道用了 let/var 还怎么去解释这些。

不过,我觉得在讨论语义时,不应该将具体实现牵扯进来(比如 C++ 中按值传递按引用传递,反汇编说传了内存地址来解释,就显得不得要领)。这么大岁数还要扯分配在堆上还是分配在栈上,这是王垠这篇文章让人大跌眼镜的地方。




15、

什么叫“抛砖引玉”,我确实真切的见识到了。。。
自叹不如




16、

如何评价王垠的《Swift 语言的设计错误》? 3

王垠:Swift语言的设计错误,以及大家对王垠此论的评价 - 敏捷大拇指 - 如何评价王垠的《Swift 语言的设计错误》? 3





17、

很遗憾,这篇文章从头到尾都在胡扯。王垠既不知道swift编译器的内部实现,也不知道swift的历史进程。

swift在设计之初就对class和struct两种类型进行了明确区分,let和var对于class而言只保证引用不可变,对于struct应保证其内部也是不可变的。在swift3.0中,还计划继续将诸如NSDate和NSData等类作为struct引入swift标准库,以确保在使用let或var的时候字面意思与其实际意义一致(即看起来应该是值类型的表现也应当是值类型)

对于堆栈使用的问题此文更是一派胡言,swift的值类型都采取了copy on write策略,并不会因为简单的赋值操作影响性能,而struct内部实现实质上是放在堆上,基于对开发者不透明的一种特殊引用计数管理其生命周期。这也引出了另外一个话题,就是在目前版本的swift下能够利用这点构造基于struct的循环引用,譬如http://www.cocoawithlove.com/blog/2016/03/27/on-delete.html中所介绍的情况。如果真的要喷的话,这种混乱的引用计数实现才是喷点,而不是王垠那篇文章中那样基于猜测的乱喷。




18、


一、引言

王垠的文章暴露了一個問題,就是他沒看過 Swift Array 的具體實現。

Swift Array 這個 struct 其本身只是一個傀儡,在這個 struct 內部存儲了指向這個 array 真正 storage 的 reference。這個 storage,沒錯,就是 reference type 的。而 Array 這個 struct 只是其內部 storage 的一個 owner。

當有多個 owner 同時指向這個 storage,而其中任意一個 owner 要修改這個 storage 內部的內容時,就會先複製這個 storage 的內容,再在複製出來的 storage 內進行真正的修改。

不同於 Objective-C 的是,Swift 的 struct 在進行 let a = b 這種形式的複製,又或者在進行 foo(bar) 這種形式的參數傳遞時會將這個 struct 內所有 reference 的引用計數 +1。而對於 Swift Array 的封裝手法而言,這樣的機制使得單純的 let a = b 形式的複製又或者函數傳參可以增加 Array 內部的 storage 的引用計數,從而新複製出來的 a,或者 foo 函數體內的 bar 這個變量會成爲同一個 storage 的新的 owner。這樣的設計豈不妙哉。


二、關於寫入時複製

Swift Array 具體的複製策略會更加複雜,比如說如果我需要取這個 array 的一個 slice,那麼是不會有複製發生的,因爲我們可以通過在 owner 內部記錄 storage 上所需內容開始與結束的位置來限制這個「傀儡」「看起來」所儲存內容。同樣,移除首部和尾部的元素應該也會有同樣的優化(這個地方的具體實現我沒看,但是我覺得我這麼笨的人都可以想到的優化點,不可能 Swift core team 想不到)。

對於使用了類似封裝手法的 Dictionary 和 Set 而言,我們也可以猜測,如果內部實現是自動平衡二叉樹的話,在複製時也應該是僅僅深拷貝一側的節點,因爲這個優化策略早在上個世紀 90 年代惠普開發的 C++ STL 的 map 中就出現了,前人的智慧很難想象 Swift core team 不會借鑑。

所以 Swift 並不是傻傻地去進行無腦複製,而是想盡辦法不複製,如果要複製,也想盡辦法不進行完全複製。


三、新的抽象

事實上,Swift 有志於做一個 system programming language,而相對於傳統的系統編程語言——「C」來說,多數人所知曉的 Swift 特性遠遠不能夠支撐 Swift 成爲一個系統編程語言。然而 Swift 的特性其實是足夠做一個系統編程語言的,比如說 ManagedBuffer 和指針。

Swift 擁有指針,但是所有和指針掛鉤的 API 都基本以 unsafe 開頭(說「基本」是因爲我不覺得我記下了 Swift 所有的 API)。

同樣,ManagedBuffer 是一個比指針更加高級的抽象,代表 heap 內存上的一段連續的儲存空間。如果你真的需要一套像 Objective-C 一樣的數組類型系列的話,那麼 ManagedBuffer 就是你的好朋友。但是在這個抽象層面,你必須自己制定容器容量的增長的策略,是 2 還是 1.5 還是 1.3?我相信沒有很好的理論基礎,你回答不出來這個問題。

我問這個問題並不是我裝屄,而是想說:像 Apple 的 OS X 分小白用戶的 GUI 和 高階用戶的 terminal 介面一樣,Swift 也被分爲了初階和高階兩套「介面」,value type 的 Array 就是初階「介面」,而更多的高階特性都被很好的隱藏起來了,而當你需要開發底層應用時能夠通過檢索使用手冊來找到,這就是 Swift 的設計思想。

而 Swift 的這套「初階介面」,其實是一套新的抽象。

Swift 編譯器將 Swift 中的符號(不是 ABI,而是編譯過程中符號表中的符號)歸類爲直接符號與間接符號,直接符號的內容保存直接符號所代表的數據,間接符號的內容保存「指向實際保存間接符號所代表數據的內存地址」。直接符號有 struct, enum。間接符號有 class 和 indirect enum。而 let 和 var 實際上是控制這些符號內容可變性的 qualifier。

Swift core team 似乎認爲入門 Swift 只需要掌握符號內容可變不可變的概念,對於內存的 heap 和 stack 抽象可以不知道——從直截使用 let 和 var 來修飾 value type 和 reference type 所產生的結果而言,初學者會得到一個非常直觀的 intuition,通過這個 intuition 可以直截指導後面的開發,而並不需要其去翻閱計算機組成原理相關書籍來瞭解計算機的幾個抽象。其實在多線程的抽象從 thread(POSIX) 變成 queue(libdispatch) 之後,heap 和 stack 的概念對於初學者而言確實可以不知道了。因爲一旦提及 thread 這個抽象,那麼必然會講到「每個 thread 都有自己的 stack 內存,但是 heap 內存共享」;而 libdispatch 的 queue 抽象則是基於任務的。

從這點看,隱藏內存的 heap 和 stack 抽象,引進 let 和 var 控制符號內容可變性的抽象就是 Swift 給初學者新的抽象。並且從控制符號內容可變性這點看, let 和 var 的語義其實是沒有問題的。


四、對稱性

我一月在 Swift Evo 的郵件列表中投遞了一封建議將 fail-able initializer 改成 throwing initializer 的郵件,Crhis Lattner 本人對此表示了否定,回覆如下:
I’d be opposed to removing failable initializers. Failable inits introduce a symmetry into the language for initializers, which make them possible to do (almost) all of what you can do with a normal method. This capability is key for them to be able to replace “factory” static methods, which allows Swift to offer a consistent initialization pattern for clients of types.

If we forced people to use error handling for anything that could return nil, then things like String to Int conversions would most likely not use initialization syntax.

Besides that, over use of error handling waters it down and makes it less valuable in itself. For more information on this, please see the design discussion for error handling:
swift/ErrorHandlingRationale.rst at master · apple/swift · GitHub

-Chris


在這封郵件中,Chris Lattner 提到了一個我之前從來沒注意過的概念,就是「對稱性」,細說就是「特性的對稱性之於語言」、「介面的對稱性之於框架」。對於 Swift 而言,let 和 var 控制符號內容可變性的抽象所帶來的 value typed Array 確實影響了「精確控制 Array 內部數據複製時機」的需求,但是 Swift 亦有 ManagedBuffer 和指針這幾個「對稱」的特性來讓你實現對數據複製時機進行精確控制的需求。當然,我想既然你有這種需求,不會連 heap 內存上容器的增長因子應該選多少這個問題都答不出來。


五、性能與內存管理方案

離開需求談性能都是耍流氓,我對於服務器開發就是小白,所以我簡單說說我熟悉的 GUI 開發。

對於目前大多數 GUI 框架而言,主線程通常被用來渲染 GUI。而長時間、又或者不可預測地佔用主線程工作時間就會導致 GUI 卡頓。這就是爲甚麼安卓機能再強,還是會出現不可預測、不可重現、不能在 profiling 中發現的卡頓的原因——因爲 Java 回收內存的時機不可預測,回收內存所佔用的時間不可預測,故而這種由 GC 引起的卡頓在 profiling 中無法有規律的出現,也就導致了安卓機器在使用中出現的不可預測、不可重現的卡頓。這也是 GC 語言並不適合用來寫 GUI 的原因。

同樣使用 GC 語言來編寫 GUI 的魔獸世界也有同樣的問題。你有沒有玩着玩着突然卡一下的經歷?有的話很有可能就是魔獸世界 GUI 所採用的 Lua 語言正在進行 GC 的原因。這點在裝了一大堆插件後尤甚。

嚴格來講,Swift 所使用的 ARC 也是 GC 的一種形式,只是世人常指的 GC 是 Java 類的 GC。而 ARC 相較於 Java 類的 GC 來說有一個明顯的優點就是內存釋放時機和內存釋放所佔時間可以預測,並且可以在 profiling 中有規律的出現。並且 ARC 的內存釋放可以發生在背景線程,對於多 CPU 架構而言,這又是提高程序表現的一個 trick。可以預測的內存釋放時機及釋放時間對於很多實時應用都非常重要,比如說錄影、直播、遊戲。

其實關於 Swift 要不要改用 GC,Swift Evo 郵件列表也已經有過充分的討論:


當然,像 Swift 這種數據結構應用了寫入時複製的一個後果也是數據複製時機幾乎不可預測,從而理論上加大了 profiling 的難度,但是如果數據規模不大的話,我想這個具有迷惑性的後果可以忽略不計。如果真的要精確控制數組內容複製的時機,那麼 ManagedBuffer 是你的好朋友。

我不是 VM 和 GC 專家,這個話題我也就不多說了。

----------------------------------------------------------


幾點澄清:

1. Swift 的初階、高階「介面」之分來源於「對使用者最低知識水平要求」的限制,而不是說用 Array 的就是初階開發者,用 ManagedBuffer 和指針的就是高階開發者。實際上,我在用 Swift 實現 graph 的 adjacent list 實現的時候也會用 Array,實現鏈表的時候則是用的 Unmanaged 這個 struct 來規避 ARC 以求性能最大化。

2. 我是 Java 黑,不是安卓黑。作爲一個學設計出身的人,要專門黑安卓也是黑設計。




19、

作者:刘闽晟

昨天早上(美西时间)醒来写的答案不知所云,我自己都看不懂:

Swift 团队对此的考虑更像是深思熟虑的结果。比如说,最近通过的 SE-0069 Mutability and Foundation Value Types 已经钦定了 Core Lib 用值类型表示不可变数据、引用类型表示可变数据。考虑到他们官方推荐给大数据结构写时复制,恐怕王垠的提议,通过不同的 class 表示同一类型的可变与不可变版本,早在内部被拒绝了,改成用 struct 与 class 表示这一区别。


不过我重新组织一下语言:

一、Swift 现有的数组实现不太需要关心性能问题,因为采用了写时复制(copy on write)语义,几个高票答案在这一方面已经描述地很好了。同时,Swift 官方文档也推荐用户在自己实现比较大的数据结构的时候实现写时复制语义,原因正如王垠所说,直接复制开销过大。

二、在 Swift 里,如果一个 let 变量的类型是一个类,该变量虽然不可以指向一个新的对象,但却可以调用该对象的任意方法,而这些方法可能会改变对象本身的内容。大家可以在 Playground 里运行这个例子:
[Swift] 纯文本查看 复制代码
class SomeClass {
    var value: Int
    init(value: Int) {
        self.value = value
    }
}

let ref = SomeClass(value: 3)
// 3
print(ref.value)
ref.value = 4
// 4
print(ref.value)


(这段内容即将失效,本答案写于 WWDC 2016 前一个星期)

套用 @米斯特菜 所提到的「对称性」一词,Swift 上现有的 Cocoa 接口缺少一个 symmetry between mutable data types and immutable ones。很多 Cocoa 的类型都是 class 而不是 struct 或者 enum,比如说 NSBundle,你在几个月内都没有办法表示一个 immutable 的 NSBundle。这也正是王垠在他博文里提到的问题:以前的 Cocoa,是有 NSArray 和 NSMutableArray 的区别的。

不过正如我原始答案所提到的,刚刚通过的 SE-0069 Mutability and Foundation Value Types 致力于解决这个问题。根据这个提议,标准库里的很多类型将成对出现,比如说

  • AffineTransform 与 NSAffineTransform
  • CharacterSet 与 NSCharacterSet
  • Date 与 NSDate
  • DateComponents 与 NSDateComponents


其中没有 NS 前缀的将是值类型(value type),是一个 struct 而不是 class,而对应的有 NS 前缀的将是引用类型(reference type),是一个 class 而不是一个 struct。虽然我很想吐槽用 NS 这个前缀表示区分,但至少在标准库层面,这玩意得到了一个统一。

本提议的状态是被 Swift 3 所接受,所以几个月之后就应该可以用了,而 WWDC 应该也会有更多的信息出现。值得注意的是,Apple 为了真正将 Swift 搬到 Linux,决定把 GUI 无关的 Cocoa 的很多类全部放入一个叫做 Core Library 的东西(与 standard library 作区分),比如我上面举的这个类型。SE-0069 调整的也正是 Core Library。

不过或许有一个问题还没有解决,NSArray 是不是也要变成一个引用类型呢?难道以后我们还要继续用 NSMutableArray?从这个角度来看,虽然王垠推理全错,但还是提了一个好问题呢(光速逃)。



20、

再次赞同一下王垠,前面@黄兢成的答案已经解释的比较清楚了。但我是不赞同为了解决什么个问题,就搞出很多限制或者付出额外的不必要的代价。
很多人模糊的意识到这跟对象的分配方式有关系,比如王垠提到了带有GC的语言的做法,黄兢成也解释了在堆中分配的额外开销,不能都用GC,而是(有选择的)进行引用计数。
除了从语义上解释这个问题,另一方面swift这种无奈的选择实际上是源自c++起就引入的一个深坑,似乎到目前为止都没有引起人们的注意,最新的语言要么完全采用GC方案,要么就去兼容C++的做法并适当去增强它。或者是在语法层面上搞出一堆限制(Rust)。本质上在于C++对delete操作做出了明确的固定,这是一个错误的设计,也引发了智能指针设计上带来的系列问题。

这个问题下,很多人拿实现说话,这似乎有点搞笑了。仅仅是实现方法里用了很多的trick来弥补设计缺陷可能引起的性能缺陷,这就算是设计正确吗?一边说写复制,一边说避免了写的开销,这不是自相矛盾吗?再回到根本上,值类型表达不可写,这是个什么鬼,虽然我明白那个目的,但明显在设计上是为了目的而目的,引入不必要的概念,增加了负担。

作者:zhenghui zhou




21、

作者:黄兢成

这篇文章有些地方我认同,更多地方是不认同。

Swift 的对象,可以分为 struct 类型和 class 类型。struct 类型就是值类型,class 类型就是引用类型。

假如 array 是引用类型。这种语法应该是对的

[Swift] 纯文本查看 复制代码
let shoppingList = ["Eggs", "Milk"]
shoppingList[0] = "Salad"


但假如 array 是值类型,上面语法就是错的。Swift 也确实是这样区分的。

王垠的意见是,array 这种大数据结构,不应该采用值类型。而应该使用引用类型,而将 array 分拆成可变和不可变两个类。因为值类型的复制代价很大。

但他似乎忽略了一点,Swift 语言的 array 和 dictionay, string 这些常用的数据结构,虽然是值类型。但它在实现时,采用了写时复制。平常将 array 作为函数参数,并不会引起复制。

对于同样的问题。王垠采用 A 方案解决,保留引用类型,而将其分拆成可变不可变两个类。 而 Swift 采用了 B 方案解决,切换到值类型,使用写时复制。

Swift 的方案没有问题。

另外“swift,Rust 和 C# 的 value type 制造的更多是麻烦,而没有带来实在的性能优势。“

这句话,我不能认同,在 iOS 开发,涉及到大量的点(CGPoint), 矩形(CGRect) 等基础对象,假如这些对象也采用引用类型,而在堆中分配,是不能忍受的。使用 Objective-C 的时候,当需要存储大量点的时候,为了性能考虑,也经常会切换到使用 C++, 用 std:vector 存储。而不是用 NSArray 将点放到 NSValue 中。

Swift 继续使用 Objective-C 的引用计数,并没有一个 GC, 这种值类型和引用类型的区分,多个选择,我觉得是好事。Objective-C 以前版本是增加过 GC 的,后来实践发现 GC 的版本会慢,就将 GC 去掉了。现在 Swift 没有 GC, 采用 ARC 管理内存。

---------
回答中,我最开始说 Swift 1.0 中 array 是引用类型,Swift 2.0 才切换到值类型。查了一下资料,似乎 Swift 的 array 从一开始,就是值类型。为避免误导,特定指出,也将之前可能出错的语句删掉了。




22、

敏捷大拇指上的诸位Swift牛人,大家也说说吧~

swifter 发表于 2016-10-28 06:13:43 | 显示全部楼层

在全球最大的Swift开发者社区敏捷大拇指Swifthumb.com)看到《王垠:Swift语言的设计错误》最新的评论。有意思,我个人的看法是:Swift是在发展中的,有问题很正常,提意见也很好,不然怎么改进嘛。下面这篇文章的内容也涉及Swift语言的设计,欢迎讨论大拇指上的各位Swift开发者热烈讨论。毕竟是老外写的,Exponential time complexity in the Swift type checker,抨击就抨击他去吧,哈哈。

大家不要像抨击《王垠:Swift语言的设计错误》一样来抨击我哈。

详解Swift的类型检查器 Exponential time complexity in the Swift type checker
http://www.swifthumb.com/thread-15056-1-1.html
swifter 发表于 2016-7-25 03:52:50 | 显示全部楼层
我是90后 发表于 2016-7-25 06:38:05 | 显示全部楼层
你怎么不直接给Chris发邮件、毛遂自荐呢?
136236012 发表于 2016-7-25 17:28:27 | 显示全部楼层
我是90后 发表于 2016-7-25 06:38
你怎么不直接给Chris发邮件、毛遂自荐呢?

你先了解下作者是谁,再评论吧!
apper 发表于 2016-7-25 23:12:50 | 显示全部楼层
136236012 发表于 2016-7-25 17:28
你先了解下作者是谁,再评论吧!

你认识作者?
我是90后 发表于 2016-10-27 23:54:53 | 显示全部楼层
136236012 发表于 2016-7-25 17:28
你先了解下作者是谁,再评论吧!

谁呢?
女汉子 发表于 2016-10-28 05:14:39 | 显示全部楼层
一正一反 这种才科学~
死神来了 发表于 2016-10-28 07:24:31 | 显示全部楼层

现在明白了,他写了个Yin语言,然后没然后了。。。。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

做任务,领红包。
我要发帖

分享扩散

都看到这里了,就把这资料推荐给您的好朋友吧,让他们也感受一下。
您的每一位朋友访问此永久链接后,您都将获得相应的金钱积分奖励
热门推荐

合作伙伴

Swift小苹果

  • 北京治世天下科技有限公司
  • ©2014-2016 敏捷大拇指
  • 京ICP备14029482号
  • Powered by Discuz! X3.1 Licensed
  • swifthumb Wechat Code
  •   
快速回复 返回顶部 返回列表