Objective-C中编程中一些推荐的书写规范小结

一、类
1. 类名
类名应该以三个大写字母作为前缀(双字母前缀为Apple的类预留)

不仅仅是类,公开的常量、Protocol等的前缀都为相同的三个大写字母。

当你创建一个子类的时候,你应该把说明性的部分放在前缀和父类名的中间。

例如:

如果你有一个 ZOCNetworkClient 类,子类的名字会是ZOCTwitterNetworkClient (注意 "Twitter" 在 "ZOC" 和 "NetworkClient" 之间); 按照这个约定, 一个UIViewController 的子类会是 ZOCTimelineViewController.

2. Initializer和dealloc
推荐的代码组织方式是将dealloc方法放在实现文件的最前面(直接在@synthesize以及@dynamic之后),init应该跟在dealloc方法后面。

如果有多个初始化方法,那么指定初始化方法应该放在最前面,间接初始化方法跟在后面。

如今有了ARC,dealloc方法几乎不需要实现,不过把init和dealloc放在一起,强调它们是一对的。通常在init方法中做的事情需要在dealloc方法中撤销。

关于指定初始化方法(designated initializer)和间接初始化方法(secondary initializer)

Objective-C 有指定初始化方法(designated initializer)和间接(secondary initializer)初始化方法的观念。 designated 初始化方法是提供所有的参数,secondary 初始化方法是一个或多个,并且提供一个或者更多的默认参数来调用 designated 初始化的初始化方法。

代码如下:

@implementation ZOCEvent

- (instancetype)initWithTitle:(NSString *)title
                         date:(NSDate *)date
                     location:(CLLocation *)location
{
    self = [super init];
    if (self) {
        _title    = title;
        _date     = date;
        _location = location;
    }
    return self;
}

- (instancetype)initWithTitle:(NSString *)title
                         date:(NSDate *)date
{
    return [self initWithTitle:title date:date location:nil];
}

- (instancetype)initWithTitle:(NSString *)title
{
    return [self initWithTitle:title date:[NSDate date] location:nil];
}

@end

initWithTitle:date:location: 就是 designated 初始化方法,另外的两个是 secondary 初始化方法。因为它们仅仅是调用类实现的 designated 初始化方法。

一个类应该有且只有一个 designated 初始化方法,其他的初始化方法应该调用这个 designated 的初始化方法(有例外)。

3. 当定义一个新类的时候有三个不同的方式:
(1)不需要重载任何初始化函数
(2)重载 designated initializer
(3)定义一个新的 designated initializer
第一种方式不需要增加类的任何初始化逻辑,也就是说在类中不必重写父类的初始化方法也不需要其他操作。

第二种方式要重载父类的指定初始化方法。例子:

代码如下:

@implementation ZOCViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    // call to the superclass designated initializer
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {
        // Custom initialization (自定义的初始化过程)
    }
    return self;
}

@end

这个例子中,ZOCViewController继承自UIViewController,这里我们有一些其他的需求(比如希望在初始化的时候给一些成员变量赋值),所以需要重写父类的指定初始化方法initWithNibName:bundle:方法。

注意,如果在这里并没有重载这个方法,而是重载了父类的init方法,那么会是一个错误。

因为在创建这个类(ZOCViewController)的时候,会调用initWithNib:bundle:这个方法,所以我们重载这个方法,首先保证父类初始化成功,然后在这个方法中进行额外的初始化操作。但是如果重载init方法,在创建这个类的时候,并不会调用init方法(调用的是initWithNib:bundle:这个指定初始化方法)。

第三种方式是希望提供自己的类初始化方法,应该遵守下面三个步骤来保证正确性:

定义你的 designated initializer,确保调用了直接超类的 designated initializer。
重载直接超类的 designated initializer。调用你的新的 designated initializer。
为新的 designated initializer 写文档。
很多开发者会忽略后两步,这不仅仅是一个粗心的问题,而且这样违反了框架的规则,而且可能导致不确定的行为和bug。

正确的例子:

代码如下:

@implementation ZOCNewsViewController

- (id)initWithNews:(ZOCNews *)news
{
    // call to the immediate superclass's designated initializer (调用直接超类的 designated initializer)
    self = [super initWithNibName:nil bundle:nil];
    if (self) {
        _news = news;
    }
    return self;
}

// Override the immediate superclass's designated initializer (重载直接父类的  designated initializer)
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
    // call the new designated initializer
    return [self initWithNews:nil];
}

@end

​很多开发者只会写第一个自定义的初始化方法,而不重载父类的指定初始化方法。

在第一个自定义的初始化方法中,因为我们要定义自己的指定初始化方法,所以在最开始的时候首先要调用父类的指定初始化方法以保证父类都初始化成功,这样ZOCNewsViewController才是可用状态。(因为父类是通过initWithNibName:bundle:这个指定初始化方法创建的,所以我们要调用父类的这个方法来保证父类初始化成功)。然后在后面给_news赋值。

​如果仅仅是这样做是存在问题的。调用者如果调用initWithNibName:bundle:来初始化这个类也是完全合法的,如果是这种情况,那么initWithNews:这个方法永远不会被调用,所以_news = news也不会被执行,这样导致了不正确的初始化流程。

解决方法就是需要重载父类的指定初始化方法,在这个方法中返回新的指定初始化方法(如例子中做的那样),这样无论是调用哪个方法都可以成功初始化。

间接初始化方法是一种提供默认值、行为到初始化方法的方法。

你不应该在间接初始化方法中有初始化实例变量的操作,并且你应该一直假设这个方法不会得到调用。我们保证的是唯一被调用的方法是 designated initializer。

这意味着你的 secondary initializer 总是应该调用 Designated initializer 或者你自定义(上面的第三种情况:自定义Designated initializer)的 self的 designated initializer。有时候,因为错误,可能打成了 super,这样会导致不符合上面提及的初始化顺序。

也就是说,你可能看到一个类有多个初始化方法,实际上是一个指定初始化方法(或多个,比如UITableViewController就有好几个)+多个间接初始化方法。这些简洁初始化方法可能会根据不同的参数做不同的操作,但是本质上都是调用指定初始化方法。所以说,间接初始化方法是有可能没有调用到的,但是指定初始化方法是会调用到的(并不是每一个都会调用到,但是最后调用的一定是一个指定初始化方法)。(这里又可以引申到上面提到的问题,我们可以直接重写父类的指定初始化方法,也可以自定义初始化方法(在这个方法中需要用到self = [super 父类初始化方法]这种形式的代码),并且如果是自定义初始化方法,还应该重写从父类继承的初始化方法来返回我们的自定义初始化方法…)。

总之就是,如果重写父类的指定初始化方法首先需要调用父类的相应初始化方法;如果增加自定义指定初始化方法,首先在新增的自定义指定初始化方法中调用父类的相应初始化方法,然后需要重写父类的指定初始化方法,在重写的方法中调用刚刚添加的自定义指定初始化方法。

4.补充

一个类可能有多个指定初始化方法,也有可能只有一个指定初始化方法。

以UITableViewController为例,我们可以看到:

代码如下:

- (instancetype)initWithStyle:(UITableViewStyle)style NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

它有三个指定初始化方法,我们刚才说,当子类从父类继承并重写初始化方法,首先需要调用父类的初始化方法,但是如果一个类的初始化方法有多个,那么需要调用哪个呢?

事实上不同的创建方式要调用不同的指定初始化方法。

比如,我们以Nib的形式创建UITableViewController,那么最后调用的就是- (instancetype)initWithNibName:(NSString )nibNameOrNil bundle:(NSBundle )nibBundleOrNil这个指定初始化方法;如果我们以Storyboard的形式创建,那么最后调用的就是- (instancetype)initWithCoder:(NSCoder *)aDecoder这个指定初始化方法。如果以代码的形式创建,那么最后调用的就是- (instancetype)initWithStyle:(UITableViewStyle)style这个指定初始化方法。所以不同的情况需要重写不同的指定初始化方法,并且重写的时候首先要调用父类相应的指定初始化方法(比如重写initWithCoder:方法,那么首先self = [super initWithCoder:…],都是一一对应的)。

再以UIViewController为例,我们以Nib的形式创建UIViewController,那么最后调用的是- (instancetype)initWithNibName:(NSString )nibNameOrNil bundle:(NSBundle )nibBundleOrNil,这与UITableViewController是一样的;如果我们以Storyboard的形式创建,那么最后调用的是- (instancetype)initWithCoder:(NSCoder )aDecoder,这与UITableViewController也是一样的;但是如果我们以代码的形式创建UIViewController(eg: CYLViewController vc = [[CYLViewController alloc] init]; CYLViewController继承自UIViewController),那么它最后调用的实际是- (instancetype)initWithNibName:(NSString )nibNameOrNil bundle:(NSBundle )nibBundleOrNil,这与UITableViewController是不一样的,因为UIViewController并没有- (instancetype)initWithStyle:(UITableViewStyle)style这个方法,所以当用代码创建的时候,最后调用的也是initWithNibName:bundle这个指定初始化方法,并且参数自动设置为nil。

所以现在反过头来再看UITableViewController,当使用代码的方式创建的时候(eg: CYLTableViewController tvc = [[CYLTableViewController alloc] init]; 或者 CYLTableViewController tvc = [[CYLTableViewController alloc] initWithStyle: UITableViewStylePlain]; ),它会调用initWithStyle:这个方法,但是如果你也实现了initWithNibName:bundle:这个方法,你会发现这个方法也被调用了。因为UITableViewController继承自UIViewController,所以当用代码创建的时候,最后也会掉用到initWithNIbName:bundle:(因为UIViewController就是这么干的)。

所以用代码创建UITableViewController的时候,它会调用initWithNibName:bundle:和initWithStyle:这两个方法。

二、属性
属性要尽可能描述性地命名,并且使用驼峰命名。

关于”*”的位置:

代码如下:

// 推荐
NSString *text;

// 不推荐
NSString* text;
NSString * text;

注意,这个习惯和常量并不同。

static NSString * const ...
你永远不能在 init (以及其他初始化函数)里面用 getter 和 setter 方法,你应该直接访问实例变量。记住一个对象是仅仅在 init 返回的时候,才会被认为是初始化完成到一个状态了。

当使用 setter/getter 方法的时候尽量使用点符号。

代码如下:

// 推荐
view.backgroundColor = [UIColor orangeColor];
[UIApplication sharedApplication].delegate;

// 不推荐
[view setBackgroundColor:[UIColor orangeColor]];
UIApplication.sharedApplication.delegate;

使用点符号会让表达更加清晰并且帮助区分属性访问和方法调用。

属性定义

代码如下:

@property (nonatomic, readwrite, copy) NSString *name;

属性的参数应该按照这个顺序排列: 原子性,读写和内存管理。

习惯上修改某个属性的修饰符时,一般从属性名从右向左搜索需要修动的修饰符。最可能从最右边开始修改这些属性的修饰符,根据经验这些修饰符被修改的可能性从高到底应为:内存管理 > 读写权限 >原子操作
你必须使用 nonatomic,除非特别需要的情况。在iOS中,atomic带来的锁特别影响性能。

如果想要一个公开的getter和私有的setter,你应该声明公开的属性为 readonly 并且在类扩展总重新定义通用的属性为 readwrite 的。

代码如下:

//.h文件中
@interface MyClass : NSObject
@property (nonatomic, readonly, strong) NSObject *object;
@end

代码如下:

//.m文件中
@interface MyClass ()
@property (nonatomic, readwrite, strong) NSObject *object;
@end

代码如下:

@implementation MyClass
//Do Something cool
@end

描述BOOL属性的词如果是形容词,那么setter不应该带is前缀,但它对应的 getter 访问器应该带上这个前缀。

@property (assign, getter=isEditable) BOOL editable;
任何可以用来用一个可变的对象设置的((比如 NSString,NSArray,NSURLRequest))属性的的内存管理类型必须是 copy 的。(原文中是这样说的,但是我理解的话并不是绝对的。如果不想让原来的可变对象影响到类的这个相应属性,那么就需要用copy,这样在赋值的时候可变对象会首先进行copy完成深拷贝,再把拷贝出的值赋给类的属性,这样就能保证类属性和原来的可变对象影响并不影响。但是如果想让类属性对原来的可变对象是一个强引用,指向这个可变对象,那么会用strong。)

你应该同时避免暴露在公开的接口中可变的对象,因为这允许你的类的使用者改变类自己的内部表示并且破坏类的封装。你可以提供可以只读的属性来返回你对象的不可变的副本。

代码如下:

/* .h */
@property (nonatomic, readonly) NSArray *elements

/* .m */
- (NSArray *)elements {
  return [self.mutableElements copy];
}

虽然使用懒加载在某些情况下很不错,但是使用前应当深思熟虑,因为懒加载通常会产生一些副作用。(但是懒加载还是比较常用的,比如下面的例子)

副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。函数副作用会给程序设计带来不必要的麻烦,给程序带来十分难以查找的错误,并且降低程序的可读性。

代码如下:

- (NSDateFormatter *)dateFormatter {
  if (!_dateFormatter) {
    _dateFormatter = [[NSDateFormatter alloc] init];
        NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        [_dateFormatter setLocale:enUSPOSIXLocale];
        [_dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSS"];//毫秒是SSS,而非SSSSS
  }
  return _dateFormatter;
}

三、方法
1.参数断言

你的方法可能要求一些参数来满足特定的条件(比如不能为nil),在这种情况下啊最好使用 NSParameterAssert() 来断言条件是否成立或是抛出一个异常。

代码如下:

- (void)viewDidLoad
{
    [super viewDidLoad];

[self testMethodWithAParameter:0];
}

- (void)testMethodWithAParameter: (int)value
{
    NSParameterAssert(value != 0);

NSLog(@"正确执行");
}

在此例中, 如果传的参数为0,那么程序会抛出异常。

2.私有方法

永远不要在你的私有方法前加上 _ 前缀。这个前缀是 Apple 保留的。不要冒重载苹果的私有方法的险。

当你要实现相等性的时候记住这个约定:你需要同时实现isEqual 和 hash方法。如果两个对象是被isEqual认为相等的,它们的 hash 方法需要返回一样的值。但是如果 hash 返回一样的值,并不能确保他们相等。

代码如下:

@implementation ZOCPerson

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;
    }

if (![object isKindOfClass:[ZOCPerson class]]) {
        return NO;
    }

// check objects properties (name and birthday) for equality (检查对象属性(名字和生日)的相等性
    ...
    return propertiesMatch;
}

- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash];
}

@end

你总是应该用 isEqualTo<#class-name-without-prefix#>: 这样的格式实现一个相等性检查方法。如果你这样做,会优先调用这个方法来避免上面的类型检查。

所以一个完整的 isEqual 方法应该是这样的:

代码如下:

- (BOOL)isEqual:(id)object {
    if (self == object) {
      return YES;
    }

if (![object isKindOfClass:[ZOCPerson class]]) {
      return NO;
    }

return [self isEqualToPerson:(ZOCPerson *)object];
}

- (BOOL)isEqualToPerson:(Person *)person {
    if (!person) {
        return NO;
    }

BOOL namesMatch = (!self.name && !person.name) ||
                       [self.name isEqualToString:person.name];
    BOOL birthdaysMatch = (!self.birthday && !person.birthday) ||
                           [self.birthday isEqualToDate:person.birthday];

return haveEqualNames && haveEqualBirthdays;
}

四、Category
category 方法前加上自己的小写前缀以及下划线。(真的很丑,但是苹果也推荐这样做)

代码如下:

- (id)zoc_myCategoryMethod

这是非常必要的。因为如果在扩展的 category 或者其他 category 里面已经使用了同样的方法名,会导致不可预计的后果。(会调用最后一个加载的方法)

代码如下:

// 推荐
@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end

代码如下:

// 不推荐
@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end

推荐使用Category来根据不同功能对方法进行分组。

代码如下:

@interface NSDate : NSObject <NSCopying, NSSecureCoding>

@property (readonly) NSTimeInterval timeIntervalSinceReferenceDate;

@end

代码如下:

@interface NSDate (NSDateCreation)

+ (instancetype)date;
+ (instancetype)dateWithTimeIntervalSinceNow:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeIntervalSinceReferenceDate:(NSTimeInterval)ti;
+ (instancetype)dateWithTimeIntervalSince1970:(NSTimeInterval)secs;
+ (instancetype)dateWithTimeInterval:(NSTimeInterval)secsToBeAdded sinceDate:(NSDate *)date;
// ...
@end

五、NSNotification
当你定义你自己的 NSNotification 的时候你应该把你的通知的名字定义为一个字符串常量,就像你暴露给其他类的其他字符串常量一样。你应该在公开的接口文件中将其声明为 extern 的, 并且在对应的实现文件里面定义。

因为你在头文件中暴露了符号,所以你应该按照统一的命名空间前缀法则,用类名前缀作为这个通知名字的前缀。(通常在头文件中对外提供的常量都需要加上前缀,声明extern + const,并且并不是在头文件中定义,而是在实现文件中定义。如果不是对外公开的常量,那么通常直接在实现文件里声明为static + const,并且也要加上前缀,直接在后面进行定义。)

同时,用一个 Did/Will 这样的动词以及用 "Notifications" 后缀来命名这个通知也是一个好的实践。

代码如下:

// Foo.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Foo.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

(0)

相关推荐

  • 简介Objective-C解析XML与JSON数据格式的方法

    解析XML 本文以解析本地XML为例,网络获取到的返回值只需转换成NSData型,解析是同理 需要解析的xml文件如下,users.xml <?xml version="1.0" encoding="UTF-8"?> <AllUsers> <message>用户信息</message> <user> <name>芳仔小脚印</name> <age>10</age&g

  • 举例讲解Objective-C中@property属性的用法

    学过c/c++的朋友都知道,我们定义struct/class的时候,如果把访问限定符(public,protected,private)设置为public的话,那么我们是可以直接用.号来访问它内部的数据成员的.比如 //in Test.h class Test { public: int i; float f; }; 我在main函数里面是可以通过下面的方式来使用这个类的:(注意,如果在main函数里面使用此类,除了要包含头文件以外,最重要的是记得把main.m改成main.mm,否则会报一些奇

  • 简单讲解Objective-C的基本特性及其内存管理方式

    一.OC简介 Oc语言在c语言的基础上,增加了一层最小的面向对象语法,完全兼容C语言,在OC代码中,可以混用c,甚至是c++代码. 可以使用OC开发mac osx平台和ios平台的应用程序. 拓展名:c语言-.c  OC语言.-m  兼容C++.-mm 注:其实c语言和oc甚至任何一门语言都只是我们为了实现一些功能,达到一些效果而采用的工具,抛开语法的差别外,我想最重要的应该是在解决问题的时候考虑的角度和方法不一样而已,然而这也构成了学习一门语言的重要性. 二.语法预览 (1)关键字 基本上所有

  • 以实例讲解Objective-C中的KVO与KVC机制

    KVO实例浅析 最近遇到个问题,在处理项目中一个评论界面时,因为直接用的是UIWebView展示评论列表,结果取到的页面上下都有一段CGSize为(320,65)的乱七八糟的广告,十分碍眼.头部广告因很方便的在头部坐标贴上自己的logo解决了,但是尾部的,因为每个页面的评论长短不一,坐标也就不一样,这样就不能给定死坐标去贴logo,思前想后,通过KVO很好的解决了这个问题. @KVO概述: KVO,即:Key-Value Observing,它提供一种机制,当指定的对象的属性被修改后,则对象就会

  • 理解Objective-C的变量以及面相对象的继承特性

    OC点语法和变量作用域 一.点语法 (一)认识点语法 声明一个Person类: 复制代码 代码如下: #import <Foundation/Foundation.h> @interface Person : NSObject {     int _age;//默认为@protected } - (void)setAge:(int)age; - (int)age; @end Person类的实现: 复制代码 代码如下: #import "Person.h" @impleme

  • Objective-C编程中语句和变量的一些编写规范建议

    语句 条件语句 条件语句体应该总被大括号包围.只有一行代码最好也加上,否则会带来安全隐患. 复制代码 代码如下: // 推荐 if (!error) {     return success; } // 不推荐 if (!error)     return success; if (!error) return success; 尤达表达式(Yoda) 不要使用尤达表达式.(名字起源于星球大战中尤达大师的讲话方式,总是用倒装的语序) 复制代码 代码如下: // 推荐 if ([myValue i

  • 详解Objective-C编程中对设计模式中适的配器模式的使用

    引言 在项目开发中,有时候会遇到这样的一种情景:需要使用以前开发的"一些现存的对象",但是新环境中要求的接口是这些现存对象所不满足的.怎样应对这种迁移的需求?使得可以复用这些对象,以满足新的应用环境,这就是适配器(Adapter)所要解决的问题. 定义 "将一个类的接口转换成客户希望的另外一个接口.适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作." 最初的定义出现于<设计模式>(Addison-Wesley,1994). 这个定义应该

  • 详解Objective-C设计模式编程中对备忘录模式的运用

    基本理解 这个模式有三个关键角色:原发器(Originator).备忘录(Memento).看管人(caretaker).三者的基本关系是:原发器创建一个包含其状态的备忘录,并传给看管人.看管人不知道如何与备忘录交互,但会把备忘录放在一个安全之处保管好. 备忘录(Memento):在 不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态.这样以后就可以将该对象回复到原先保存的状态. Originator(发起人):负责创建一个备忘录,用以记录当前时刻它的内部状态,并且可使用恢

  • 浅析Objective-C中分类Category的使用

    无论一个类设计的如何完美,都不可避免的会遇到没有预测到的需求,那怎么扩展现有的类呢?当然,继承是个不错的选择.但是Objective-C提供了一种特别的方式来扩展类,叫Catagory,可以动态的为已经存在的类添加新的行为.这样可以保证类的原原来的基础上,较小的改动就可以增加需要的功能.使用Category对类进行扩展时,不需要访问其源代码,也不需要创建子类,这样我们可以扩展系统提供的类.Category使用简单的方式,实现了类的相关方法的模块化,把不同的类方法分配到不同的分类文件中. 使用Ob

  • Objective-C的NSOperation多线程类基本使用指南

    NSOperation 一.NSOperation 1.简介 NSOperation实例封装了需要执行的操作和执行操作所需的数据,并且能够以并发或非并发的方式执行这个操作. NSOperation本身是抽象基类,因此必须使用它的子类,使用NSOperation子类的方式有2种: 1> Foundation框架提供了两个具体子类直接供我们使用:NSInvocationOperation和NSBlockOperation 2> 自定义子类继承NSOperation,实现内部相应的方法 2.执行操作

随机推荐