The main job inside an Objective-C application is to send messages between objects. Some objects are instances of classes provided by Cocoa or Cocoa Touch, and some are our own custom objects.

The previous article described how to define the interface and implementation of a class, and mentioned how to implement methods in response to a message. This article focuses on sending a message to an object, including some Objective-C dynamic features, dynamic typing, and runtime method resolution.

Before an object can be used, it must allocate Memory for its properties and initialize its internal values. This article describes how to continuously call the open up memory method and the initialization method to ensure that the object can be set correctly.

Object to send and receive messages

Although there are many different ways to send messages in Objective-C. But by far the most common syntax uses square brackets [] as shown in the following example:

[someObject doSomething];
Copy the code

The someObject on the left represents the Receiver of the message. DoSomething on the right represents the name of the method that called the receiver. When the above line of code is called, it sends a message for doSomething to someObject.

The previous article described how to create a class interface, as follows:

@interface XYZPerson : NSObject
- (void)sayHello;
@end
Copy the code

It also describes how to implement a class like this:

@implementation XYZPerson - (void)sayHello { NSLog(@"Hello, world!" ); } @endCopy the code

Note: This example uses Objective-C string syntax, @”Hello, world!” The.nsString class is one of the objective-C classes that allows instances to be created using syntactic sugar. You’re creating an Objective-C string that says “Hello,world!” The object.

Assuming we already have a XYZPerson object, we can send it a message saying Hello like this.

[somePerson sayHello];
Copy the code

Sending Objective-C messages is conceptually similar to calling C functions. The following figure shows the actual flow of the program when a sayHello message is sent.

To specify message recipients, it is important to understand how Pointers point to objects in Objective-C.

Use Pointers to track objects

C and Objective-C use variables to hold values, just like most other programming languages.

In the standard C language, many value variable types are defined, including integers, floating-point numbers, and characters, which are declared and assigned as follows:

int someInteger = 42; Float someFloatingPointNumber = 3.14 f;Copy the code

Declare local variables – variables declared in a method or function are limited in scope to the method they are defined in.

- (void)myMethod {
    int someInteger = 42;
}
Copy the code

In this example, someInteger is a local variable declared inside myMethod. Once the program executes to the end of the} braces, someInteger is not accessible. When the scope of a local value type variable (such as int or float) ends, its value disappears.

In contrast, memory allocation for Objective-C objects is slightly different. Objects usually have a longer lifetime. The object itself usually needs to live longer than the variable storing it, so the object’s memory is allocated/reclaimed dynamically.

Note: In terms of Stack or Heap, the space of a local variable is allocated on the Stack and the space of an object is allocated on the Heap.

This requires us to use C Pointers that hold objects’ memory addresses to track their location in memory:

- (void)myMethod { NSString *myString = // get a string from somewhere... [...]. }Copy the code

Although the scope of the pointer variable myString (* means it is a pointer) is limited to myMethod, the string object it points to may have a longer lifetime in memory.

Pass objects as method parameters

If we need to pass an object as a parameter when sending a message, we pass the object pointer as a parameter. The previous article described the declaration of a single-parameter method:

- (void)someMethodWithValue:(SomeType)value;
Copy the code

When an object of type NSString is passed in, it looks like this:

- (void)saySomething:(NSString *)greeting;
Copy the code

We can do this:

- (void)saySomething:(NSString *)greeting {
    NSLog(@"%@", greeting);
}
Copy the code

Like a local variable, the greeting pointer is scoped inside the saySomething method, even though the string object it points to exists before the method is called and will continue to exist after the method completes.

Note: Similar to C’s standard library function printf(), NSLog() uses format specifiers to indicate placeholder formats. The string output on the console is the result of inserting the corresponding string (the remaining argument) into the formatted string (the first argument).

Objective-c has an extra placeholder %@ to refer to an object. At run time, this identifier will be replaced by the object’s descriptionWithLocale: method (if it exists) or by the description method. The NSObject class implements the description method to return the class and memory address of the object, but many Cocoa and Cocoa Touch classes override this method to provide more useful information. Like the NSString class, the description method directly returns the string value it represents.

For more NSLog(), see String Format Specifiers for the NSString class.

Methods can return values

We can pass parameters to a method, and the method can have a return value. Every method in this article so far has returned a value of type void. The void keyword in C means that this method returns no value.

Specifying a return value of type int means that the method returns an integer value of type int:

- (int)magicNumber;
Copy the code

The implementation of this method uses C’s return, indicating that the value should be returned to the caller at the end of the method:

- (int)magicNumber {
    return 42;
}
Copy the code

We can simply ignore the return value of the method. In this example, the magicNumber does nothing but return a value, but there is no problem calling the method this way.

[someObject magicNumber];
Copy the code

If we really need to get the return value, we can declare a variable and assign the method return value to it:

int interestingNumber = [someObject magicNumber];
Copy the code

We can also return objects in the same way, for example NSString provides an uppercaseString method that returns NSString *.

- (NSString *)uppercaseString;
Copy the code

The method of returning an object is much the same as the method of returning a numeric type, except that we need to use a pointer to hold the result.

NSString *testString = @"Hello, world!" ; NSString *revisedString = [testString uppercaseString];Copy the code

When the above two lines of code are called, revisedString will point to a code that says HELLO WORLD! String of.

When we implement a method that returns an object like this:

- (NSString *)magicString {
    NSString *stringToReturn = // create an interesting string...
 
    return stringToReturn;
}
Copy the code

The string object still exists, even though the scope of the stringToReturn pointer has expired.

This is where objective-C memory management comes in. In this case, the object created on the heap needs to live long enough for the method caller to use the return value, but you can’t let the object live forever, or you’ll create a memory leak. In general, the Objective-C compiler’s ARC technique (Automatic Reference Count – Automatic Reference Count) manages this memory for us.

Objects can send messages to themselves

Every time we write a method implementation, we get an important hidden value, self. Conceptually, self is a way of getting “the object that received this message.” It is a pointer that, like the greeting object mentioned above, can be used to call methods on the object that receives the message.

Suppose we need to refactor the implementation of XYZPerson by using the saySomething: method inside its sayHello method. This means we can add more methods in the future, such as sayGoodbye, that handle logic through the saySomething: method. If we later need to display the results in a text box, we just need to change saySomething: a method, not every method.

@implementation XYZPerson - (void)sayHello { [self saySomething:@"Hello, world!"] ; } - (void)saySomething:(NSString *)greeting { NSLog(@"%@", greeting); } @endCopy the code

The flow of the program is as follows:

Object can call methods implemented by its parent class

There’s another important Objective-C keyword — super. Sending a message to super is a way of calling a method of a parent class in an inheritance chain. The most common scenario in which the super keyword is used is when overriding a method.

Suppose we want to create a ShoutingPerson class similar to the Person class, with all output in uppercase letters. We could have copied the entire XYZPerson class and changed the corresponding implementation to print uppercase letters, but it would have been easier to use inheritance and override the saySomething: method to print uppercase letters:

@interface XYZShoutingPerson : XYZPerson
@end
Copy the code
@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
    NSString *uppercaseGreeting = [greeting uppercaseString];
    NSLog(@"%@", uppercaseGreeting);
}
@end
Copy the code

This example declares an additional string object pointer to uppercaseGreeting, then sends the uppercaseString message to the greeting object and assigns the return value of the message to uppercaseGreeting. As we saw earlier, this is a newly generated string object by converting the original string to fully uppercase.

Since sayHello is implemented by XYZPerson and XYZShoutingPerson inherits from XYZPerson, we can also call the sayHello method on the XYZShoutingPerson instance object. When we call the sayHello method on XYZShoutingPerson instance object, [self saySomething:… Will use the rewritten implementation, output uppercase string, the program flow is as follows:

However, our new implementation of XYZShoutingPerson is not ideal. If we later decide to change the saySomething: method of XYZPerson and want the output to be displayed in the text box, we would have to change the saySomething: method of XYZShoutingPerson as well.

It would be better to modify the saySomething: method of XYZShoutingPerson to handle strings by calling the parent’s implementation:

@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
    NSString *uppercaseGreeting = [greeting uppercaseString];
    [super saySomething:uppercaseGreeting];
}
@end
Copy the code

The program flow is as follows:

Objects are created dynamically

As described earlier in this chapter, memory for Objective-C objects is created dynamically. The first step in creating an object is to allocate enough memory for the object, not only for the attributes of the object’s own class, but also for the attributes of all the classes in the inheritance chain.

The root class NSObject provides a class method, alloc, that handles the flow for us.

+ (id)alloc;
Copy the code

Notice that the return type of this method is id. This is a special keyword in Objective-C that stands for “some object “. It’s a pointer to an object, just like NSObject. But the special thing about ids is that they don’t use asterisks. This will be described in more detail later.

The alloc method has another important task: clean up the space allocated to object properties and set them all to 0. This avoids garbage stored before memory remains, but it is not enough to fully initialize an object.

We need to combine the alloc and init methods:

- (id)init;
Copy the code

The object uses the init method to ensure that all of its properties have appropriate initial values when they are created, as explained in the next article.

Notice that the return type of the init method is also id.

If a method returns a pointer to an object. Nest method calls can be nested. An object can be properly allocated and initialized with a nested call to alloc init:

NSObject *newObject = [[NSObject alloc] init];
Copy the code

The example above has the variable newObject pointing to a newly created instance of NSObject.

In the nested hierarchy, the innermost method is called first, so the NSObject class receives the alloc message, and then returns an instance object of NSObject that allocates memory, and that instance object acts as the message receiver of the init message, Finally, an initialized object is returned and the newObject pointer is assigned, as shown below:

Note: Init may return a different object than alloc. So it’s best to nest calls to alloc init like above.

Never initialize an object like this without assigning it to any variable.

// Don't do that! NSObject *someObject = [NSObject alloc]; [someObject init];Copy the code

If the init method returns another object, someObject points to an NSObject that has been allocated memory but not initialized.

Add parameters to the initialization method

Some objects need to be supplied with the required values at initialization time. An NSNumber object, for example, must be initialized with a value that it represents. The NSNumber class defines some initialization methods:

- (id)initWithBool:(BOOL)value;
- (id)initWithFloat:(float)value;
- (id)initWithInt:(int)value;
- (id)initWithLong:(long)value;
Copy the code

Parameterized initializers are called just like parameterless initializers:

NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42];
Copy the code

Class factory methods provide a quick path to memory allocation + initialization

As mentioned above, a class can define factory methods. The factory method provides a shortcut to alloc init, eliminating the need to nest calls to the alloc init method.

The NSNumber class defines factory methods corresponding to its initialization method:

+ (NSNumber *)numberWithBool:(BOOL)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithLong:(long)value;
Copy the code

The factory method can be called like this to get the instance object:

NSNumber *magicNumber = [NSNumber numberWithInt:42];
Copy the code

This is almost the same as using alloc ‘ ‘initWithInt above. Class factory methods are also generally implemented only through alloc and related init methods, but are much easier to use.

This parameter is used when no initialization parameters are needednewMethod to create an object

You can use the new class method to create new class instance objects. This method is provided by the NSObject class, and subclasses do not need to override it.

It’s almost equivalent to calling alloc and init with no arguments.

XYZObject *object = [XYZObject new];
    // is effectively the same as:
    XYZObject *object = [[XYZObject alloc] init];
Copy the code

Create objects quickly using Literal Syntax

Some classes allow instance objects to be created with a more concise literal syntax.

For example, we can quickly create a string object using the @ literal:

NSString *someString = @"Hello, World!" ;Copy the code

This is almost equivalent to alloc init an NSString or using a class factory method.

NSString *someString = [NSString stringWithCString:"Hello, World!"
                                              encoding:NSUTF8StringEncoding];
Copy the code

The NSNumber class also provides a lot of literal syntax:

NSNumber *myBOOL = @YES; NSNumber * myFloat = @ 3.14 f; NSNumber *myInt = @42; NSNumber *myLong = @42L;Copy the code

The above examples are also equivalent to calling alloc init or class factory methods.

We can also create an NSNumber instance using expressions:

NSNumber *myInt = @(84 / 2);
Copy the code

In the example above, the value of the expression is evaluated, and then the NSNumber instance is created from that value.

Objective-c also supports literal syntax for creating NSArray and NSDictionary objects

Objective-c is a dynamic language

As mentioned earlier, we need Pointers to keep track of the object’s memory. Due to the dynamic nature of Objective-C, it doesn’t matter what type the pointer to the object is. When we send a message to it, Objective-C will always find the right method to call it.

The ID type defines a wildcard object type pointer. We can use the ID keyword when defining object variables, but we will lose compile-time information about the object.

id someObject = @"Hello, World!" ; [someObject removeAllObjects];Copy the code

The removeAllObjects message is defined by Cocoa or Cocoa Touch objects (such as NSMutableArray). So the compiler does not report an error. However, at run time the NSString object cannot respond to the removeAllObjects message, resulting in a runtime exception.

Rewrite the code above to use static typing:

NSString *someObject = @"Hello, World!" ; [someObject removeAllObjects];Copy the code

At this point, the compiler will report an error: removeAllObjects is not found in the NSString public interface declaration.

Since an object’s class is determined at run time, it makes no difference whether the object’s class is specified at creation time or at use time.

XYZPerson *firstPerson = [[XYZPerson alloc] init];
    XYZPerson *secondPerson = [[XYZShoutingPerson alloc] init];
    [firstPerson sayHello];
    [secondPerson sayHello];
Copy the code

While both firstPerson and secondPerson are statically specified as type XYZPerson, at run time secondPerson points to a variable of type XYZShoutingPerson. When the sayHello message is sent to both objects, the runtime finds the correct implementation. For secondPerson, the sayHello method in XYZShoutingPerson is used.

Determines whether objects are equal

If we need to determine whether two objects are equal, we need to keep in mind that we are getting objects through Pointers.

The == operator of standard C is used to compare the values of two variables:

if (someInteger == 42) {
        // someInteger has the value 42
    }
Copy the code

When we compare objects, the == operator is used to compare whether two Pointers point to the same object:

if (firstPerson == secondPerson) {
        // firstPerson is the same object as secondPerson
    }
Copy the code

When we need to compare whether two objects represent the same data, we need to call a method like isEqual: provided by NSObject.

If we need to compare the size of values represented by two objects, we should not use C’s comparison operator < or >. Instead, use the basic Foundation types NSNumber, NSString, and NSDate that provide the compare: method:

if ([someDate compare:anotherDate] == NSOrderedAscending) {
        // someDate is earlier than anotherDate
    }
Copy the code

usenil

When defining variables of value type, it is best to always initialize them at definition, otherwise their initial values may contain previous memory garbage.

BOOL success = NO;
    int magicNumber = 42;
Copy the code

But this is not necessary for object type Pointers. If we don’t specify an initial value for an object, the compiler automatically assigns it nil

XYZPerson *somePerson;
    // somePerson is automatically set to nil
Copy the code

It’s safest to use nil if we don’t have a value for an object to initialize. It’s perfectly fine to send messages to nil in Objective-C. If you send a message to nil, nothing happens.

Note: If we want to get the return value of a message sent to nil, we get nil if the return type is an object type, 0 if it’s a numeric type, and NO if it’s BOOL. All members of the returned structure are initialized to 0.

If we need to check to make sure an object is not nil, we can use C! = operator:

if (somePerson ! = nil) { // somePerson points to an object }Copy the code

Or directly provide variables as a judgment condition:

if (somePerson) {
        // somePerson points to an object
    }
Copy the code

If somePerson is nil, its logical value is 0(false). If it has an address, the address must not be 0(true).

Similarly, if we need to check if an object is nil, we can use ==, or we can just use it! :

if (somePerson == nil) {
        // somePerson does not point to an object
    }
Copy the code
if (! somePerson) { // somePerson does not point to an object }Copy the code

Practice:

  1. Using the project from the previous exercise, open the main.m file and find the main() function, which, like any other C-written executable, is the entry function to your application. Create a new XYZPerson instance with alloc init and call its sayHello method.

    Note: If the compiler does not automatically prompt you, you need to introduce the XYZPerson header file in main.m first.

  2. Implement saySomething:, override sayHello, and call saySomething in sayHello. Add some “hello” methods and call them in the instance you create.

  3. Create a new class that inherits from XYZShoutingPerson, override the saySomething: method to print the corresponding all-uppercase character, and then create an instance of XYZShoutingPerson to test if the result is as expected.

  4. Implement the Person factory method of the XYZPerson class — return a properly allocated and initialized XYZPerson instance, and then replace the XYZPerson instance created using alloc init in main.m.

    Try using [[self alloc] init] instead of [[XYZPerson] alloc] init] in the class factory. Use self in a class method; self is equivalent to the class itself. This way we don’t have to override the Person method in the XYZShoutingPerson class and can create the instance object correctly. You can test to see if you have created an instance object of the correct type:

XYZShoutingPerson *shoutingPerson = [XYZShoutingPerson person];
Copy the code
  1. Create a new oneXYZPersonPointer, do not assign a value to it, use conditional checks to see if the variable is automatically set tonil

Reference: Working with Objects