Effective Objective-C: Getting Familiar with Objective-C
A Summary of Chapter One: Accustoming Yourself to Objective-C
Introduction
With Swift getting more and more popular, stable, and reliable. It's kind of doesn't make sense to start a project with Objective-C. In my case, I'm learning Objective-C because we're maintaining legacy code at my company Instabug which is in Objective-C.
I've been using Objective-C for the past few months at my work. I've been exposed to a lot of Objective-C code. But honestly, to get started into writing Objective-C I just read Objective-C for Swift Developers from Hacking With Swift. It is a good book to read if you're getting started with Objective-C but, at some point, you'll need to eventually read a more in depth book about Objective-C.
That's because:
Writing Objective-C can be learned quickly but has many intricacies to be aware of and features that are often overlooked. Similarly, some features are abused or not fully understood, yielding code that is difficult to maintain or to debug.
The Effective Objective-C book has a good reputation and I think it's a good dive into Objective-C. That's why I decided to read it this month.
In this series, I'll be creating summary of each chapter of the book and discussing what I think about the tips available in the book.
Please, if you like the content of the book don't forget to buy the book itself to support the author.
In this article, we will be discussing a few of the most fundamental topics in Objective-C.
Table of Contents
- Familiarize Yourself with Objective-C Roots
- History with Smalltalk
- History with C
- Minimize Importing Headers in Headers
- Prefer Literal Syntax over the Equivalent Methods
- Arrays
- Prefer Typed Constants to Preprocessor #define
- Local Constants
- Global Constants
- Use Enumerations for States, Options, and Status Codes
- Conclusion
Familiarize Yourself with Objective-C Roots
History with Smalltalk
A bit of history, that I didn't know before, is that Objective-C is evolved from a programming language called Smalltalk.
One of the first things you'll notice in Objective-C is that it uses a messaging structure rather than function calling structure like many other programming languages. That messaging structure was one of the things evolved from Smalltalk.
// Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);
// Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
A key difference between messaging and function calling is:
Messaging | Function calling |
The runtime decides which code gets executed | The compiler decides which code will be executed |
When polymorphism is introduced to the function calling example, a form of runtime lookup is involved through what is known as a virtual table. But with messaging, the lookup is always at runtime.
History with C
It's fair to say that Objective-C is a superset of C, so all the features in the C language are available when writing Objective-C.
One of the important things is that understanding the memory model of C will help you to understand the memory model of Objective-C and why reference counting works the way it does.
To declare an object in Objective-C you'll need to write something like this:
NSString *someString = @"The string";
All Objective-C objects must be declared in this way because the memory for objects is always allocated in heap space and never on the stack. It is illegal to declare a stack allocated Objective-C object.
Since, the Objective-C objects are allocated on the heap when doing something like this:
NSString *someString = @"The string";
NSString *anotherString = someString;
There will be only one NSString
instance here.
Sometimes in Objective-C, you will encounter variables that don’t have a *
in the definition and might use stack space. These variables are not holding Objective-C objects.
Creating objects (i.e. classes) incurs overhead that using structures (i.e. structs) does not, such as allocating and deallocating heap memory. When nonobject types (int
, float
, double
, char
, etc.) are the only data to be held, a struct is usually used.
Minimize Importing Headers in Headers
Objective-C, just like C and C++, makes use of header files and implementation files.
When you have a property in a class that's of another class's type. You might think that you should import the header file of the second class inside of your first class.
// EOCPerson.h
#import <Foundation/Foundation.h>
#import "EOCEmployer.h" // Bad practice.
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
This would work, but it’s bad practice. To compile anything that uses EOCPerson
, you don’t need to know the full details about what an EOCEmployer
is. All you need to know is that a class called EOCEmployer
exists. Fortunately, there is a way to tell the compiler this is called forward declaring the class.
// EOCPerson.h
#import <Foundation/Foundation.h>
@class EOCEmployer; // Good practice.
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) EOCEmployer *employer;
@end
Deferring the import to where it is required enables you to limit the scope of what a consumer of your class needs to import.
Using forward declaration also alleviates the problem of both classes referring to each other. Consider what would happen if EOCEmployer
had methods to add and remove employees, defined like this in its header file.
This time, the EOCPerson
class needs to be visible to the compiler, for the same reasons as in the opposite case. However, achieving this by importing the other header in each header would create a chicken and egg situation.
Sometimes, though, you need to import a header in a header. You must import the header that defines the superclass from which you are inheriting. Similarly, if you declare any protocols that your class conforms to, they have to be fully defined and not forward declared. The compiler needs to be able to see the methods the protocol defines rather than simply that a protocol does exist from a forward declaration.
Finally, when writing an import into a header file, always ask yourself whether it’s really necessary. If the import can be forward declared, prefer that. If the import is for something used in a property, instance variable, or protocol conformance and can be moved to the class continuation category, prefer that. Doing so will keep compile time as low as possible and reduce interdependency, which can cause problems with maintenance or with exposing only parts of your code in a public API should ever you want to do that.
Prefer Literal Syntax over the Equivalent Methods
Objective-C is well known for having a verbose syntax. That’s true. However, ever since Objective-C 1.0, there has been a very simple way to create an NSString
object. It is known as a string literal. Without this type of syntax, creating an NSString
object would require allocating and initializing an NSString
object in the usual alloc
and then init
method call.
Using the literal syntax reduces source code size and makes it much easier to read.
Arrays
Arrays are a commonly used data structure. Before literals, you would create an array as follows:
NSArray *animals = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", @"badger", nil];
Using literals, however, requires only the following syntax:
NSArray *animals = @[@"cat", @"dog", @"mouse", @"badger"];
However, you need to be aware of one thing when creating arrays using the literal syntax. If any of the objects is nil, an exception is thrown. So, in the following scenario:
id object1 = /* ... */; // valid
id object2 = /* ... */; // nil object
id object3 = /* ... */; // valid
NSArray *arrayA = [NSArray arrayWithObjects: object1, object2, object3, nil];
NSArray *arrayB = @[object1, object2, object3];
Consider that object1
and object3
point to valid Objective-C objects, but object2
is nil
. The literal arrayB
, will cause the exception to be thrown. However, arrayA
will still be created but will contain only object1
. The reason is that the arrayWithObjects:
method looks through the variadic arguments until it hits nil
, which is sooner than expected.
This subtle difference means that literals are much safer. It’s much better that an exception is thrown, causing a probable application crash, rather than creating an array having fewer than the expected number of objects in it. A programmer error most likely caused nil to be inserted into the array, and the exception means that the bug can be found more easily.
Finally, I think we should use the literal syntax to create strings, numbers, arrays, and dictionaries. It is clearer and more succinct than creating them using the normal object creation methods.
Prefer Typed Constants to Preprocessor #define
Local Constants
When writing code, you will often want to define a constant. You might use a #define
preprocessor like:
#define ANIMATION_DURATION 0.3
But this definition has no type information. It is likely that something declared as a “duration” means that the value is related to time, but it’s not made explicit. Also, the preprocessor will blindly replace all occurrences of ANIMATION_DURATION
, so if that were declared in a header file, anything else that imported that header would see the replacement done.
To solve these problems, you should make use of the compiler. There is always a better way to define a constant than using a preprocessor define. For example, the following defines a constant of type NSTimeInterval
:
static const NSTimeInterval kAnimationDuration = 0.3;
It is important that the variable is declared as both static and const. The const qualifier means that the compiler will throw an error if you try to alter the value. The static qualifier means that the variable is local to the translation unit in which it is defined.
A translation unit is the input the compiler receives to generate one object file. In the case of Objective-C, this usually means that there is one translation unit per class: every implementation (.m) file.
Global Constants
Sometimes, you will want to expose a constant externally. Such constants need to appear in the global symbol table to be used from outside the translation unit in which they are defined. There- fore, these constants need to be declared in a different way from the static const example. These constants should be defined like so:
// In the header file
extern NSString *const EOCStringConstant;
// In the implementation file
NSString *const EOCStringConstant = @"VALUE";
The constant is “declared” in the header file and “defined” in the implementation file. In the constant’s type, the placement of the const qualifier is important. The extern
keyword in the header tells the compiler what to do when it encounters the constant being used in a file that imports it. The keyword tells the compiler that there will be a symbol for EOCStringConstant
in the global symbol table. This means that the constant can be used without the compiler’s being able to see the definition for it. The compiler simply knows that the constant will exist when the binary is linked.
In conclusion, avoid using preprocessor defines for constants. Instead, use constants that are seen by the compiler, such as static const globals declared in implementation files.
Use Enumerations for States, Options, and Status Codes
An enumeration is nothing more than a way of naming constant values. A simple enumeration set might be used to define the states through which an object goes. For example, a socket connection might use the following enumeration:
enum EOCConnectionState {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef enum EOCConnectionState EOCConnectionState;
So when you represent a EOCConnectionState state the code will be:
EOCConnectionState state = EOCConnectionStateDisconnected;
Using an enumeration means that code is readable, since each state can be referred to by an easy-to-read value. The compiler gives a unique value to each member of the enumeration, starting at 0 and increasing by 1 for each member. It’s also possible to define the value a certain enumeration member relates to rather than letting the compiler choose for you.
One final extra point about enumerations has to do with using a switch statement. Sometimes, you will want to do the following:
typedef NS_ENUM(NSUInteger, EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
switch (_currentState) {
EOCConnectionStateDisconnected:
// Handle disconnected state
break;
EOCConnectionStateConnecting:
// Handle connecting state
break;
EOCConnectionStateConnected:
// Handle connected state
break;
}
Things to note about enums,
- Use enumerations to give readable names to values used for the states of a state machine, options passed to methods, or error status codes.
- If an enumeration type defines options to a method in which multiple options can be used at the same time, define its values as powers of 2 so that multiple values can be bitwise OR’ed together.
- Use the
NS_ENUM
andNS_OPTIONS
macros to define enumeration types with an explicit type. Doing so means that the type is guaranteed to be the one chosen rather than a type chosen by the compiler. - Do not implement a default case in switch statements that handle enumerated types. This helps if you add to the enumeration, because the compiler will warn that the switch does not handle all the values.
Conclusion
In this chapter we leaned about the history of Objective-C and a few important things to consider when writing in Objective-C like the correct use of header files, the importance of using literal definitions, best practices when working with global constants, and finally the uses of enums.
I hope you enjoyed reading this article, if so don't forget to share and if you have any feedback please add a comment below.