Methods, Messages, and Selectors
In standard C, you’d perform two function calls to allocate and initialize data. Here is how that might look, in contrast to Objective-C’s [[Car alloc] init] statement:
Car *myCar = malloc(sizeof(Car)); init(myCar);
Objective-C doesn’t use function_name(arguments) syntax. Instead, you send messages to objects using square brackets. Messages tell the object to perform a method. It is the object’s responsibility to implement that method and produce a result. The first item within the brackets is the receiver of the message; the second item is a method name, and possibly some arguments to that method that together define the message you want sent. In C, you might write
printCarInfo(myCar);
but in Objective-C, you say
[myCar printCarInfo];
Despite the difference in syntax, methods are basically functions that operate on objects. They are typed using the same types available in standard C. Unlike function calls, Objective-C places limits on who can implement and call methods. Methods belong to classes. And the class interface defines which of these are declared to the outside world.
Dynamic Typing
Objective-C uses dynamic typing in addition to static typing. Static typing restricts a variable declaration to a specific class at compile time. With dynamic typing, the runtime system, not the compiler, takes responsibility for asking objects what methods they can perform and what class they belong to. That means you can choose what messages to send and which objects to send them to as the program runs. This is a powerful feature—one that is normally identified with interpreted systems such as Lisp. You can choose an object, programmatically build a message, and send the message to the object—all without knowing which object will be picked and what message will be sent at compile time.
With power, of course, comes responsibility. You can only send messages to objects that actually implement the method described by that selector (unless that class can handle messages that don’t have implementations by implementing Objective-C invocation forwarding, which is discussed at the end of this chapter). Sending printCarInfo to an array object, for example, causes a runtime error and crashes the program. Arrays do not define that method. Only objects that implement a given method can respond to the message properly and execute the code that was requested.
2009-05-08 09:04:31.978 HelloWorld[419:20b] *** -[NSCFArray printCarInfo]: unrecognized selector sent to instance 0xd14e80 2009-05-08 09:04:31.980 HelloWorld[419:20b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[NSCFArray printCarInfo]: unrecognized selector sent to instance 0xd14e80'
During compilation, Objective-C performs object message checks using static typing. The array definition in Figure 3-1 is declared statically, telling the compiler that the object in question is of type (NSArray *). When the compiler finds objects that may not be able to respond to the requested methods, it issues warnings.
Figure 3-1. Xcode’s Objective-C issues warnings when it finds a method that does not appear to be implemented by the receiver.
These warnings do not make the compilation fail, and it’s possible that this code could run without error if NSArray implemented printCarInfo and did not declare that implementation in its published interface. Since NSArray does not, in fact, implement this method, running this code produces the actual runtime crash shown previously.
Objective-C’s dynamic typing means you can point to the same kind of object in several different ways. Although array was declared as a statically typed (NSArray *) object, that object uses the same internal object data structures as an object declared as id. The id type can point to any object, regardless of class, and is equivalent to (NSObject *). This following assignment is valid and does not generate any warnings at compile time:
NSArray *array = [NSArray array]; // This assignment is valid id untypedVariable = array;
To further demonstrate, consider a mutable array. The NSMutableArray class is a subclass of NSArray. The mutable version offers arrays that you can change and edit. Creating and typing a mutable array but assigning it to an array pointer compiles without error. Although anotherArray is statically typed as NSArray, creating it in this way produces an object at runtime that contains all the instance variables and behaviors of the mutable array class.
NSArray *anotherArray = [NSMutableArray array]; // This mutable-only method call is valid but // produces a compile-time warning [anotherArray addObject:@"Hello World"];
What produces a warning here is not the creation and assignment. It’s the use. Sending addObject: to anotherArray uses our knowledge that the array is, in fact, mutable despite the fact that it is statically typed as (NSArray *). That’s something the compiler does not understand. This use generates a compile-time warning, namely:
'NSArray' may not respond to '-addObject:'
At runtime, however, the code works without error.
While assigning a child class object to a pointer of a parent class generally works at runtime, it’s far more dangerous to go the other way. A mutable array is a kind of array. It can receive all the messages that arrays do. Not every array, on the other hand, is mutable. Sending the addObject: message to a regular array is lethal. Doing so bombs at runtime, as arrays do not implement that method.
NSArray *standardArray = [NSArray array]; NSMutableArray *mutableArray; // This line produces a warning mutableArray = standardArray; // This will bomb at run-time [mutableArray addObject:@"Hello World"];
The code seen here produces just one warning, at the line where the standard array object is assigned to the mutable array pointer, namely “assignment from distinct Objective-C type.” Parent-to-child assignments do not generate this warning. Child-to-parent assignments do. So do assignments between completely unrelated classes. Do not ignore this warning; fix your code. Otherwise, you’re setting yourself up for a runtime crash. Because Objective-C is a compiled language that uses dynamic typing, it does not perform many of the runtime checks that interpreted object-oriented languages do.
Inheriting Methods
As with data, objects inherit method implementations as well as instance variables. A Car is a kind of NSObject, so it can respond to all the messages that an NSObject responds to. That’s why myCar can be allocated and initialized with alloc and init. These methods are defined by NSObject. Therefore, they can be used to create and initialize any instance of Car, which is derived from the NSObject class.
Similarly, NSMutableArray instances are a kind of NSArray. All array methods can be used by mutable arrays, their child class. You can count the items in the array, pull an object out by its index number, and so forth.
A child class may override a parent’s method implementation, but it can’t negate that the method exists. Child classes always inherit the full behavior and state package of their parents.
Declaring Methods
As Listing 3-1 showed, a class interface defines the instance variables and methods that a new class adds to its parent class. This interface is normally placed into a header file, which is named with an .h extension. The interface from Listing 3-1 declared three methods, namely:
- (void) setMake:(NSString *) aMake andModel:(NSString *) aModel andYear: (int) aYear; - (void) printCarInfo; - (int) year;
These three methods, respectively, return void, void, and int. Notice the dash that starts the method declaration. It indicates that the methods are implemented by object instances. For example, you call [myCar year] and not [Car year]. The latter sends a message to the Car class rather than an actual car object. A discussion about class methods (indicated by “+” rather than “-”) follows later in this section.
As mentioned earlier, methods calls can be complex. The following invocation sends a method request with three parameters. The parameters are interspersed inside the method invocation. The name for the method (that is, its selector) is setMake:andModel:andYear:. The three colons indicate where parameters should be inserted. The types for each parameter are specified in the interface after the colons, namely (NSString *), (NSString *), and (int). As this method returns void, the results are not assigned to a variable.
[myCar setMake:@"Ford" andModel:@"Prefect" andYear:1946];
Implementing Methods
Together, a method file and a header file pair store all the information needed to implement a class and announce it to the rest of an application. The implementation section of a class definition provides the code that implements functionality. This source is usually placed in an .m (for “method”) file.
Listing 3-2 shows the implementation for the Car class example. It codes all three methods declared in the header file from Listing 3-1 and adds a fourth. This extra method redefines init. The Car version of init sets the make and model of the car to nil, which is the NULL pointer for Objective-C objects. It also initializes the year of the car to 1901.
The special variable self refers to the object that is implementing the method. That object is also called the “receiver” (that is, the object that receives the message). This variable is made available by the underlying Objective-C runtime system. In this case, self refers to the current instance of the Car class. Calling [self message] tells Objective-C to send a message to the object that is currently executing the method.
Several things are notable about the init method seen here. First, the method returns a value, which is typed to (id). As mentioned earlier in this chapter, the id type is more or less equivalent to (NSObject *), although it’s theoretically slightly more generic than that. It can point to any object of any class (including Class objects themselves). You return results the same way you would in C, using return. The goal of init is to return a properly initialized version of the receiver via return self.
Second, the method calls [super init]. This tells Objective-C to send a message to a different implementation, namely the one defined in the object’s superclass. The superclass of Car is NSObject, as shown in Listing 3-1. This call says, “Please perform the initialization that is normally done by my parent class before I add my custom behavior.” Calling a superclass’s implementation before adding new behavior demonstrates an important practice in Objective-C programming.
Finally, notice the check for if (!self). In rare instances, memory issues arise. In such a case, the call to [super init] returns nil. If so, this init method returns before setting any instance variables. Since a nil object does not point to allocated memory, you cannot access instance variables within nil.
As for the other methods, they use year, make, and model as if they were locally declared variables. As instance variables, they are defined within the context of the current object and can be set and read as shown in this example. The UTF8String method that is sent to the make and model instance variables converts these NSString objects into C strings, which can be printed using the %s format specifier.
Listing 3-2. The Car Class Implementation (Car.m)
#import "Car.h" @implementation Car - (id) init { self = [super init]; if (!self) return nil; // These initial nil assignments are not really needed. // All instance variables are initialized to zero by alloc. // Here is where you would perform any real assignments. make = nil; model = nil; year = 1901; return self; } - (void) setMake:(NSString *) aMake andModel:(NSString *) aModel andYear: (int) aYear { // Note that this does not yet handle memory management properly // The Car object does not retain these items, which may cause // memory errors down the line make = aMake; model = aModel; year = aYear; } - (void) printCarInfo { if (!make) return; if (!model) return; printf("Car Info\n"); printf("Make: %s\n", [make UTF8String]); printf("Model: %s\n", [model UTF8String]); printf("Year: %d\n", year); } - (int) year { return year; } @end
Class Methods
Class methods are defined using a plus (+) prefix rather than a hyphen (-). They are declared and implemented in the same way as instance methods. For example, you might add the following method declaration to your interface:
+ (NSString *) motto;
Then you could code it up in your implementation:
+ (NSString *) motto { return(@"Ford Prefects are Mostly Harmless"); }
Class methods differ from instance methods in that they generally cannot use state. They are called on the Class object itself, which does not have access to instance variables. That is, they have no access to an instance’s instance variables (hence the name) because those elements are only created when instantiated objects are allocated from memory.
So why use class methods at all? The answer is threefold. First, class methods produce results without having to instantiate an actual object. This motto method produces a hard-coded result that does not depend on access to instance variables. Convenience methods such as this often have a better place as classes rather than instance methods.
You might imagine a class that handles geometric operations. The class could implement a conversion between radians and angles without needing an instance (for example, [GeometryClass convertAngleToRadians:theta];). Simple C functions declared in header files also provide a good match to this need.
The second reason is that class methods can hide a singleton. Singletons refer to statically allocated instances. The iOS SDK offers several of these. For example, [UIApplication sharedApplication] returns a pointer to the singleton object that is your application. [UIDevice currentDevice] retrieves an object representing the hardware platform you’re working on.
Combining a class method with a singleton lets you access that static instance anywhere in your application. You don’t need a pointer to the object or an instance variable that stores it. The class method pulls that object’s reference for you and returns it on demand.
Third, class methods tie into memory management schemes. Consider allocating a new NSArray. You do so via [[NSArray alloc] init] or you can use [NSArray array]. This latter class method returns an array object that has been initialized and set for autorelease. As you read about later in this chapter, Apple has provided a standard about class methods that create objects. They always return those objects to you already autoreleased. Because of that, this class method pattern is a fundamental part of the standard iOS memory management system.
Fast Enumeration
Fast enumeration was introduced in Objective-C 2.0 and offers a simple and elegant way to enumerate through collections such as arrays and sets. It adds a for-loop that iterates through the collection using concise for/in syntax. The enumeration is very efficient, running quickly. It is also safe. Attempts to modify the collection as it’s being enumerated raise a runtime exception.
NSArray *colors = [NSArray arrayWithObjects: @"Black", @"Silver", @"Gray", nil]; for (NSString *color in colors) printf("Consider buying a %s car", [color UTF8String]);