ObjC/Swift Swift 和 Ruby 差别还是挺大的...

luikore · 2014年06月04日 · 最后由 luikore 回复于 2014年07月22日 · 21933 次阅读
本帖已被管理员设置为精华贴

这里讲的不是语法层面,而是运行时层面

区别一:Swift 是支持泛型的静态类型

类型系统上应该说不支持泛型的 OC 和 Ruby 还比较接近,Swift 区别就有点大了。

比方说数组类型,Swift 中 [1, 2, 3] 的类型是 Int[], ["foo", "bar"] 的类型是 String[], Ruby 的数组在 Swift 中大概相当于 Any[] 或者 var x = [] // 数组类型变成了 NSArray .

由于泛型数组的类型在编译时已经确定,所以可以比多态类型的数组更能发掘性能潜力,RC4 的 Swift 实现比 OC 实现速度快的原因就在于此。

泛型约束可以极度增加函数签名的长度...

区别二:nil 对象

Swift 默认禁止 nil Object, 只有在类型上加问号 (Optional type) 才可以成为 nil.

var s: String = nil // 错误
var b: String? = nil
b?.isEmpty // nil 值链传递
b!               // 取出了 nil 值
b.isEmpty  // 错误, 你必须用 ! 拆箱或者用 ?. 去做链式调用

区别三:对象系统

Swift 是面向类的语言,对象成员表是编译时决定的。

Swift 和 Ruby 同样是 单继承,和 Ruby 可以 include 任意多个 module 相比,Swift 可以添加任意多个 protocol 和 extension.

构造方法 init 的规则有点复杂。Swift 里可以定义两种构造方法:designated (写法是:init(...) { ... }) 和 convenient (写法是 convenient init(...) { ... })

convenient 构造方法的常见作用是提供某些默认值,在 convenient 构造方法中,是必须调用 designated 构造方法的。还有个限制是子类中不能调用超类的 convenient 构造方法。

初始化方法的继承有两个规则:

  1. 如果子类没有定义 designated 构造方法,那子类可以调用所有超类的 designated 构造方法。
  2. 如果子类提供了与超类的所有 designated 构造方法签名相同的方法 (可以是 designated 或者 convenience 的,也可以是规则 1 继承的), 那子类可以调用所有超类的 convenience 构造方法。

对象成员的初始化分为 2 阶段,第一阶段是放默认值 (成员声明中的 var v = 默认值), 第二阶段是执行 init 方法的赋值。初始化代码中有 4 个编译时的 safety check, 如果访问顺序不满足 4 个条件,就会报错。和 Java 完全相反:Swift 是子类成员先赋值,超类成员后赋值 -- 估计这个顺序规则有利于提高性能。(从实现角度看,我猜想 Swift 对象的内存布局可能会类似下面这样,既方便实现 extension method, 又方便 init 方法的优化,没看过内存里什么样子,扯远了...)

struct Object {
  pointer klass;
  pointer super_klass_object;
  ... // members defined in child object
}

成员的默认值其实可以用比较复杂的代码设置,例如

class A {
  var x = {
    ...
  }()
}

有些成员可以用 @lazy 修饰,到用的时候才初始化 ( lazy 成员才能在初始化块中引用 self)... 所以太滥用成员的初始化机制还是挺容易产生坑的... Swift 对象系统还挺复杂难掌握的... 还好报错信息都非常人性化,很容易发现问题在哪里。

区别四:传值类型和 inout

区别于 class, 用 struct 定义的类型都是传值类型,不用担心这类对象内存管理的问题 (下面会提到).

Swift 还有一种参数属性是 inout, 使得函数中可以改变参数的值。不过可以返回 tuple 了我觉得挺多余的,这到底是为了提高性能还是吸引 C# 程序员?

区别五:内存管理

Swift 的运行时和 OC 的运行时有很大的重合,它也是基于自动引用计数而不是 GC 的,所以还是要和 OC 一样,用 weak var 来避免循环引用内存不能释放的问题。如果要把 closure 中的强引用改成弱引用,就要用和 C++1x 相似的方括号语法和 unowned 关键字指定 capture list, 例如

someClosure = {
  [unowned foo] (foo: Foo) -> Bar in
  ...
}

区别六:Immutable 对象

let 声明的变量往往会让人想起 Ruby 中的常量,但用 let 对象不但不可以改变值,还不可以改变它的内部状态

let x: Int[] = []
x.append(3)  // can not modify immutable

var x: Int[] = []
x.append(3) // ok

容易坑的地方

下面代码可以看到 a 和 b 指向的是同一个数组:

a = [1]
b = a
a[0] = 3
b[0] // 3

但是当数组长度发生变化时,就坑爹了...

a = [1]
b = a
a.append(2)
a[0] = 3
b[0] // 1

所以数组是个 copy on extension 的东西,用的时候要多加注意... 用意为何没弄明白,难道是并发环境下数组内存重分配的考虑?还在在学 golang 的 slice?? 预测将来要么这个行为会改掉,要么向用户解释十几年...

反射除了 object is Klassreflect(object) 以外,都用的 objc 的反射机制,所以还得回想 objc 的 selector ...

如果想获得 class 对象,需要自己包装一个调用 [object class] 的函数暴露给 swift

"".respondsToSelector("respondsToSelector:") // true

少数函数如 CGColorGetRandomColor 返回对象是没有被 ARC managed 的,需要手动 retain 和 release, 参见 Unmanaged 的文档 ...

和 OC / C 交互

Swift 比引入 Ruby Framework 的优点之一是语言内建了方便交互的桥梁。

在 Swift 项目中加个 .m 文件或者在 OC 项目中加个 .swift 文件,XCode 就会问你要不要加个 项目名-Bridging-Header.h, 然后你把相应的头文件加进去就可以在 Swift 中使用了。

例如 OC 用 NS_ENUM 定义的枚举类型在 Swift 中就可以直接用:

typedef NS_ENUM(NSInteger, MyEnum) { FOO = 1, BAR };

swift:

var e: MyEnum = MyEnum.BAR

如果没用 NE_ENUM 定义,也能用,只是相当于整数类型了。不过好处是范围依然有检查:

typedef enum {FOO = 1, BAR} MyEnum;

swift:

var e: MyEnum = 2
e = 3 // 错误, 超出范围了

简化的 CF API

  • Swift 数组有定型 native 形式和 NSArray 形式,当用 native 形式时,可以直接赋值给 CFArrayRef 而不用像 OC 那样 __bridge
  • create ... release 的内容,在 swift 里都可以省掉 release 了
  • CFMake* 都映射成了短很多的结构体构造函数,并且是带命名参数的
  • 有很多很长前缀的枚举类型,在 Swift 中都直接可以 . + 后缀使用

例如 Swift Interoperability In Depth 中提到的:

func drawGradientRect(context: CGContext, startColor: CGColor, endColor: CGColor, width: CGFloat, height: CGFloat) {
    let colorSpace = CGColorSpaceCreateDeviceRGB() // 不用手动 CGColorSpaceRelease 了
    let gradient = CGGradientCreateWithColors(colorSpace, [startColor, endColor], [0.0, 1.0]) // 不用 __bridge
    let startPoint = CGPoint(x: width / 2, y: 0) // 代替 Make 系列函数
    let endPoint = CGPoint(x: width / 2, y: height)
    CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0)
}

特色 feature

整数类型 Int 在 64 位系统相当于 Int64, 在 32 位系统相当 Int32.

为了解决溢出问题,Swift 的运算符默认检查溢出,但是前面加了 & 的话就不检查 (例如 &+, &*, &% ...).

Swift 还可以声明 +=, ||= 等赋值操作符,估计将来可以写原子性的检查 - 赋值语句,但是貌似还不支持 objc 中的 @synchronized, @atomic 等 attributes.

playground 只能用系统已经提供的库,暂时还不能引入项目中内容或者第三方库

ArrayDictionary 是 structure……NSArray 是 class……可以安全的 append……

感觉最爽的地方还是很像 Ruby 的 extension 机制……碉堡的 DSL 指日可待。。。

#1 楼 @Kabie NSArray 不能 append, NSMutableArray 才可以 能否修改内部状态和 struct 无关,structure 也可以是 mutable 的

吕大 干的棒。。。。。

Swift 是编译型语言,跟动态语言千差万别

正在看 Swift Tour,确实简洁了不少~

确实,容易坑的地方感觉一样;swift 里面分了 Value Type 和 Reference Type,但是在 Array 和 Dictionary 的 Assignment 和 Copy Behavior 上确有些奇怪的地方;它的 Language Guide 中有单独一节来描述“Assignment and Copy Behavior for Dictionaries”和“Assignment and Copy Behavior for Arrays”,看的时候被绕晕了。。。。

https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ClassesAndStructures.html#//apple_ref/doc/uid/TP40014097-CH13-XID_94

原文描述是这样的:

var a = [1, 2, 3]
var b = a
var c = a

You can retrieve the first value in the array with subscript syntax on either a, b, or c:

println(a[0])
// 1
println(b[0])
// 1
println(c[0])
// 1

If you set an item in the array to a new value with subscript syntax, all three of a, b, and c will return the new value. Note that the array is not copied when you set a new value with subscript syntax, because setting a single value with subscript syntax does not have the potential to change the array’s length:

a[0] = 42
println(a[0])
// 42
println(b[0])
// 42
println(c[0])
// 42

However, if you append a new item to a, you do modify the array’s length. This prompts Swift to create a new copy of the array at the point that you append the new value. Henceforth, a is a separate, independent copy of the array.

If you change a value in a after the copy is made, a will return a different value from b and c, which both still reference the original array contents from before the copy took place:

a.append(4)
a[0] = 777
println(a[0])
// 777
println(b[0])
// 42
println(c[0])
// 42

至于为什么确实也没讲 @luikore

#8 楼 @donnior 如果用 NSMutableArray 和 NSMutableDictionary, 行为和 objc 是一样的。但如果用泛型数组和泛型字典,就是 copy-on-extension 了...

var a = [:] as NSMutableDictionary
var b = a
b["foo"] = "bar"
a["foo"] // "bar"

#2 楼 @luikore 哦……这样……我本来以为 struct 是传值的所以会有问题……

试了一下发现所谓的 mutaing 和我的理解还不太一样。。。

Swift 的 Runtime 应该也会在正式发布的时候开放源码出来。

匿名 #12 2014年06月05日

Swift 还有一种参数属性是 inout, 使得函数中可以改变参数的值。不过可以返回 tuple 了我觉得挺多余的,这到底是为了提高性能还是吸引 C# 程序员?

这个的存在是因为本来 Objetive-C 里面就经常有处理错误之类的代码(其实在 C 里面也算常见),这样写:

NSError *e = nil;
[[NSFileManager defaultManager] moveItemAtPath:sourcePath toPath:targetPath error:&e];

NSFileManager#moveItemAtPath:toPath:error: 这个方法会在错误发生时设置 e 的值。

#12 楼 @dorentus 明白了,可以兼容 OC 的现存 API

吕大神,牛逼

安装完 10.10 之后 ruby 版本变成了 2.0,之前装的 homebrew 用不了了,有没有高手指点下

#15 楼 @lchenl cd /usr/local && git pull --rebase ?

#8 楼 @donnior 文档不是说了么,一旦你在一个数组上执行 append,系统就会分配一个新的内存空间,创建一个新的数组,然后把数据都 copy 过去,最后将新数组的指针指向那个消息发送者 "a",然后你改变 "a" 的值就已经不影响 b,c 了,因为 b,c 还指向原有的数组内存。

与 Array 类似的情况

var a = "abc"
var b = a

a = a + "d" // "abcd"
b // "abc"
a // "abcd"

#18 楼 @_kaichen 不同,这个在 java 和 ruby 都存在。 swift 的 immutable collection 以后应该会改用方法来更新然后返回新的容器,否则太恶心了。

a = [1]
b = a
a[0] = 3
b[0] // can not modify immutable
a.update(0, 3) // new collection

#18 楼 @_kaichen 赋值运算符改变了变量名指向的实例很正常,关键是一个 append 方法改变了指向的实例 ... 把 append 改成 <<= 运算符就更直观点

#20 楼 @luikore 吕老板,你是我心中的 Programming Languist.

围观。。。

23 楼 已删除

swift 和 ruby 一点都不像,ruby 是脚本,带虚拟机的,swift 是编译型的,和 rust 挺像似,如果你看过 rust 文档的话,和 go 有几分类似。

本来就不太喜欢 swift,现在有点讨厌它了

#25 楼 @fsword swift 已经比 OC 好了,而且可以做很多高层次的优化,OC 的一些代码是没 swift 快的... 而且 OC 语法已经没办法救了,ObjC++ 完全是个怪物... swift 相对于 OC 可以看作 coffeescript 之于 javascript, F# 之于 C#, Elixir 之于 Erlang 的程度,多好

27 楼 已删除

#27 楼 @sefier 自动引用计数不是 GC 的策略,自动引用计数是在编译的时候对每个对象设一个值,然后在语义阶段追踪这个对象的生命周期,然后在这个对象的最后一次调用时,编译器自己打上这个对象的 release。

相当于一个老程序员一行行的检查个小白的代码,耐心的帮他加上释放内存的语句。

而 GC 是先让这些跑起来,在运行时一个个对象的标记,待内存不足或者 Timer 到点,把那些无用的对象释放掉。

自动引用计数即 ARC 不需要占据运行时时间及资源去做释放内存的事,所以高级很多,效率也高。

啊。。。为什么我辛苦打了一大段这人被删除了。。。

#26 楼 @luikore 最后一句不太准确吧?是说语法层面?

#29 楼 @WolfLee 是语法层面啊,运行时几乎一样的,Swift 编译的程序可以运行在以往的 Mac 和 iOS 上,就是因为基本用的 OC 运行时

#30 楼 @luikore 还是觉得应该把 coffeescript -> javascript 这个类比去掉 ...

应该是 convenient init() { ... } ,convenient init() 没有参数

需要 登录 后方可回复, 如果你还没有账号请 注册新账号