Objective-C 常见陷阱

Mike Ash Friday Q&A 中文译文:Objective-C 常见陷阱

作者 TommyWu
封面圖片: Objective-C 常见陷阱

译文 · 原文: Friday Q&A 2012-12-14: Objective-C Pitfalls · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2012-12-14-objective-c-pitfalls.html 发布:2012-12-14 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


引言

Objective-C 是一种强大且极其有用的语言,但也有一点危险。对于今天的文章,我的同事 Chris Denter 建议我谈谈 Objective-C 和 Cocoa 中的陷阱,灵感来自 Cay S. Horstmann 关于 C++ 陷阱的文章。

我将使用与 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 语言早期没有内置布尔类型(boolean type)时遗留下来的特性。这里与 0.5 进行比较的是那个 0 或 1 的值,而非 x 本身。实际上,第二个比较表达式相当于一个措辞极其诡异的取反运算符,使得 if 语句的主体仅当 x 小于 - 0.5 时才会执行。

Nil 比较 Objective-C 非常特殊的一点是,向 nil 发送消息不会执行任何操作,只会简单地返回 0。在你可能遇到的几乎任何其他编程语言中,类似的机制要么被类型系统禁止,要么会产生运行时错误。这既是优点也是缺点。鉴于本文主题,我们将着重讨论其弊端。

首先,我们来看看等值测试(equality testing):

[nil isEqual: @"string"]

向 nil 发送消息(Messaging)返回 0,在这种情况下等价于 NO。这在这里恰好是正确答案,所以我们有了一个良好的开端!然而,考虑以下情况:

[nil isEqual: nil]

这同样返回 NO。参数是完全相同的值并不重要。参数的值完全不重要,因为向 nil(空对象)发送消息总是返回 0,无论什么情况。所以根据 isEqual:(用于比较对象的方法),nil 从不等于任何东西,包括它自己。大多数时候是正确的,但并非总是如此。最后,考虑一下 nil 的另一种排列:

[@"string" isEqual: nil]

这会产生什么效果?我们无法确定。它可能返回 NO,可能抛出异常,也可能直接导致崩溃。将 nil 传递给一个没有明确允许这种做法的方法是不明智的,而 isEqual: 并未声明它接受 nil 参数。

许多 Cocoa 类还包含 compare: 方法。该方法接收同一类的另一个对象作为参数,并返回 NSOrderedAscending(升序,即小于)、NSOrderedSame(相等)或 NSOrderedDescending(降序,即大于),以指示两个对象之间的大小关系。

如果我们用 nil 进行比较会怎样?

[nil compare: nil]

该方法返回 0,恰好等于 NSOrderedSame。与 isEqual: 不同,compare: 认为 nil 等于 nil,这很方便!然而:

[nil compare: @"string"]

这同样返回 NSOrderedSame,这无疑是个错误答案。compare: 会将 nil 视为与任何事物都相等。

最后,正如 isEqual:(相等判断方法) 一样,将 nil 作为参数传递给它同样是个糟糕的主意:

[@"string" compare: nil]

简而言之,要小心 nil(空值)和比较。它真的就是无法正常工作。如果你的代码有任何可能遇到 nil,你必须在开始进行 isEqual:(相等比较方法)或 compare:(比较方法)之前,单独检查并处理它。

哈希(Hashing)你写了一个小类来包含一些数据。你有这个类的多个等价实例,因此你实现了 isEqual:,以便这些实例被视为相等。然后你开始将你的对象添加到一个 NSSet(集合类)中,事情开始变得奇怪。集合声称在你刚刚添加一个对象后持有多个对象。它找不到你刚刚添加的东西。它甚至可能崩溃或损坏内存。

如果你实现了 isEqual: 但没有实现 hash(哈希值),这就会发生。许多 Cocoa(Cocoa 框架)代码要求如果两个对象比较相等,它们也应该具有相同的哈希值。如果你只重写 isEqual:,你就违反了这个要求。任何时候你重写 isEqual:,总是同时重写 hash。有关更多信息,请参阅我的文章《实现相等性和哈希》。

宏(Macros)想象一下你正在编写一些单元测试。你有一个方法应该返回一个包含单个对象的数组,所以你编写一个测试来验证:

STAssertEqualObjects([obj method], @[ @"expected" ], @"Didn't get the expected array");

这里使用了新的字面量语法(Literals Syntax)以保持代码简洁。不错吧?

现在我们有另一个方法(method)会返回两个对象,所以我们要为它编写一个测试:

STAssertEqualObjects([obj methodTwo], @[ @"expected1", @"expected2" ], @"Didn't get the expected array");

突然之间,代码无法编译,并抛出完全莫名其妙的错误。这是怎么回事?

问题在于 STAssertEqualObjects 是一个宏(macro)。宏由预处理器(preprocessor)展开,而预处理器是一个古老且相当笨拙的程序,它对现代 Objective-C 语法一无所知,实际上对现代 C 语法也是如此。预处理器会根据逗号来分割宏的参数。它足够聪明,能识别括号可以嵌套,因此下述代码会被识别为三个参数:

Macro(a, (b, c), d)

这里第一个参数是 a,第二个是 (b, c),第三个是 d。然而预处理器并不知道应该对 []{} 执行同样的处理。使用上述宏时,预处理器会识别出四个参数:

  • [obj methodTwo]
  • @[ @"expected1"
  • @"expected2 ]
  • @"Didn't get the expected array"

这会导致完全混乱的代码,不仅无法编译,而且会让编译器陷入无法提供可理解诊断信息的困惑。解决方案很简单,一旦你明白问题所在。只需用圆括号包裹字面量,这样预处理器就会将其视为单一参数:

STAssertEqualObjects([obj methodTwo], (@[ @"expected1", @"expected2" ]), @"Didn't get the expected array");

单元测试是我最常遇到这种情况的场景,但实际上只要有宏的地方都可能出现。Objective-C 字面量(Objective-C literals)会成为受害者,C 复合字面量(compound literals)也是如此。如果在块(block)内使用逗号运算符(comma operator),也会产生问题 —— 虽然这种情况少见但语法合法。你会发现苹果在 /usr/include/Block.h 中的 Block_copy 和 Block_release 宏已考虑到这个问题:

#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))

这些宏在概念上只接受一个参数,但为了规避此问题,它们被声明为接受可变参数(variable arguments)。通过接受...并使用__VA_ARGS__来指代” 该参数”,带有逗号的多个” 参数” 能在宏的输出中被重现。你可以采用同样的方法来确保自己的宏不受此问题影响,不过该方法仅对多参数宏的最后一个参数有效。

属性合成
考虑以下类定义:

@interface MyClass : NSObject {
NSString *_myIvar;
}
@property (copy) NSString *myIvar;
@end
@implementation MyClass
@synthesize myIvar;
@end

这样做没什么问题,对吧?在现代开发中,实例变量(ivar)声明和 @synthesize 虽然有些冗余,但不会造成伤害。

遗憾的是,这段代码会静默忽略 _myIvar,并合成一个名为 myIvar 的新变量 —— 没有前导下划线。如果有代码直接使用该实例变量,它看到的值将与通过属性访问的值不同。这就造成混乱了!

@synthesize 的变量命名规则有点奇怪。如果你用 @synthesize myIvar = _myIvar; 指定变量名,它当然会使用你指定的名称。如果省略变量名,它会合成一个与属性同名的变量。如果完全省略 @synthesize,它会合成一个与属性同名、但带有前导下划线的变量。

除非你需要支持 32 位 Mac(译注:指旧版 32 位 macOS 系统的运行时环境),目前最佳做法是避免为属性显式声明存储实例变量。让 @synthesize 来创建变量,这样如果名称写错,你会得到一个清晰的编译器错误,而不是神秘的行为。

被中断的系统调用
Cocoa 代码通常遵循更高层级的构造,但有时也需要下沉到更底层进行一些 POSIX 操作。例如,以下代码会将一些数据写入文件描述符(file descriptor):

int fd;
NSData *data = ...;
const char *cursor = [data bytes];
NSUInteger remaining = [data length];
while(remaining > 0) {
ssize_t result = write(fd, cursor, remaining);
if(result < 0)
{
NSLog(@"Failed to write data: %s (%d)", strerror(errno), errno);
return;
}
remaining -= result;
cursor += result;
}

然而,这种做法可能会失败,而且失败的方式会很奇怪且呈间歇性。像这样的 POSIX(可移植操作系统接口)调用可能会被信号(signal)中断。即使是应用程序中其他地方处理的无害信号,如 SIGCHLD 或 SIGINFO,也可能导致这种情况。如果你使用了 NSTask 或其他方式操作子进程,就可能发生 SIGCHLD 信号中断。当 write 调用被信号中断时,它会返回 - 1 并将 errno 设置为 EINTR,以表明该调用被中断了。上述代码将所有错误都视为致命错误并会退出,尽管该调用只需要重试即可。正确的做法是单独检查这种情况,并简单地重试该调用:

while(remaining > 0) {
ssize_t result = write(fd, cursor, remaining);
if(result < 0 && errno == EINTR)
{
continue;
}
else if(result < 0)
{
NSLog(@"Failed to write data: %s (%d)", strerror(errno), errno);
return;
}
remaining -= result;
cursor += result;
}

字符串长度同一个字符串,以不同的方式表示,可能具有不同的长度。这是一个相对常见但错误的模式:

write(fd, [string UTF8String], [string length]);

问题在于,NSString 的 length 属性基于 UTF-16 码元(code units)计算长度,而 write 函数需要的是字节数。当字符串仅包含 ASCII 字符时,这两个数值恰好相等(这就是为什么人们常常侥幸写出这种错误代码),但一旦字符串包含非 ASCII 字符(如带重音的字符),它们就不再相等了。务必根据你正在操作的同一表示形式来计算长度:

const char *cStr = [string UTF8String];
write(fd, cStr, strlen(cStr));

类型转换为 BOOL 来看这段仅用于检查对象指针是否为空指针(nil)的代码:

- (BOOL)hasObject
{
return (BOOL)_object;
}

这通常有效…… 然而,大约 6% 的情况下,即使 _object 不是 nil,它也会返回 NO。怎么回事?

不幸的是,BOOL 类型并非布尔类型。它的定义如下:

typedef signed char BOOL;

这又是从 C 语言没有布尔类型那个年代遗留下来的不幸产物。Cocoa 诞生于 C99 标准引入_Bool之前,因此它将自己的” 布尔” 类型定义为signed char(有符号字符),这实际上只是一个 8 位整数。当你将一个指针强制转换为整数时,你得到的仅仅是该指针的数值。当你将一个指针强制转换为一个小整数时,你得到的只是该指针低位部分的数值。当指针看起来像这样时:

....110011001110000

BOOL(布尔类型)

得到如下结果:

01110000

这并非 0,意味着它在布尔上下文中评估为真(evaluates as true),那么问题出在哪里呢?问题出现在指针像这样的时候:

....110011000000000

然后 BOOL 获得了这个:

00000000

这里得到的是 0,也就是 NO,尽管指针并非 nil。糟糕!

这种情况发生的频率如何?BOOL 类型有 256 种可能的取值,其中只有一种对应 NO,所以我们天真地预期大约每 256 次会发生一次。然而,Objective-C 对象的分配是对齐的,通常是 16 字节对齐。这意味着指针的低四位始终为零(tagged pointers(标签指针)就是利用了这一点),因此得到的 BOOL 值实际只有四位是自由的。所有这四位都为零的概率大约是 1 / 16,或者说约为 6%。

要安全地实现这个方法,应该与 nil 进行显式比较:

- (BOOL)hasObject
{
return _object != nil;
}

如果你想显得更机灵但难以阅读,你还可以使用两次!运算符。这种!!结构有时被称为 C 语言的” 转换为布尔值” 运算符,尽管它只是由基本部件拼凑而成:

- (BOOL)hasObject
{
return !!_object;
}

第一个 ! 产生 1 或 0,取决于 _object 是否为 nil,但结果是反的。第二个 ! 然后将其纠正,如果 _object 不为 nil 则结果为 1,为 nil 则结果为 0。你可能应该坚持使用 != nil 的版本。

Missing Method Argument

假设你正在实现一个 table view data source(表视图数据源)。你将此添加到你类的 methods 中:

- (id)tableView:(NSTableView *) objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
return [dataArray objectAtIndex: rowIndex];
}

然后运行你的应用,NSTableView 会报错说你没有实现这个方法。但它明明就在那里!

一如既往,计算机是正确的。计算机是你的朋友。

仔细看看,第一个参数不见了。这居然能通过编译?

原来,Objective-C 允许存在空的 selector(选择子)段。上面的写法并没有声明一个缺少参数名的名为 tableView:objectValueForTableColumn:row: 的方法。它声明了一个名为 tableView::row: 的方法,并且第一个参数被命名为 objectValueForTableColumn。这是一种特别恼人的方法名拼写错误方式,如果你在编译器无法警告你方法缺失的上下文中犯了这个错误,你可能要花很长时间去调试它。

结论 Objective-C 和 Cocoa 有很多陷阱等着粗心的程序员。以上只是部分示例。不过,这确实是一份需要小心的事项清单。

今天的内容就到这里!下次再来看看这些怪异的建议。周五问答(Friday Q & A)是由用户的想法驱动的 —— 如果你还不知道的话,那么在下次之前,请把你的文章建议发过来!


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2012-12-14-objective-c-pitfalls.html

Objective-C is a powerful and extremely useful language, but it’s also a bit dangerous. For today’s article, my colleague Chris Denter suggested that I talk about pitfalls in Objective-C and Cocoa, inspired by Cay S. Horstmann’s article on C++ pitfalls.

IntroductionI’ll use the same definition as Horstmann: a pitfall is code that compiles, links, runs, but doesn’t do what you might expect it to. He provides this example, which is just as problematic in Objective-C as it is in C++:

if (-0.5 <= x <= 0.5) return 0;

A naive reading of this code would be that it checks to see whether x is in the range [-0.5, 0.5]. However, that’s not the case. Instead, the comparison gets evaluated like this:

if ((-0.5 <= x) <= 0.5)

In C, the value of a comparison expression is an int, either 0 or 1, a legacy from when C had no built-in boolean type. It is that 0 or 1, not the value of x, that is compared with 0.5. In effect, the second comparison works as an extremely weirdly phrased negation operator, such that the if statement’s body will execute if and only if x is less than -0.5.

Nil ComparisonObjective-C is highly unusual in that sending messages to nil does nothing and simply returns 0. In nearly every other language you’re likely to encounter, the equivalent is either prohibited by the type system or produces a runtime error. This can be both good and bad. Given the subject of the article, we’ll concentrate on the bad.

First, let’s look at equality testing:

[nil isEqual: @"string"]

Messaging nil returns 0, which in this case is equivalent to NO. That happens to be the correct answer here, so we’re off to a good start! However, consider this:

[nil isEqual: nil]

This also returns NO. It doesn’t matter that the argument is the exact same value. The argument’s value doesn’t matter at all, because messages to nil always return 0 no matter what. So going by isEqual:, nil never equals anything, including itself. Mostly right, but not always.

Finally, consider one more permutation with nil:

[@"string" isEqual: nil]

What does this do? Well, we can’t be sure. It may return NO. It may throw an exception. It may simply crash. Passing nil to a method that doesn’t explicitly say it’s allowed is a bad idea, and isEqual: doesn’t say that it accepts nil.

Many Cocoa classes also include a compare: method. This takes another object of the same class and returns either NSOrderedAscending, NSOrderedSame, or NSOrderedDescending, to indicate less than, equal, or greater than.

What happens if we compare with nil?

[nil compare: nil]

This returns 0, which happens to be equal to NSOrderedSame. Unlike isEqual:, compare: thinks nil equals nil. Handy! However:

[nil compare: @"string"]

This also returns NSOrderedSame, which is definitely the wrong answer. compare: will consider nil to be equal to anything and everything.

Finally, just like isEqual:, passing nil as the parameter is a bad idea:

[@"string" compare: nil]

In short, be careful with nil and comparisons. It really just doesn’t work right. If there’s any chance your code will encounter nil, you must check for and handle it separately before you start doing isEqual: or compare:.

HashingYou write a little class to contain some data. You have multiple equivalent instances of this class, so you implement isEqual: so that those instances will be treated as equal. Then you start adding your objects to an NSSet and things start behaving strangely. The set claims to hold multiple objects after you just added one. It can’t find stuff you just added. It may even crash or corrupt memory.

This can happen if you implement isEqual: but don’t implement hash. A lot of Cocoa code requires that if two objects compare as equal, they will also have the same hash. If you only override isEqual:, you violate that requirement. Any time you override isEqual:, always override hash at the same time. For more information, see my article on Implementing Equality and Hashing.

MacrosImagine you’re writing some unit tests. You have a method that’s supposed to return an array containing a single object, so you write a test to verify that:

STAssertEqualObjects([obj method], @[ @"expected" ], @"Didn't get the expected array");

This uses the new literals syntax to keep things short. Nice, right?

Now we have another method that returns two objects, so we write a test for that:

STAssertEqualObjects([obj methodTwo], @[ @"expected1", @"expected2" ], @"Didn't get the expected array");

Suddenly, the code fails to compile and produces completely bizarre errors. What’s going on?

What’s going on is that STAssertEqualObjects is a macro. Macros are expanded by the preprocessor, and the preprocessor is an ancient and fairly dumb program that doesn’t know anything about modern Objective-C syntax, or for that matter modern C syntax. The preprocessor splits macro arguments on commas. It’s smart enough to know that parentheses can nest, so this is seen as three arguments:

Macro(a, (b, c), d)

Where the first argument is a, the second is (b, c), and the third is d. However, the preprocessor has no idea that it should do the same thing for [] and {}. With the above macro, the preprocessor sees four arguments:

  • [obj methodTwo]

  • @[ @“expected1”

  • @“expected2 ]

  • @“Didn’t get the expected array”

This results in completely mangled code that not only doesn’t compile, but confuses the compiler beyond the ability to provide understandable diagnostics. The solution is easy, once you know what the problem is. Just parenthesize the literal so the preprocessor treats it as one argument:

STAssertEqualObjects([obj methodTwo], (@[ @"expected1", @"expected2" ]), @"Didn't get the expected array");

Unit tests are where I’ve run into this most frequently, but it can pop up any time there’s a macro. Objective-C literals will fall victim, as will C compound literals. Blocks can also be problematic if you use the comma operator within them, which is rare but legal. You can see that Apple thought about this problem with their Block_copy and Block_release macros in /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__))

These macros conceptually take a single argument, but they’re declared to take variable arguments to avoid this problem. By taking … and using VA_ARGS to refer to “the argument”, multiple “arguments” with commas are reproduced in the macro’s output. You can take the same approach to make your own macros safe from this problem, although it only works on the last argument of a multi-argument macro.

Property SynthesisTake the following class:

@interface MyClass : NSObject {
NSString *_myIvar;
}
@property (copy) NSString *myIvar;
@end
@implementation MyClass
@synthesize myIvar;
@end

Nothing wrong with this, right? The ivar declaration and @synthesize are a little redundant in this modern age, but do no harm.

Unfortunately, this code will silently ignore _myIvar and synthesize a new variable called myIvar, without the leading underscore. If you have code that uses the ivar directly, it will see a different value from code that uses the property. Confusion!

The rules for @synthesize variable names are a little weird. If you specify a variable name with @synthesize myIvar = _myIvar;, then of course it uses whatever you specify. If you leave out the variable name, then it synthesizes a variable with the same name as the property. If you leave out @synthesize altogether, then it synthesizes a variable with the same name as the property, but with a leading underscore.

Unless you need to support 32-bit Mac, your best bet these days is to just avoid explicitly declaring backing ivars for properties. Let @synthesize create the variable, and if you get the name wrong, you’ll get a nice compiler error instead of mysterious behavior.

Interrupted System CallsCocoa code usually sticks to higher level constructs, but sometimes it’s useful to drop down a bit and do some POSIX. For example, this code will write some data to a file descriptor:

int fd;
NSData *data = ...;
const char *cursor = [data bytes];
NSUInteger remaining = [data length];
while(remaining > 0) {
ssize_t result = write(fd, cursor, remaining);
if(result < 0)
{
NSLog(@"Failed to write data: %s (%d)", strerror(errno), errno);
return;
}
remaining -= result;
cursor += result;
}

However, this can fail, and it will fail strangely and intermittently. POSIX calls like this can be interrupted by signals. Even harmless signals handled elsewhere in the app like SIGCHLD or SIGINFO can cause this. SIGCHLD can occur if you’re using NSTask or are otherwise working with subprocesses. When write is interrupted by a signal, it returns -1 and sets errno to EINTR to indicate that the call was interrupted. The above code treats all errors as fatal and will bail out, even though the call just needs to be tried again. The correct code checks for that separately and just retries the call:

while(remaining > 0) {
ssize_t result = write(fd, cursor, remaining);
if(result < 0 && errno == EINTR)
{
continue;
}
else if(result < 0)
{
NSLog(@"Failed to write data: %s (%d)", strerror(errno), errno);
return;
}
remaining -= result;
cursor += result;
}

String LengthsThe same string, represented differently, can have different lengths. This is a relatively common but incorrect pattern:

write(fd, [string UTF8String], [string length]);

The problem is that NSString computes length in terms of UTF-16 code units, while write wants a count of bytes. While the two numbers are equal when the string only contains ASCII (which is why people so frequently get away with writing this incorrect code), they’re no longer equal once the string contains non-ASCII characters such as accented characters. Always compute the length of the same representation you’re manipulating:

const char *cStr = [string UTF8String];
write(fd, cStr, strlen(cStr));

Casting to BOOLTake this bit of code that just checks to see whether an object pointer is nil:

- (BOOL)hasObject
{
return (BOOL)_object;
}

This works… usually. However, roughly 6% of the time, it will return NO even though _object is not nil. What gives?

The BOOL type is, unfortunately, not a boolean. Here’s how it’s defined:

typedef signed char BOOL;

This is another bit of unfortunate legacy from the days when C had no boolean type. Cocoa predates C99’s _Bool, so it defines its “boolean” type as a signed char, which is just an 8-bit integer. When you cast a pointer to an integer, you just get the numeric value of that pointer. When you cast a pointer to a small integer, you just get the numeric value of the lower bits of that pointer. When the pointer looks like this:

....110011001110000

The BOOL gets this:

01110000

This is not 0, meaning that it evaluates as true, so what’s the problem? The problem is when the pointer looks like this:

....110011000000000

Then the BOOL gets this:

00000000

This is 0, also known as NO, even though the pointer wasn’t nil. Oops!

How often does this happen? There are 256 possible values in the BOOL, only one of which is NO, so we’d naively expect it to happen about 1/256 of the time. However, Objective-C objects are allocated aligned, normally to 16 bytes. This means that the bottom four bits of the pointer are always zero (something that tagged pointers takes advantage of) and there are only four bits of freedom in the resulting BOOL. The odds of getting all zeroes there are about 1/16, or about 6%.

To safely implement this method, perform an explicit comparison against nil:

- (BOOL)hasObject
{
return _object != nil;
}

If you want to get clever and unreadable, you can also use the ! operator twice. This !! construct is sometimes referred to as C’s “convert to boolean” operator, although it’s just built from parts:

- (BOOL)hasObject
{
return !!_object;
}

The first ! produces 1 or 0 depending on whether _object is nil, but backwards. The second ! then puts it right, resulting in 1 if _object is not nil, and 0 if it is.

You should probably stick to the != nil version.

Missing Method ArgumentLet’s say you’re implementing a table view data source. You add this to your class’s methods:

- (id)tableView:(NSTableView *) objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
{
return [dataArray objectAtIndex: rowIndex];
}

Then you run your app and NSTableView complains that you haven’t implemented this method. But it’s right there!

As usual, the computer is correct. The computer is your friend.

Look closer. The first parameter is missing. Why does this even compile?

It turns out that Objective-C allows empty selector segments. The above does not declare a method named tableView:objectValueForTableColumn:row: with a missing argument name. It declares a method named tableView::row:, and the first argument is named objectValueForTableColumn. This is a particularly nasty way to typo the name of a method, and if you do it in a context where the compiler can’t warn you about the missing method, you may be trying to debug it for a long time.

ConclusionObjective-C and Cocoa have plenty of pitfalls ready to trap the unwary programmer. The above is just a sampling. However, it’s a good list of things to be careful of.

That’s it for today! Check back next time for more wacky advice. Friday Q&A is driven by user ideas, in case you didn’t already know, so until next time, please send in your ideas for articles!