Objective-C 弱类型的大弱点

A big weakness in Objective-C's weak typing


http://cocoawithlove.com/2011/06/big-weakness-of-objective-c-weak-typing.html


We generally assume that we can send any message we want to a variable in our code typed as "id" and Objective-C's dynamic message handling will make the invocation work correctly at runtime. In some rare cases, this assumption is wrong. I'll look at situations where you need to be careful about sending messages to "id" typed variables and a situation where a limitation in the Objective-C language requires a hideous workaround to avoid serious bugs.

Introduction

We generally assume we can send any message we like to an "id" variable (or a Class variable). In fact, that's the real purpose of the "id" type: it is the "any" type in Objective-C to which any Objective-C message may be sent.

We use this in lots of different situations but one of the most common is sending messages to objects store in an NSArray:

NSString *description = [[someArray objectAtIndex:0] substringFromIndex:5];

In this code sample, we don't need to cast the result of the objectAtIndex: invocation to an NSString before sending it the substringFromIndex: message — we know that as long as the object at index 0 actually is an object that responds to the substringFromIndex: selector, it will work.

This post is about invoking methods on either of Objective-C's two "weak" types: id or Class. This post does not apply if the variable you invoke a method on is typed to anything else (even id<SomeProtocol> will count as "anything else" and avoid the issues in this post).
False assumptions

The false assumption often applied here is that the compiler doesn't need to know any type information. In reality, even though the method lookup happens at runtime, that's only enough to ensure the correct method is invoked, it is not enough to ensure the parameters will work.

The compiler definitely does need to infer some information about the method signature involved. Even though the compiler does not necessarily need to know the type of the id, it does need to know the byte lengths of all parameters and the explicit type of any return values. This is because marshalling of the parameters (pushing and popping them from the stack) is configured at compile time.

Normally, we don't need to take any steps for this to happen though. The parameter information is obtained by looking at the name of the method you're trying to invoke, searching through the included headers for methods matching the invoked method name and then getting the parameter lengths from the first matching method it finds.

99.99% of the time, there's no problem with this: even if there's ambiguity about the exact method you're really targeting, the parameters are likely to be the same between matching method because method names in Objective-C generally imply the types of the data, so this type of conflict is likely to cause no difference in signature.

And then there's that other 0.01% of the time...

Catastrophic failure

Imagine you have class MyClass with an instance method named currentPoint that returns an int. You want to get the currentPoint from an object stored in an array, so you use the code:

int result = [[someArray objectAtIndex:0] currentPoint];

When you run the code, you know the exact value returned from invoking currentPoint on the first object in the array should be zero (because you set it to zero and you can see in the debugger that it is still zero) but the value that ends up in the result is 2,147,483,647 (or some other partial garbage value).

What has gone wrong?

The correct method is invoked at runtime. The problem is that the compiler marshalled the parameters for this invocation incorrectly leading to data corruption of the return type.

The compiler needs to push parameters onto the stack correctly before the message send and perform the message send using the correct variant of objc_msgSend to get the return value back afterwards. This is what has failed.

The compiler prepares parameters using the method signature (which it gets by looking at the type of the receiver and all method names that are valid for the receiver) and trying to work out which method you're likely to be invoking. Since the type of the receiver (i.e. the result from objectAtIndex:) is just id then we have no explicit type information so the compiler will look through the list of all known methods.

Unfortunately, instead of our MyClass method, the compiler decided to match against the NSBezierPath method named currentPoint and has prepared the parameters to match that method's signature. The NSBezierPath method returns an NSPoint which is a struct and handles the return parameter very differently compared to the int parameter our real method actually uses. This has lead to our return type getting corrupted.

I use the example of a struct value return type here because it is the most likely to generate a bug, since a struct return type causes the compiler to generate an objc_msgSend_stret for the message invocation instead of a regular objc_msgSend. However, it is also possible to create problems with non-return parameters of different lengths, particularly if the conflict is between floating point and non-floating point parameters or struct and non- struct parameters.
Fixing the problem (most of the time)

The best way to fix the problem is to add additional type information about the receiver, so that there will only be 1 possible match for the method name.

We do this by casting the receiver:

int result = [(MyClass *)[someArray objectAtIndex:0] currentPoint];

A MyClass receiver has only one match for currentPoint so there is no possible conflict with the NSBezierPath method and the problem here is solved.

Why is this allowed to happen? Why isn't there a compiler error?

Technically, there is a compiler warning that will alert you to this category of problem. The compiler warning "Strict Selector Matching" (a.k.a. -Wstrict-selector-match) will tell you when there is a conflict between two different method names when you're invoking a method on either of Objective-C's two weak types (id or Class).

It would be great if Strict Selector Matching always worked and we could turn it on at all times. That Apple don't turn it on by default is either because they consider the problem rare enough to ignore or it is an admission of the significant limitations of the warning as it currently behaves:

  1. It will over-warn you. Basically, there's no reason to care if there is a conflict between two methods different but totally compatible method signatures but this compiler warning will still occur.
  2. Plenty of Apple's own methods will cause spurious warnings due to point (1). I'm looking at you, different implementations of objectForKey:, count and most methods in NSNotificationCenter versus NSDistributedNotificationCenter. These spurious warnings may force you to carefully typecast large numbers of method calls that won't actually cause any problem.
  3. It will not warn you about conflicts between a class and instance methods. This one is a bit absurd since a Class object is regularly handled as a generic id.
  4. It won't help if you haven't imported the correct definition at all. If you failed to import the declaration of the correct method but you did import the declaration of a different method with a matching name but different signature, then I'm not sure the compiler could warn you about this problem.
A scenario where casting won't fix the problem

Imagine in the example above, the problem was between two class methods, instead of two instance methods.

i.e. instead of a conflict between -[NSBezierPath currentPoint] and -[MyClass currentPoint], the conflict was between +[NSBezierPath currentPoint] and +[MyClass currentPoint].

For the instance methods, we fixed the problem by casting to the specific object type required but when your objects are both Class, it is not possible to cast to the specific Class involved. Seriously: you cannot cast classes in Objective-C.

I consider this a serious failing of Objective-C. It makes avoiding this scenario with conflicting class method names hideous. If you're not able to change the name of the method, then the only work around looks like this:

int result = objc_msgSend([someArray objectAtIndex:0], @selector(currentPoint));

That's right: we would need to bypass the compiler entirely and insert the objc_msgSend call ourselves.

It gets worse though, since objc_msgSend uses a variable argument list by default, if the parameters you need to pass/receive from objc_msgSend are not signature compatible with a variable argument list, then you'll need to fully cast the objc_msgSend yourself to make certain that the parameters are passed correctly:

SomeReturnType result =
    ((SomeReturnType(*)(float, short, WeirdStruct))objc_msgSend)
    (
        [someArray objectAtIndex:0],
        @selector(methodWithMultipleVariables:thatAreNot:varArgCompatible:),
        someFloat,
        someShort,
        someWeirdStruct
    );

And if SomeReturnType is a struct, you'll need to use objc_msgSend_stret instead and for floating point types, you'll need to use objc_msgSend_fpret.

Conclusion

I would like to give a blanket suggestion that you switch on "Strict Selector Matching" in your Workspace/Project/Target settings for every project but unfortunately, the limitations of this warning make that suggestion difficult.

Spurious warnings from Apple's code, every time you try to use objectForKey: (or a large group of other methods) on an id typed variable can be infuriating and pointlessly increase your workload.

The warning doesn't catch all problems because it doesn't check conflicts between methods in the Class and id spaces. Along with the spurious warnings nuisance, the argument that you could skip the warning itself (and simply keep this potential problem in mind as a potential issue) is probably valid.

The warning itself should be fixed in GCC and Clang to make it useable enough to leave on in all situations. It shouldn't give a warning when the parameters in conflicting signatures are compatible. It should also gain cross checking between Class and id when the type in the code is id.

And Objective-C really needs a way to declare a type that is a specific Class. Even syntax as ugly as @classtype(SomeClass) would do it but I'm sure something more graceful could be found.

I've seen people argue that if you ever require a specific Class then you're designing things badly. I think the bugs caused by method name conflicts are a situation where you may not (if you can't rename either method) be able to cleanly design your way around this serious problem without the ability to cast a Class to something more specific.

In addition to allowing a conflict between two Class methods to be resolved more gracefully it would also help in the unrelated situation where a method wants a Class object passed as a parameter but that Class must be a subclass of a specific class.


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值