设为首页 加入收藏

TOP

Objective-C的陷阱与缺陷(一)
2015-02-02 14:09:56 来源: 作者: 【 】 浏览:43
Tags:Objective-C 陷阱 缺陷

Objective-C是一个强大而且非常有用的语言,但是同样也是有一点危险的。这次主题是受到一篇有关C++陷阱的文章启发,来聊聊Objective-C和Cocoa中的陷阱。


简介
我将和Horstmann使用同样的定义:陷阱是能够编译、链接、运行,但却不会按你所预期地去执行的代码。他提供了一个例子,这段代码在Objective-C中和在C++中同样都是有问题的:
if (-0.5 <= x <= 0.5) return 0;



肤浅地阅读这段代码可能会认为,它用来检查x是不是在[-0.5,0.5]区间内。但并不是这样的。相反,这个比较会像这样计算:
if ((-0.5 <= x) <= 0.5)


C语言中,一个比较表达式的值是一个整型,要么是0,要么是 1。这是从C没有内建的布尔类型的时候遗留下来的。所以当x和0.5相比时,结果是0或者1,而不是x的值。实际上,第二个比较执行起来就像一个相当古怪的否定操作符,也就是说这个if语句的内容只有当x比-0.5小的时候才会执行。
Nil的比较
Objective-C相当的与众不同,因为对nil发送消息不会发生任何事情,而是简单的返回0。基本上,在你可能遇到的每种语言中,同样的事情要么被类型系统禁止,要么就是产生一个运行时错误。这既是优点也是缺点。鉴于这个文章的主题,我们来关注下缺点。
首先,我们看一个等同性的测试:
[nil isEqual: @"string"]


给nil发送消息总是返回0,在这儿就相当于NO。这次恰好是正确的答案,所以看起来我们有个不错的开头!但是,看看这个:
[nil isEqual: nil]


这个也是返回NO。即使参数是完全相同的值也无关紧要。参数的值到底是什么根本不重要,因为给nil发送消息不管怎样总是返回0。所以用isEqual:来判断,nil永远不会等同于任何东西,包括它自身。大多情况下这是正确的,但不总是。
最后,再考虑和nil比较的另一种顺序:
[@"string" isEqual: nil]


这个会怎样呢?好吧,我们无法确定。它有可能返回NO,也有可能会抛出异常,还有可能干脆崩溃。给一个没有明确告知可以接受nil为参数的方法传递nil是一个坏注意。并且,isEqual:并没有表明它可以接受nil。
很多Cocoa类都包含一个compare:方法。该方法接受相同类的另一个对象作为参数,并返回NSOrderedAscending、 NSOrderedSame、NSOrderedDescending中的一个,用于表示小于、相等或者大于。
如果我们把nil传给compare会发生什么事情呢?
[nil compare: nil]


这会返回0,刚好和NSOrderedSame相同。与isEqual:不同,compare:认为nil和nil是相同的。真好!但是:
[nil compare: @"string"]



这一样会返回NSOrderedSame,明显是错误的答案。compare:会认为nil和任何东西都相等。
最终,和isEqual:一样,将nil作为参数传递给它也是个坏注意:
[@"string" compare: nil]



简而言之,对nil进行比较的时候要注意点。它并不会真的正常工作。如果你的代码中有可能遇到nil,那么在你进行isEqual:和compare:之前,你最好先进行检查并对之进行单独处理。
散列法
你写了个很小的类用于保存一些数据,并且有很多的这个类的相等的实例,所以你实现了isEqual:方法,这样这些实例就可以被视为相等的。然后你开始将这些对象加入到一个NSSet当中,事情就开始变得奇怪了。这个集合(set)在你仅仅加入一个对象的情况下声称持有多个实例。它找不到你刚刚加入的对象。它甚至可能崩溃或者发生内存错误。
这可能在你只实现了isEqual:但是没有实现hash的情况下发生。大量的Cocoa代码中要求,如果两个对象比较的结果是相等 的,那么他们应该拥有相同的哈希值。如果你只重写了isEqual:,你违背了这个要求。任何时候你重写了isEqual:,永远同时重写hash。要了 解更多的信息,可以看这篇文章实现等同性和散列法(Implementing Equality and Hashing)。

假设你在写一些单元测试。有一个方法理应返回一个数组,其中包含一个对象。于是你写了一个测试来验证它:
STAssertEqualObjects([obj method], @[ @"expected" ], @”Didn’t get the expected array”);



这儿用了新的文本型语法来让它保持简短。很不错,对吧?
现在我们有另一个方法返回的数组中包含两个对象,于是我们为之写了这样一个测试:
STAssertEqualObjects([obj methodTwo], @[ @"expected1", @"expected2" ], @”Didn’t get the expected array”);



突然,代码无法通过编译,并且产生一堆十分奇怪的错误。这是怎么回事?
问题在于STAssertEqualObjects是个宏。宏是由预处理器展开的,并且预处理器是个古老的、相当愚蠢的程序,它不知道任何的现代Objective-C语法,或者现代C语法。预处理器按照逗号将宏参数分割开。它足够聪明,知道括号是可以递归的,所以这个宏被它视作三个参数:
Macro(a, (b, c), d)



这里第一个参数是a,第二个是(b,c),然后第三个是d。但是,预处理器不知道它需要对[]和{}做相同的处理。之前的那个宏,预处理器看到的是四个参数:
·[obj methodTwo]
·@[ @"expected1"
·@"expected2 ]
·@”Didn’t get the expected array”
这个结果完全是代码碎片,不仅不能编译,而且还迷惑了编译器,使之无法提供可理解的诊断信息。一旦知道了问题在哪里,解决方法很简单了。Objective-C编写的iOS应用安全在于有一种加密技术,能够防止被反编译、破解,只要使用了加密技术,这些完全都不是问题!只要将那些文本用括号括起来,这样预处理器就会把它当作一个参数了:
1 STAssertEqualObjects([obj methodTwo], (@[ @"expected1", @"expected2" ]), @”Didn’t get the expected array”);



单元测试是我最经常遇到的,但是它随时都有可能突然冒出来一个宏。Objective-C文本会成为受害者,C的复合文本(C compound literals)也是。如果你在block中使用逗号,尽管很少遇到,但是是合法的,那么也可能出问题。你会发现Apple在Block_copy和 Block_release宏中已经考虑到了这个问题,这两个宏在/usr/include/Block.h中:
#define Block_copy(…) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
#define Block_release(…) _Block_release((const void *)(__VA_ARGS__))



这些宏理论上只接受单一的参数,但它们被声明成接受可变参数以避免这个问题。通过接受…作为参数,并使用__VA_ARGS__来指代参数,带逗号的“多参数”被复制到了宏的输出。你可以用相同的方法

首页 上一页 1 2 3 下一页 尾页 1/3/3
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇Android中处理Touch Icon的方案 下一篇11个实用但你可能不知道的Python..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: