This is the third article in the Objective-C series.

  • Objective-c (1) Familiar with Objective-C
  • Objective-c (2) Objects, messages, and runtime
  • Objective-c language (3) Interface and API design
  • Objective-c (4) Protocols and classification
  • Objective-c language (5) System framework
  • Objective-c (6) Block and GCD

1. Best practices

  • Prefix all class names appropriately

  • Provides “universal initialization methods”

    • Provide a universal initialization method in a class and specify it in the documentation. All other initialization methods should call this method;
    • If the omnipotent initialization method is different from the superclass method, the corresponding method in the superclass needs to be overwritten.
    • If the initialization method of a superclass method does not apply to a subclass, you should override the superclass method and throw an exception in it.
  • The description method

    • Implement the description method to return a meaningful string that describes the instance;
    • If you want to print a more detailed object description during debugging, you should implement the debugDescription method.
    • You can look for plug-ins that implement this to quickly generate this method.
  • Try to create immutable objects;

    • If an attribute is only available for internal object modification, it is present in the class-CONTINUATION categoryreadonlyProperty extends toreadwrite;
    • Instead of exposing mutable collections as properties, provide methods to modify mutable collections in objects.
  • named

    Refer to clear and coordinated naming

  • copy

    • If you want to copy your written objects, you need to implement the NSCopying protocol.
    • If the custom object is mutable and immutable, then both NSCopying and NSMutableCopying are implemented.
    • When copying objects, you need to decide whether to use shallow copy or deep copy. Generally, you should perform shallow copy as much as possible.
    • If you are writing objects that require deep copies, consider adding a method that performs deep copies exclusively.

Second, practical explanation

2.1 Use prefixes to avoid namespace Conflicts

Do you encounter the following errors?

duplicate symbol _OBJC_METACLASS_$DuplicateTheClass in 
 	build /somethings.o
 	build /something_else.o
Copy the code

The above error is caused by the DuplicateTheClass class being defined twice each in both copies of the application code.

The only way to avoid this problem is to implement namespaces in disguise: give all names the appropriate prefix.

Prefixes can be words associated with a company, application, or function module, or even a person’s identity. But note that Apple says it reserves all right to use “two-letter prefixes,” so your own prefixes should be three.

It is also important to note that the pure C functions and global variables used in the class implementation file are “top-level symbols” in the compiled object file.

At the same time, when importing third-party libraries, be careful not to make your own names overlap with those of other libraries.

2.2 Provide “universal initialization Method”

  • Provide a universal initialization method in a class and specify it in the documentation. All other initialization methods should call this method;
  • If the omnipotent initialization method is different from the superclass method, the corresponding method in the superclass needs to be overwritten.
  • If the initialization method of a superclass method does not apply to a subclass, you should override the superclass method and throw an exception in it.

2.3 Implement the Description method

  • Implement the description method to return a meaningful string that describes the instance;
  • If you want to print a more detailed object description during debugging, you should implement the debugDescription method.
  • You can look for plug-ins that implement this to quickly generate this method.

2.4 Use immutable objects whenever possible

When designing classes, make full use of attributes to encapsulate data. When an attribute is used, it can be declared as readonly. The default properties are readwrite.

When designing a class, set some properties that do not need to be changed to Readonly to prevent them from being changed.

@interface HOReadOnlyProperty : NSObject
@property (nonatomic ,readonly ,copy)NSString *name;
@end
Copy the code

In this case, although readonly is specified, there is no set method, but memory management semantics are specified so that it can be changed to read and write properties later.

Sometimes you want to modify the data encapsulated inside an object, but you don’t want that data to be modified by outsiders. The readonly property is typically redeclared as readwrite inside the object. As follows:

@interface HOReadOnlyProperty(a)
@property (nonatomic ,readwrite ,copy)NSString *name;
@end

@implementation HOReadOnlyProperty
@end
Copy the code

This allows you to change the property value only internally.

! But! There is no guarantee that external property values will not change, as changes can still be made through KVC. SetValue :forKey:.

In addition, when defining the properties that represent various collections, a readonly property is typically provided for external use, which returns an immutable set that is a copy of the internal mutable set. For example:

@interface HOPerson : NSObject
@property (nonatomic ,copy)NSString *name;
@property (nonatomic ,strong ,readonly)NSSet *friends;
@end
Copy the code

Implementation file:

@interface HOPerson(a)
{
    NSMutableSet *_internalFriends;
}

@end

@implementation HOPerson

- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        _name = name;
        _internalFriends = [NSMutableSet set];
    }
    return self;
}

- (NSSet *)friends
{
    return [_internalFriends copy];
}

- (void)addFriend:(HOPerson *)person
{
    [_internalFriends addObject:person];
}

- (void)removeFriend:(HOPerson *)person
{
    [_internalFriends removeObject:person];
}

@end
Copy the code
  • Try to create immutable objects;
  • If an attribute is only available for internal object modification, it is present in the class-CONTINUATION categoryreadonlyProperty extends toreadwrite;
  • Instead of exposing mutable collections as properties, provide methods to modify mutable collections in objects.

2.5 Use clear and coordinated naming

2.5.1 Method Naming Rules

  • If the return value of a method is newly created, the first word of the method name should be the type of the return value, for examplestringWithString. Unless it’s preceded by a qualifier likelocalizedString. Property access methods do not follow this naming pattern because they are not expected to create new objects, even if they sometimes return a copy of the internal object, which we consider equivalent to the original object. These access methods should be named according to their corresponding properties;
  • You should place the noun that describes the parameter type before the parameter. Such as- (void) getCharacters: (unichar *) buffer range: (aRange NSRange)In therangeThe type of argument expressed;
  • If a method is to perform an operation on the current object, it should include a verb; If you need parameters to perform an operation, you should add one or more nouns after the verb, such as:lowercaseString;
  • Don’t usestrThis abbreviation should be usedstringSuch a full name;
  • The Boolean attribute should be addedisPrefix. If a method returns a Boolean value that is not an attribute, it should be selected based on its functionalityhasorisAs the prefix;

2.5.2 Naming classes and Protocols

Class and protocol names should be prefixed to avoid namespace conflicts. Add nouns that identify classes and protocols such as View, ViewController, Delegate, and Protocol.

  • The method name should be concise and read from left to right like an everyday sentence.
  • Do not use abbreviated type names in method names;
  • The first thing to do when naming a method is to make sure that the style matches your own code or the framework you are integrating with.

2.5.3 Prefixing private method names

There is often more to a class than meets the eye. When you write implementation code for a class, you often write methods that are only used internally. This method should be prefixed to help with debugging because it is easy to distinguish public from private methods.

In addition, the presence of prefixes makes it easy to change method names or method signatures, meaning that these prefixed methods can be changed at any time without worrying about affecting the external-facing apis.

The exact prefix you use is up to your personal preference, but it’s best to include underscores and the letter P. P stands for private, underlined to distinguish it from the real method name. So the private method name is written as:

- (void)p_privateMethod {
		/.../	
}
Copy the code
  • Prefix the names of private methods so that they can be easily distinguished from public methods;

  • Do not use a single underscore prefix, as this method is reserved for Apple.

2.6 Understand the Objective-C error model

Objective-c also has an exception mechanism, but it is not “exception safe” by default. This means that if an exception is thrown, the object selection that should be released at the end of the action is not automatically released.

If you want to generate “exception-safe” code, you can do so by setting the compile flag, but this introduces some extra code that will be executed when no exceptions are thrown. The compiler flag to turn on is: -fobjc-arc-exceptions.

That’s ARC, but what about MRC?

Even with MRC, it is difficult to write code that throws exceptions without causing a memory leak. For example, a resource can be created and released after it is used, but if an exception is thrown before it is released, the resource will not be released. One solution to this problem is to release resources before throwing exceptions. This can solve the problem, but if there are too many resources and the execution path is complex, there can be confusion as to what resources to release when an exception is thrown.

The objective-C solution to the above problem is to throw an exception only in the rarest of cases, after which recovery is not a concern, and the application should exit. In other words, there is no need to write complex “exception-safe” code.

Exceptions should only be used for extremely serious errors.

For “nonfatal errors,” objective-C uses a programming paradigm that makes methods return nil/0 or uses NSError to indicate that an error has occurred.

For example, when init, return nil, which means initialization failed.

NSError is a flexible object that returns the cause of the error to the caller.

- (instancetype)errorWithDomain:(NSErrorDomain)domain code:(NSInteger)code userInfo:(nullable NSDictionary *)dict;
Copy the code
  • Error Domain (Error range, which is of type string)

    The root cause of an error, usually defined by a unique global variable. The system provides some, see the nseror.h header file.

  • Error code (an Error code of type integer)

  • User Info (User information, of type dictionary)

So how is the caller informed of the error, that is, by what means?

The first common use is to pass this error through a delegate protocol.

- (void)URLSession:(__unused NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 
Copy the code

Another common use is to return it to the caller via the “output parameter” of a method.

- (BOOL)doSomething:(NSError **)error 
{
  if(/* error logic */) {if(error) {
		// Error must be judged first. If nil, dereferencing will result in a "segment fault" and the program will crash
		*error = [NSError errorWithDomain:domain code:code userinfo:userinfo];
	}	
	return NO;
  } else {
    return YES; }}/ / call
NSError *error = nil;
BOOL ret = [object doSomething:&error];
if(error){
	// Processing error
}
Copy the code

Here is an example of a domain and code code:

//HOErrors.h
extern NSString *const HOErrorDomain;

typedef NS_ENUM(NSUInteger, HOError) {
  HOErrorUnknown = - 1,
  HOErrorInternalInconsistency = 100,}//HOErrors.m
NSString *const HOErrorDomain = @"HOErrorDomain"
Copy the code

2.7 Understand the NSCopying protocol

When you work with objects, you often need to copy them. In Objective-C, this is done using the copy method. If you want a class to support copy, you need to implement the NSCopying protocol, which has only one method:

- (id)copyWithZone:(NSZone*)zone;
Copy the code

The zone was previously developed to divide memory into different “zones”, and objects are created in a zone. No longer. Each program has only one zone: the default zone. So, forget about the zone parameter.

Sometimes, you need to implement mutable copy, following the NSMutableCopying protocol, to implement:

- (id)mutableCopyWithZone:(NSZone*)zone;
Copy the code

When writing a copy method, you also need to decide whether to perform a “deep copy” or a “shallow copy.” In deep copy, the underlying data is copied as well as the object itself. All Collection classes in the Foundation framework perform shallow copying by default, that is, copying only the object itself and not the data in it.

In general, we implement copying in the style that the system uses, implementing the “copyWithZone” method with a shallow copy. But if necessary, add a way to perform a deep copy. Taking NSSet as an example, this class implements the method of deep copy:

- (id)initWithSet:(NSArray*)array copyItems:(BOOL)copyItems;
Copy the code

If copyItems is YES, the method sends a copy message to each element in the array, creates a new set with the copied elements, and returns it to the caller.

  • If you want to copy your written objects, you need to implement the NSCopying protocol.
  • If the custom object is mutable and immutable, then both NSCopying and NSMutableCopying are implemented.
  • When copying objects, you need to decide whether to use shallow copy or deep copy. Generally, you should perform shallow copy as much as possible.
  • If you are writing objects that require deep copies, consider adding a method that performs deep copies exclusively.