有趣的Swift特性

转自 http://www.cocoachina.com/industry/20140703/9019.html 
原文: https://mikeash.com/pyblog/friday-qa-2014-06-20-interesting-swift-features.html   

今天,我想要开始一些事情,探索几个Swift的特性,其从Objective-C的角度看是令人感兴趣的并且不寻常的,并且我将会思考他们会有什么好处。
 
显式可变值
Swift 使一些可变值作为一个优秀语言的构成,并且使任何可变类型像这样被标识。例如:
  
  
  
  
  1. var a: Int 
  2. var b: Int? 
在这里,a是一个显式的Int,并且它总是包含一些整型值。b是一个可变的Int,并且它也包含一个整型值,或者它什么也不包含。
 
那个例子并无特别之处,但是借助于其他的类型它会变得更有意思。
  
  
  
  
  1. var c: String 
  2. var d: String? 
像上面这样,c是一个简单的String,并且它总是包含一些字符串值。d 是一个可变的String,并且它也可能包含一个字符串值或者可能什么也不包括。
 
让我们看看Objective-C 中的等价的声明:
  
  
  
  
  1. int e; 
  2. NSString *f; 
这里,e是一个明显的整型,并且它总是包含一些整型值。f 是一个NSString *,这就意味着它也可以包括一个指向NSString的指针,或者它可能包括nil,这就通常意味着"nothing"。
 
注意:e是一个非可变类型,而f是有效的可变类型。NULL在c中的存在(和Objective-C中的nil一样)意味着语言中的全部的指针类型是隐式的可变类型。如果你有一个 NSString *,就意味着”指向 NSString 的指针,或者是nil”。如果你有一个char *,就意味着“指向char 的指针,或者NULL”.
 
C中存在的NULL在成为指针时隔开整个静态系统。每一个指针类型都是half truth类型的。无论何时你看到一个类型”指向X的指针”,那总是一个含蓄的表述”指向X的指针或者为NULL”。
 
NULL 行为完全不同。你可以解引用一个指针,但是你不能解引用NULL。Objective-C中你将给它发送消息,但是返回值总是0,甚至是没有意义的。
  
  
  
  
  1. [nil isEqual: nil] // returns NO! 
记录一个nil字符串,你会得到null。[nil arrayByAppendingArray: obj]不会产生一个包括单元素obj而不是nil的数组。所有都有两个不同的行为:常规的一个,以及nil或者NULL值。
 
这是一个主要的问题因为我们的代码不是那种方式运行。有时候,我们写我们的代码“指向X或者NULL的指针”,但是有时我们仅仅写“指向X的指针”。但是无法在语言中表达这个字母类型。
 
为了说明这个问题,考虑如下两个问题:
1.nil 是一个传递给isEqual:的合法参数吗?
2.nil 是一个传递给isEqualToString:的合法参数吗?
 
我将会在你检查文档的时候给你一点时间。
 
第一个问题是“yes”。文档写道:“在这个方法返回NO的时候或许是nil。”
 
第二个问题是...no吗?可能吗?文档没说明。最好的假设如果nil不是明确被允许,那么它就不合法。
 
方法参数有时候可以为nil,有时候不行。返回值也是同样。一些方法从不返回nil,然而一些返回。如果一个方法返回nil并且你传递返回值给一个不接受nil的方法,问题就来了。你必须增加校验,并且编译器不会帮助你,就像编译器知道的那样,一切接受nil。甚至如果你可以掌控一切,文档经常掩盖nil处理。
 
这有另一个要说明的问题:这个代码合法吗?
  
  
  
  
  1. free(NULL); 
在我的经验中,大概99% 的C 和Objective-C程序员将会说”no”。通常这样校验:
  
  
  
  
  1. if(ptr) 
  2.     free(ptr); 
但是如果你检查代码,它是好的。 free()函数释放ptr指向的内存。如果ptr是一个NULL指针,没有操作会被完成。
 
如果人们经常得到一些和这个同样简单的错误,我们还有什么希望在更复杂的情况下。
 
Swift 在一个一致性模式下解决这个问题,方法是借助于产生所有的非可变类型并且允许任何类型变成可变的。
 
对于Objective-C里指针类型,这样解决了由nil和NULL导致的不一致。如果一个参数可以接受nil,那么它将会被声明成一个可变类型。反之不可以。编译器可以轻松检查你的代码以确保它做正确的事情。同样的,如果一个返回值可以为nil,很明显它将会是一个可变类型。通常情况下,你的类型将会是不可变的,让它清空它曾经持有的东西。
 
因为任何类型可能成为可变类型,这也将解决一些别的问题。框架中的许多方法返回指定的标记值用来标识一个返回值。例如indexOfObject: 返回NSNotFound,仅仅是NSIntegerMax的别名。这个很容易忘记:
  
  
  
  
  1. NSUInteger index = [array indexOfObject: obj]; 
  2. [array removeObjectAtIndex: index]; // oops 
 
Swift 中等价的代码将会返回一个可变类型:
  
  
  
  
  1. func indexOfObject(obj: T) -> Int? 
没有找到对象将会返回一个非整数值,并且事实上可变类型是一个不同的类型,意味这编译器可以帮你检查一切。
 
多个返回值
Swift 允许一个函数返回多个值。或者如果你以一个不同的方式看到它,Swift 函数总是返回一个值,但是那个值可能是一个容易得到的数组。
 
C 和Objective-C用两种方式支持多值,都不太棒。函数/方法可以返回一个包含多个值的结构体或者类,或者他们可以使用外部参数。返回一个结构体是如此笨重,所以基本上从来不会被使用,除非结构体理论上是一个单独的单位,像frame返回一个NSRect 或者CGRect。外部的参数在Cocoa中用的很多,虽然,特别为了错误句柄而设计。
 
NSError 类和对应的NSError ** 模式在10.2时代被展现并且快速普及。一些语言抛出异常,Cocoa 把一个NSError * 通过参数传递给调用者来指出错误。像这样的代码是很常见的:
  
  
  
  
  1. NSError *error; 
  2. NSData *data = [NSData dataWithContentsOfFile: file options: 0 error: &error]; 
  3. if(data == nil) { 
  4.     // handle error 
这个将会变得麻烦,并且错误总是可变的,许多代码将会像这样替代:
  
  
  
  
  1. NSData *data = [NSData dataWithContentsOfFile: file options: 0 error: NULL]; 
这有点糟糕,但是有吸引力。
 
多个返回值显示除了异常和外部指针外的另一个选项。在Python 中,例如,代码将会看起来这样:
  
  
  
  
  1. data, error = NSData.dataWithContentsOfFile_options_error_(file, 0, None) 
  2. if not data: 
  3.     # handle error 
因为桥接,这变的有点奇怪,你将会必须要传递NONE作为外部参数,甚至它正被转换成第二个返回值。一个本地的Python调用可能看起来像这样: 
  
  
  
  
  1. data, error = Data.dataWithContentsOfFile(file, 0) 
Swift 版本看起来几乎一样的:
  
  
  
  
  1. let (data, error) = Data.dataWithContentsOfFile(file, 0) 
这是一件小事,但是NSError返回在Cocoa 中是很常见的,使一切变得更友好。问题已经足够困扰我,我已经提交 preprocessor crimes against humanity,尝试在 Objective-C中构建多返回值,并且我不需要做其他的任何事情。(我认为一个好的情况可能发生,用一个 可选的类型返回错误。在任何情况下,我期望研究哪些选项。)
 
泛型
这是重要的一点。Swift 支持泛型类和函数。
 
这打开了许多可能性。一个是容器类现在可能有一个静态类型,在编译阶段,声明他们包括的类型。在Objective-C中,一旦对象进入一个容器,你便失去了静态类型信息。
  
  
  
  
  1. NSArray *array = @[ @"a", @"b", @"c" ]; 
我们能够处理这种情况,但它不是最好的。由于一件事情,它完全终止了点语法:
  
  
  
  
  1. NSLog(@"%@", array[0].uppercaseString); 
这句编译失败,因为 array[0] 的类型是id并且没有使用点语法。
 
另一点,它努力保持对东西内部的追踪。这并不是一个大的对于你设置并立刻使用本地变量的处理。对于一个小的实例变量它可能有点小痛苦,并且可能真的让你因为参数和返回值而烦恼。当你失败并且得到错误类型,结果是一个运行时错误。
 
对于字典来说,它变得更糟糕,大概有两种类型错误。我有很多名字像_idStringToHamSandwichMap的实例变量,用来帮助我记住键值是NSString 实例以及HamSandwich实例的值。
 
在Swift中,类型不是数组和字典,而是Array<String> 和Dictionary<String, HamSandwich>。当你从数组中获得一个元素的时候,结果不是id (或者Swift中Any/AnyObject对象)。它也连接的很好,所以如果你为了一个所有它的值的列表请求Dictionary<String, HamSandwich> ,结果是一个集合类型,其包括Dictionary<String, HamSandwich> 实例。
 
函数可能也是泛型的。这让你写操作任何类型的函数,而不需要在你传递他们的时候丢失类型信息。例如,你可以用 Objective-C写一个小的帮助函数用来提供一个默认值,在值为nil的时候用。
  
  
  
  
  1. id DefaultNil(id first, id fallback) { 
  2.     if(first) return first; 
  3.     return fallback; 
让我们忽略了一会不规范?:操作符提供这个精确的行为。
 
这个工作是不错的,但是丢失了类型信息,以致于不能运行。
  
  
  
  
  1. NSLog(@"%@", DefaultNil(myName, genericName).uppercaseString); 
通过使用macros和非标准的__typeof__ 操作符有可能变得更好,但是它会变的惊人的快速。
 
Swift中等价的代码看起来将是像这样的:
  
  
  
  
  1. func DefaultNil(first: AnyObject?, fallback: AnyObject) -> AnyObject { 
  2.     if first { return first! } 
  3.     return fallback 
Swift 的严格的静态类型会使得使用这个变得痛苦,比起Objective-C更是如此,其丢失了类型信息,但是基本上还是信任我们所说的类型。然而,借助于泛型,我们可以轻易解决这个问题。
  
  
  
  
  1. func DefaultNil<T>(first: T?, fallback: T) -> T { 
  2.     if first { return first! } 
  3.     return fallback 
这样保存类型以便于返回值有同样的参数类型。类型推断意味着你甚至不需要告诉它要使用的类型,它仅仅知道参数类型即可。
 
泛型是另一个我的 preprocessor crimes against humanity 的例子,尝试将他们嵌入到Objective-C中,并且我为能够真正有这些特性而高兴。
 
类型推断
静态输入是不错的,但是它可能在代码中引起多余的困恼:
  
  
  
  
  1. NSString *string = @"hello"
  2. UIAlertView *alert = [[UIAlertView alloc] initWithTitle...]; 
编译器知道这些表述内容的类型,所以为什么我们必须每次都重复那些信息?有了Swift,我们可以不用重复:
  
  
  
  
  1. let string = "hello" 
  2. let alert = UIAlertView(...) 
尽管对于像JavaScript这样的语言从表面看,有基本的类型变量,仅仅是类型不作为声明的一部分被写出来。如果有一种情况,你真正想要确保那个变量是一个确定的类型(如果初始化不匹配,会产生一个编译器错误),你仍然可以声明它:
  
  
  
  
  1. let string: String = ThisMustReturnAString() 
编程冗长从某种程度来说可能是好事,有助于提高可读性。但是Objective-C 和Cocoa 可能将其带入一个荒谬的极端,如果有选项用来减少倒是不错的。
 
Class-like结构体
像Objective-C一样,Swift有类和结构体。和Objective-C不同的是,Swift 结构体可以包括方法。代替一个大规模的用来操作结构体的函数集,一个Swift 结构体可能仅仅包括作为方法的代码。CGRectGetMaxX(rect) 可以变成 rect.maxX.
 
该语言也提供一个默认的初始化方法(如果你不提供一个),这可以省去你为了初始化而编写代码的麻烦,并且摆脱像CGRectMake这样难看的函数。
 
此外,结构体被更好地整合进该语言。在 Objective-C中,在结构体和语言之间有一个难看的分隔。声明语法是不同的,你不能在没有装箱的情况下借助于Cocoa 集合使用他们,并且在用ARC的时候,将Objective-C对象作为结构体的成员会变得极其不方便。
 
在Swift中,声明语法是一样的。因为相应的集合使用泛型,他们没有包含结构体的问题。对象成员执行起来也不困难。他们基本变成按值传递而不是一个完全不同的对象。
 
结果是小的模型类在Swift中变得更好。你曾经多少次借助于几个固定的键值来代表一个相关的本应该是一个类的数据集?如果你喜欢我,那么答案就是“太多了”。那是因为生成一个简单的Objective-C类有点小小的痛苦。在ARC之前这是特别真实的,但是甚至使用ARC,它也是很烦人的。你仅仅可以把属性扔给一个类并且调用它一天,当然:
  
  
  
  
  1. @interface Person : NSObject 
  2. @property (copy) NSString *name; 
  3. @property NSUInteger age; 
  4. @property (copy) NSString *phoneNumber; 
  5. @end 
  6. @implementation Person 
  7. @end 
但是,现在它是可变的,所以你必须希望没人决定开始改变他们。并且没有初始化,所以你必须通过几行代码设置实例变量,希望你别忘记他们。如果你想要一个好的不可变的模型类,你必须手动写出一个初始化的函数并且保持它和相关属性的同步。
 
有了Swift,这个简单了:
  
  
  
  
  1. struct Person { 
  2.     let name: String 
  3.     let age: Int 
  4.     let phoneNumber: String 
这个自动生成的initializer和实例全是不可变的。作为奖励,一旦结构体可以潜在地被内联分配内存,而不是需要在堆上分配,它也将会更有效。
 
尾部闭包
这是一个一流的特点,虽然很少被指出但是真的很好。当一个函数或者方法的最后一个参数是闭包的时候(即一个block),Swift 允许你在调用之后写block,在括号外,两种调用是等价的:
  
  
  
  
  1. dispatch_async(queue, { 
  2.     println("dispatch!"
  3. }) 
  4.  
  5. dispatch_async(queue) { 
  6.     println("dispatch!"
 
如果闭包仅仅是一个参数,那么你甚至不需要括号:
  
  
  
  
  1. func MainThread(block: Void -> Void) { 
  2.    dispatch_async(dispatch_get_main_queue(), block) 
  3.  
  4. MainThread { 
  5.     println("hello from the main thread"
这是有意思的,因为它拆除了一个在语言编写和库调用之间的障碍。当 我在2008年讨论的时候,对于Objective-C来说blocks是一个很棒的补充,因为他们允许你写你自己的控制结构。它借助于GCD有一个大的用途,其能够提供许多有用的像语言构建那样的异步调用,但是作为库调用来实现。然而,使用Objective-C,在如何调用你写的block和如何使用内建的语言结构上仍旧有一个语法上的区别。
 
例如,Swift (至少当前)缺少像Objective-C的@synchronized语法。尾部闭包意味着你能够实现你自己的,并且使其看起来很像内建的实现方式。
  
  
  
  
  1. synchronized(object) { 
  2.     ... 
 
如果你正在用Objective-C来做这个事情,括号不能到达合适的地方,并且你略尴尬地终止一些事情。
  
  
  
  
  1. synchronized(object, ^{ 
  2.     ... 
  3. }); 
 
这是一个小事情,因为它仍然运行良好,并且易于阅读。但同能够以更自然的方式组织代码是不错的事情。
 
运算符重载
我想象这将会是有争议的。不幸的是,有很多程序员已经被C++中严重的操作符重载文化所影响。当某种语言设计者思考为了IO重载<< 和>>位转换操作符,那么你知道你已经在困境中了。我不害怕去责怪人。
 
然而,我认为如果你超越C++,情况会好得多。操作符重载存在于许多语言中,没有像C++这样疯狂滥用。
 
当人们仅仅使用任意标志来代替那些操作符,操作符重载就是有危害的。当人们使用它以至于传统的操作符能够以新的形式被用作他们传统的目的,就是一件不错的事情。
 
举一个简单的Objective-C 例子:
  
  
  
  
  1. NSNumber *count = ...; 
  2. count = @([count intValue] + 1); 
 
你可以像这样写代码,即如果数量被保存在一个字典或者一个数组中。处理装箱或者拆箱仅仅增加数目将会是一件痛苦的事情。一旦NSNumber 的值能够超过最大整型值,就会变得有点危险。你可以写一个方法完成那个事情,但是它仍然不太棒。
  
  
  
  
  1. count = [count add: @(1)]; 
 
借助于操作符重载,你能够实现 让+, +=, 和++来 做和操作一个标准整型值同样的事情。代码将会变成:
  
  
  
  
  1. NSNumber *count = ...; 
  2. count++; 
 
当然,Swift中的泛型本意是NSNumber自身通常更少有这种需要,但是有许多其他的潜在的运算符重载的需求。Swift没有内建的任意大小的整型,但是你能够自己实现一个用来完成所有的标准运算的操作符。
 
一个真正有意思的例子是建立一个自动布局约束,作为 SwiftAutoLayout的示例。这个使用操作符重载将表达式view1.al_left() == view2.al_right() * 2.0 + 10.0 转换成NSLayoutConstraint 的实例。这是一个好的方式,比苹果官方的更酷,但是成了疯狂的视觉格式的语言。
 
除了重载已有的操作符之外,Swift 允许你利用限定字符集声明全新的操作符。如果你真的想做,你可以调用<~^~>建立一个操作符并且使其做你想做的事情。看到人们这样做将会是有意思的,我们将会必须非常小心以便于让这种能力不会上瘾。
 
结论
Swift 是一种我们期待的那种用来实际做事的非常有意思的新的语言。它有许多不同于Objective-C的特点,但是我认为这些最终将会很好用。
 

你可能感兴趣的:(泛型,swift,特性,结构)