那些设计iOS API须要知道的事

原文地址:http://chars.tech/2017/07/09/...ios

为了可以将咱们项目中的代码可以在后续开发者使用(重用代码),一般使用的方法是将代码按照功能模块编写成API。那么咱们就颇有必要了解Objective-C语言中常见的编程范式(paradigm),同时还需了解各类可能碰到的陷阱。git

命名

命名冲突的问题

Objective-C没有其余语言的那种内置命名空间(namespace)机制。所以,咱们只能本身想办法来解决命名冲突问题。最经常使用的解决方式就是,仿照其余语言(C++)创建本身的namespace,例如,使用前缀。编程

所选前缀能够是与公司、应用程序或两者皆有关联之名。例如,ZAKER User Interface可使用ZUI做为前缀。使用Cocoa建立应用程序时必定要注意,Apple宣称其保留使用全部“两字母前缀”(two-letter prefix)的权利,因此开发者选用的前缀应该是三个字母的。若是开发者使用了两个字母做前缀,那么颇有可能开发者自定义的API和Apple的API冲突。api

不只仅是类名,应用程序中的全部名称都应该加前缀。若是要为既有类新增“分类”(category),那么必定要给“分类”及“分类”中的方法加上前缀。另外,类的实现文件中所用的纯C函数及全局变量也应该注意添加前缀。数组

若是使用了第三方库编写本身的代码,并准备将其发布为程序库供他人开发应用程序所用,则尤为要注意重复符号问题。这种状况下为了不使用者使用了与你相同的第三方库,应该为第三方库都加上你本身的前缀。安全

第三方库引入使用前缀

命名方式

类、方法和变量的命名是Objective-C编程的重要环节。若是命名方式好,能够提升代码可读性,减小没必要要的注释。
初学者一般会以为Objective-C是门很繁琐的语言,由于其语法结构使得代码读起来和句子同样。命名中通常都带有“in”、“for”、“with”等介词,特别是在命名时还要讲究英文语法。例如:数据结构

NSString *text = @"This is a good idea.";
NSString *newText = [text stringByReplacingOccurrencesOfString:@"idea" withString:@"think"];

上面的代码虽然用了比较啰嗦的方式描述一个看上去很简单的表达式。对于执行替换的那个方法,代码读起来就像平常语言里的那个句子:“Take text and give me a new string by replacing the occurrences of the string 'idea' with the string 'think'”。
这个句子准确描述了开发者想作的事。在命名不像Objective-C这般繁琐的语言中,相似的程序可能会写成:dom

string text = "This is a good idea.";
string new Text = text.replace("idea", "think");

上面代码这样写,看起来方法名简洁不少,可是带来的代码不可读性倒是很是大的。首先,咱们不知道 text.replace 方法的两个参数到底按照什么顺序解读(除非查看方法声明);再者,这两个参数谁替换谁?编程语言

另外,和大多数语言同样,Objective-C也是采用“驼峰式大小写命名法”(camel casing)——以小写字母开头,其后每一个单词首字母大写。ide

方法命名

清晰的方法名从左至右读起来好似一段文章。并非说非得按照那些命名规则来给方法起名,不过这样作能够令代码变得更好维护,使他人更容易读懂。
虽然相似C++或Java中那种函数命名简单,可是,若想知道每一个参数的用途,就得查看函数原型,这会令代码难于读懂。
NSString这个类展现了一套良好的命名习惯。下面列举几个方法及命名原因:

1)+ (instancetype)string;
工厂方法(factory method),用于建立新的空字符串。方法名清晰地描述了返回值的类型。

2)+ (instancetype)stringWithString:(NSString *)string;
工厂方法,根据某字符串建立出与以内容相同的新字符串。与建立空字符串所用的那个工厂方法同样,方法名的第一个单词也指明了返回类型。

3)+ (instancetype)localizedStringWithFormat:(NSString *)format, ...;
工厂方法,根据特定格式建立出新的“本地化字符串”(localized string)。返回值类型是方法名的第二个单词(string),由于其前面还有个修饰语(localized)用来描述其逻辑含义。此方法的返回值依然是“字符串”(string),只不过是一种通过本地化处理的特殊字符串。

4)- (NSUInteger)lengthOfBytesUsingEncoding:(NSStringEncoding)enc;
若字符串是以给定的编码格式(ASCII、UTF八、UTF16)来编码的,则返回其字节数组长度。此方法与length类似,但该方法还需一个参数,该参数紧跟着方法名中描述其类型的那个名词(encoding)。

所以,咱们能够总结成几条方法命名规则:

1)若是方法的返回值是新建立的,那么方法名的首个词应该是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,由于通常认为这些方法不会建立新对象。即使有时返回内部对象的一份拷贝,咱们也认为那至关于原有对象。这些存取方法应该按照其所对应的属性来命名。

2)应该把表示参数类型的名词放在参数前面。

3)若是方法要在当前对象上执行操做,那么就应该包含动词;若执行操做时还须要参数,则应该在动词后面加上一个或多个名词。

4)不要使用str这种简称,应该使用string这样的全称。

5)boolean属性应加is前缀。若是某方法返回非属性的boolean值,那么应该根据其功能,选用has或is当前缀。

6)将get这个前缀留给那些借由“输出参数”来保存返回值的方法,好比说,把返回值填充到“C语言式数组”(C-style array)里的那种方法就可使用这个词作前缀。

类与协议命名

不只仅是方法,类和协议也应该加上前缀,避免命名空间冲突。例如:

  • UIView
  • UIViewController
  • UITableViewDelegate

错误模型

目前有不少编程语言都有“异常”(exception)机制,Objective-C也不例外。

“自动引用计数”(ARC, Automatic Reference Counting)在默认状况下不是“异常安全的”。这意味着:若是抛出异常,那么本应该在做用域末尾释放的对象如今却不会自动释放了。若是想生成“异常安全”的代码,能够经过设置编译器的标志来实现,不过这将引入额外代码,在不抛出异常时,也照样要执行这部分代码。须要打开的编译器标志叫作-fobjc-arc-exception

Objective-C如今所采用的办法是:只在极其罕见的状况下抛出异常,异常抛出以后,无须考虑恢复问题,并且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。

异常只应该用于极其严重的错误,好比,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,而后使用这个子类。在这种状况下,若是有人直接使用了这个抽象基类,那么能够考虑抛出异常。与其余语言不一样,Objective-C中没办法将某个类标识为“抽象类”。要想达成相似效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。

异常只用于处理严重错误(fatal error),对于其余错误,Objective-C语言所用的编程范式为:令方法返回nil/0,或使用NSError,以代表有错误发生。

NSError对象里封装了三条信息:

  • Error domain (错误范围,其类型为字符串)

错误发生的范围,也就是产生错误的根源,一般用一个特有的全局变量来定义。例如,URL-handling-subsystem,在从URL中解析或获取数据时若是出错了,那么就使用NSURLErrorDomain来表示错误范围。

  • Error code (错误码,其类型为整数)

独有的错误码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误状况一般采用enum来定义。

  • User info (用户信息,其类型为字典)

有关此错误的额外信息,其中或许包含一段“本地化描述”,或许还包含有致使该错误发生的另一个错误,经由此种信息,可将相关错误串成一条“错误链”。

使用不可变对象

设计类的时候,应充分使用属性来封装数据。而在使用属性时,则可将其声明为readonly。默认状况下,属性是readwrite

由于若是把可变对象(mutable object)放入collection以后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。故此,咱们应该尽可能减小对象中的可变内容。具体到编程实践中,则应该尽可能把对外公布出来的属性设为readonly,并且只在有必要时才将属性对外公布。

定义类的公共API时,须要注意,对象里表示各类collection的那些属性究竟应该设成可变的,仍是不可变的。若是某个属性能够为外界所增删,那么这个属性就须要用可变的set来实现。在这种状况下,一般应该提供一个readonly属性供外界使用,该属性将返回不可变的set,而此set则是内部那个可变set的一份拷贝。

//  ZKRPointOfInterest.h

#import <UIKit/UIKit.h>

@interface ZKRPointOfInterest : NSObject

@property (nonatomic, copy, readonly) NSString *identifier;
@property (nonatomic, copy, readonly) NSString *title;
@property (nonatomic, assign, readonly) CGFloat latitude;
@property (nonatomic, assign, readonly) CGFloat longitude;
@property (nonatomic, strong, readonly) NSSet *locations;

- (instancetype)initWithIdentifier:(NSString *)identifier
                             title:(NSString *)title
                          latitude:(CGFloat)latitude
                         longitude:(CGFloat)longitude;

- (void)addLocation:(ZKRPointOfInterest *)location;
- (void)removeLocation:(ZKRPointOfInterest *)location;

@end

//  ZKRPointOfInterest.m

#import "ZKRPointOfInterest.h"

@implementation ZKRPointOfInterest
{
    NSMutableSet *_internalLocations;
}

- (instancetype)initWithIdentifier:(NSString *)identifier
                             title:(NSString *)title
                          latitude:(CGFloat)latitude
                         longitude:(CGFloat)longitude
{
    self = [super init];
    if (self) {
        
    }
    return self;
}

- (NSSet *)locations
{
    return [_internalLocations copy];
}

- (void)addLocation:(ZKRPointOfInterest *)location
{
    if (location) {
        [_internalLocations addObject:location];
    }
}

- (void)removeLocation:(ZKRPointOfInterest *)location
{
    [_internalLocations removeObject:location];
}

@end

注意:不要在返回的对象上查询类型以肯定其是否可变。(即便不用isKindOfClass:方法来判断返回值类型是否可变)

description方法

在调试程序时,常常须要打印并查看对象信息。一种办法是编写代码把对象的所有属性都log到日志中。NSLog(@"object=%@", object);

在构建须要打印到日志的字符串时,object对象会收到description消息,该方法所返回的描述信息将取代“格式字符串”(format string)里的“%@”。

NSArray *obj = @[@"A string", @(123)];
NSLog(@"object=%@", obj);

输出:

object=(
    "A string",
    123
)

若是在自定义类上这么作,那么则输出的信息倒是以下:

object=<ZKRSqure: 0x7656d8a90060>

若是想要像上面NSArray那样打印出有用的信息,那么咱们就应该在本身的类中覆写description方法,不然打印信息时就会调用NSObject类所实现的默认方法。此方法定义在NSObject协议里,不过NSObject类也实现了它。

- (NSString *)description
{
    return [NSString stringWithFormat:@"<%@: %p, \"%f %f\">", [self class], self, _width, _height];
}

使用结果:

ZKRRectangle *rectangle = [[ZKRRectangle alloc] initWithWidth:5.0 height:7.0];
NSLog(@"%@", rectangle);

//Output
<ZKRRectangle: 0x60000002fc20, "5.000000 7.000000">

NSObject协议中还有个须要注意的方法,就是debugDescription,此方法用意与description类似。两者区别在于,debugDescription方法是开发者在调试器(debugger)中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,它只是直接调用description

初始化方法

全部对象均要初始化,在初始化时,有些对象可能无须开发者向其提供额外信息,不过通常来讲仍是须要提供的。一般状况下,对象若不知道必要的信息,则没法完成其工做。例如,UITAbleViewCell类初始化该类对象时,须要指明其样式及标识符,标识符可以区分不一样类型的单元格。因为这种对象的建立成本较高,因此绘制表格时可依照标识符来复用,以提高程序效率。这种可为对象提供必要信息以便其能完成工做的初始化方法叫作“全能初始化方法”(designated initializer)。

若是建立类实例的方式不止一种,那么这个类就会有多个初始化方法。可是,咱们仍然须要选定一个做为全能初始化方法,令其余初始化方法都来调用它。例如,NSDate类

- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTimeIntervalSinceNow:(NSTimeInterval)secs;
- (instancetype)initWithTimeIntervalSince1970:(NSTimeInterval)secs;
- (instancetype)initWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;

在上面几个初始化方法中,initWithTimeIntervalSinceReferenceDate:是全能初始化方法。只有在全能初始化方法中,才会存储内部数据。这样的话,当底层数据存储机制改变时,只需修改此方法的代码就好,无须改动其余初始化方法。

示例代码:

//  ZKRRectangle.h

#import <UIKit/UIKit.h>

@interface ZKRRectangle : NSObject<NSCopying>

@property (nonatomic, assign, readonly) CGFloat width;
@property (nonatomic, assign, readonly) CGFloat height;

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;

@end

//  ZKRRectangle.m

#import "ZKRRectangle.h"

@implementation ZKRRectangle

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    if (self) {
        _width = [[aDecoder decodeObjectForKey:@"width"] floatValue];
        _height = [[aDecoder decodeObjectForKey:@"height"] floatValue];
    }
    return self;
}

- (instancetype)init
{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithWidth:height: instad." userInfo:nil];
    
    return [self initWithWidth:0 height:0];
}

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height
{
    self = [super init];
    if (self) {
        _width = width;
        _height = height;
    }
    return self;
}

@end

//  ZKRSquare.h

#import "ZKRRectangle.h"

@interface ZKRSquare : ZKRRectangle

- (instancetype)initWithDimension:(CGFloat)dimension;

@end

//  ZKRSquare.m

#import "ZKRSquare.h"

@implementation ZKRSquare

- (instancetype)init
{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instad." userInfo:nil];
    
    return [self initWithDimension:0];
}

- (instancetype)initWithDimension:(CGFloat)dimension
{
    return [super initWithWidth:dimension height:dimension];
}

- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height
{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instad." userInfo:nil];
    CGFloat dimension = MIN(width, height);
    return [self initWithDimension:dimension];
}

@end

小结

  • 在类中提供一个全能初始化方法,并于文档里指明。其余初始化方法均调用此方法。
  • 若全能方法于超类不一样,则须要覆写超类中的对应方法。
  • 若是超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

NSCopying协议

使用对象时常常须要拷贝它。在Objective-C中,此操做经过copy方法完成。若是想令本身的类支持拷贝操做,那就要实现NSCopying协议,该协议只有一个方法:

- (id)copyWithZone:(nullable NSZone *)zone;

为何会出现NSZone呢?由于之前开发程序时,会据此把内容分红不一样的“区”(zone),而对象会建立在某个区里面。如今不用了,每一个程序只有一个区:“默认区”(default zone)。因此说,尽管必须实现这个方法,可是你没必要担忧其中的zone参数。

copy方法由NSObject实现,该方法只是以“默认区”为参数来调用copyWithZone:。咱们老是想覆写copy方法,其实真正须要实现的是copyWithZone:方法。若想使某个类支持拷贝功能,只需声明该类听从NSCopying协议,并实现其中的那个方法便可。

- (id)copyWithZone:(NSZone *)zone
{
    ZKRRectangle *copy = [[[self class] allocWithZone:zone] initWithWidth:_width height:_height];
    return copy;
}

说到copy方法,除了NSString这样的不可变类型的copy,与之相似的还有NSMutableString类的mutableCopy方法。与copyWithZone:方法相对应的可变内容的copy方法mutableCopyWithZone:方法来自于NSMutableCopying协议。若是你的类分为可变版本(mutable)与不可变版本(immutable),那么就应该实现NSMutableCopying协议。若采用此模式,则在可变类中覆写copyWithZone:方法时,不要返回可变的拷贝,而应该返回一份不可变的版本。不管当前实例是否可变,须要获取其可变版本的拷贝,均应调用mutableCopy方法;获取不可变版本的拷贝,则总应该经过copy方法。

mutableCopy和copy类

深拷贝就是在拷贝对象自身时,将其底层数据也一并复制过去。
浅拷贝就是在拷贝对象时,只拷贝容器对象自己,而不复制其中数据。

深拷贝和浅拷贝比较

相关文章
相关标签/搜索