Objective-C is a compromise by design, and it is utterly unembarrassed by this. It is, I think, a good compromise, finding a sweet spot where one has very convenient access to low-overhead constructs for performance (C and C++ can be linked in and even intermingled with ObjC) while still having a nice dynamic messaging system supporting flexible late-bound polymorphism.
It’s also a compromise from the ’80s. (Relatively) recent advances in functional programming (among other spheres) sometimes make me wonder if we could strike a better one today.
I use Objective-C every day for my bread and butter and many of my hobby/research programs. I quite like the compromise it strikes, but it does run into issues that I think wouldn’t exist in the context of another language, and may even be fixable within Objective-C if Apple decides to pursue them.
Some of the following issues are comp.sci. theoretical, in that they affect the elegant and correct expression of desired behaviour. All of them are practical, in that they have affected my work with the language directly; these are limitations that I have been aggrieved at having to put up with. Whether your work would intersect these same issues is another question altogether; I wouldn’t care to speculate.
Not everything is an object. Since Objective-C supports C’s types, and, in ObjC++ files, C++ types as well, it’s much harder to have universal collections. This can be mitigated somewhat by the use of boxing types, e.g. NSNumber and NSValue, which wrap “primitive” types, and even further with recent changes to allow tagged pointers and boxed values. Even so, you pay a cost in terms of performance, and more damningly, syntax.
The type system lacks in expressiveness. One of Objective-C’s best features is protocols, known as interfaces in some other languages, which provide a way to group classes together based solely on interface, rather than on implementation. While this is an excellent start, it isn’t quite enough to safely and completely express the interactions with things like collections. Parameterized interfaces, like “NSArray of NSStrings” or “NSDictionary of NSNumbers to id<Foo> instances,” would help quite a bit. For example, you can’t solve the expression problem (supporting both new operations over types and new types of data) in Objective-C without resorting to id—losing type safety and valuable information.
Composition is not easy enough. Subclassing (implementation inheritance) is an inflexible mechanism for polymorphism, but composition can be difficult, even with the use of protocols. When Objective-C 2.0 was being designed, there was brief reference in the documentation to “concrete protocols,” which are also known as traits in other languages (or nearly so—it’s unclear whether concrete protocols would have implemented the aliasing semantics of full-blown traits). Unfortunately, this was pulled, apparently due to issues with calling super[1].
While this is not, strictly speaking, a shortcoming of the language, composition is not encouraged enough by the community. Cocoa and its related libraries (including both those written by Apple and those written by the community as a whole) often to encourage subclassing instead of composition for extensibility (where extensibility is feasible at all; often important behaviour is walled behind private methods). As described above, this is lacking in flexibility relative to composition. The Liskov substitution principle forces you to make stronger statements about your classes than perhaps you want to, requiring you to adopt all your superclass’ behaviour. This additionally tends to encourage tighter coupling, since the strong statements about a class might lead you to rely on another class’ behaviour rather than simply what is defined in a given interface onto it.
While objc_msgSend can tail call, it is never guaranteed to[2]. This may not matter in practice, since it does tail call in reasonable situations, but it makes me uncomfortable. Further, it can’t collapse a tail call to an iterative loop and optimize just that, which misses some opportunities for optimization.
Pointers are dangerous, especially in the presence of unenforced typing. Certainly they are powerful, and able to enable great efficiency, but it’s likely the case that the vast majority of e.g. view controllers don’t require them for anything that they’re doing, and could be implemented or executed in a more managed environment. That said, Objective-C is actively becoming a more managed language, with ARC adding retain/release into the language runtime proper; it’s possible, although I’d consider it unlikely, that general pointer safety could come in a later version of the language.
The existence of pointers also makes it harder or impossible to make good guarantees about references, which has consequences for both performance and security. You can’t implement the capability security model in a language that has pointers, since pointers enable forged references. You can’t implement accurate automatic garbage collection (for example a compacting collector) in a language that has pointers (at least with C’s type semantics) since you can get both false negatives and false positives as references to existing objects. The old autozone -fobjc-gc collector was therefore a conservative collector, came with some important gotchas, and suffered in performance relative to a state-of-the-art accurate collector. (I have seen it asserted, although not backed up to my satisfaction, that a modern accurate garbage collector can outperform reference counting, in part due to enabling good locality of reference. I am not sure if these claims would hold in the somewhat restricted environment of iOS, however.)
As Objective-C has no facility for JIT compilation, some optimizations become unavailable, or at least more difficult. For example, a JIT compiler could theoretically compile a message send to a static dispatch when it can make strong enough guarantees about the type of the receiver and the entries in the corresponding method table. These guarantees could be reevaluated when adding methods, loading code dynamically, and so forth, and the appropriate methods could be recompiled accordingly. Thus you would get the dynamic dispatch everywhere it is required, and the static dispatch everywhere it is possible; the best of both worlds. (Again, this is entirely theoretical. It may be that these sorts of optimizations would be impossible due to other difficulties in making such guarantees.)
You can’t introspect the parse tree at runtime, which means you can only build lexical macros, not more interesting ones. This is perhaps the least practical complaint thus far, but the C preprocessor offends me morally. (I do not hesitate to use it where appropriate, but I generally frown while doing so.)
While metaprogramming has become much more tractable in Objective-C of late due to the addition of the modern runtime API, blocks, blocks-as-IMPs, and other lovely additions, I sometimes wish all of this were simpler to deploy. Value classes (e.g. records and tuples) can be quite charming implemented with this sort of pattern.
I want to note also that I wouldn’t describe Objective-C’s verbosity as being either a practical or theoretical downside. It may be distasteful to some, but I personally don’t much like seeing ctxt->rtrv() or similar nonsense. Clarity of communication is a virtue, and that Cocoa encourages it is praiseworthy. (If your fingers are that tired/sore from typing, talk to your doctor, and make better use of your IDE or text editor’s autocompletion facilities.)