iOS - 采用现代化的Objective-C

说明:本篇文章是作者(Mitchell)参考苹果的 Adopting Modern Objective-C 这篇文章加上自己的总结所翻译,如需转载请注明出处。


一、instancetype###

  • 使用 instancetype 关键词作为方法中返回类型,该方法返回一个它们所调用的类的实例(或者子类),这些方法包括 alloc,init 和类的工厂方法。
  • 在适当的地方使用 instancetype 代替 id 会改善 Objective-C 中的代码类型安全。`
    例如:
 @interface MyObject : NSObject
 + (instancetype)factoryMethodA;
 + (id)factoryMethodB;
 @end
 @implementation MyObject
 + (instancetype)factoryMethodA { return [[[self class] alloc]    init]; }
 + (id)factoryMethodB { return [[[self class] alloc] init]; }
 @end
void doSomething() {
NSUInteger x, y;
x = [[MyObject factoryMethodA] count]; // Return type of +factoryMethodA is taken to be "MyObject *"
y = [[MyObject factoryMethodB] count]; // Return type of +factoryMethodB is "id"
}

因为 factoryMethodA 的返回值类型是instancetype ,返回值的类型被表达为MyObject*,然而MyObject并没有-count的方法,所以编译器在x的行给出了一则警告。

main.m: ’MyObject’ may not respond to ‘count’

然而,factoryMethodB 返回的是一个id 类型的返回值,编译器不能在 y 行给出警告。因为一个 id 类型的对象可能是任何的类,并且一个叫 -count 的方法可能存在于其中某些类中,对于编译器来说这个返回值是有可能去响应这个方法的。
为了确保instancetype的工厂方法有正确的子类化行为,一定要确保在分配类的时候使用的是 [self class] ,而不是直接引用类的名称。遵循这个惯例会确保编译器将正确推断出子类的类型。举例:考虑尝试让 MyObject 的子类做一个这样的事情:

@interface MyObjectSubclass : MyObject
@end
 void doSomethingElse() {
        NSString *aString = [MyObjectSubclass factoryMethodA];
}

这里如果这样写了,编译器会报错:

main.m: Incompatible pointer types initializing ’NSString *’ with an expression of type ’MyObjectSubclass *’

也就是说编译器已经能够识别你的这个类型就是MyObjectSubclass*。但是如果将 instancetype 改成 id ,我们又会发现这个警告将会消失,也就是说编译器不会知道你这个对象的准确类型是什么。

  • 在这个例子中,+ factoryMethodA 方法传递了一个类型是MyObjectSubclass 的返回值类型,它作为接受者的类型。编译器将会适时的确定+ factoryMethodA方法的返回值就是子类MyObjectSubclass,而不是父类的工厂方法中所返回的父类。
  • 如何运用?
    在你的代码中,在适当的地方用 instancetype 替换现有的返回值为 id 的类型。通常是在 init 初始化方法和工厂类方法中。即使编译器会自动将 “alloc、init、new ”返回值是 id 的方法转换为instancetype,它不会转换其他方法。Objective-C 约定会为所有方法写入instancetype。
    注意:你只能在返回值的时候用 instancetype 替换 id ,而不是在你代码的其他地方。instancetype 并不像 id,instancetype 关键词只能被做声明方法的返回值类型。
    例如:
 @interface MyObject
 - (id)myFactoryMethod;
 @end

应该被改为:

```
@interface MyObject
- (instancetype) myFactoryMethod;
```
或者,您可以用现代Objective-C变化器在Xcode自动进行更改您的代码,具体请参考   [Refactoring Your Code Using Xcode](https://developer.apple.com/library/ios/releasenotes/ObjectiveC/ModernizationObjC/AdoptingModernObjective-C/AdoptingModernObjective-C.html#//apple_ref/doc/uid/TP40014150-CH1-SW13) 。
  • 对于 instancetype 和 id 异同的小结:
    • 不同点:
      1、instancetype可以返回和方法所在类相同类型的对象,而id只能返回未知类型的对象,简言之:instancetype会有一个类型检测,在初始化的时候它会检测是哪个类在调用初始化方法,然后返回这个类,而这个是id所做不到的。
      2、instancetype只能作为返回值,不能像id那样作为参数。
    • 相同点:都能作为返回值的类型。

二、属性###

一个Objective-C的属性是由@property语法来定义是公有或者私有。

@property (readonly, getter=isBlue) BOOL blue;

属性捕捉对象的状态。他们反映了对象固有的属性和其他对象们的关系。属性提供了一个安全,方便的方式关联这些属性,而无需编写一组自定义访问器方法(虽然属性允许定制getter、setter方法,如果需要的话)。

  • 自动合成 getter和setter方法:当你定义一个属性的时候,默认为你创建了getter和setter方法。
  • 更好的办法是声明一系列的方法。因为访问器方法有命名约定,可以很清楚的知道getter和setter中做了什么。
  • 属性的关键词可以表达出关于自身行为的额外信息。属性提供了一个潜在的声明属性的方式,assign(vs copy),weak,atomic(vs nonatomic),等等。
  • 属性方法遵守了一个简单的命名约定。getter方法的是以属性的名称命名,setter方法是以set前缀命名,书写的规范是驼峰式的大小写。Boolean属性的命名约定是在getter方法前加一个“is”:
@property(readonly,getter = isBlue)BOOL blue;

据此,所有的接下来的工作可以这样:

if(color.blue){}
if(color.isBlue){}
if([color isBlue]){}

当决定什么可以称为一个属性的时候,请记住以下的几点不可以称为属性:
- init 方法
- copy 方法,mutableCopy方法
- 一个类的工厂方法
- 一个初始化方法并且返回一个BOOL值的方法
- 一个就像getter方法作用的一方面来明确内部状态变化的方法。
除此之外,考虑考虑以下的规则当在你的代码中定义一个潜在的属性:
- readwrite的属性有两个访问器方法,setter带一个参数无返回值。getter没有参数但是有一个返回值。如果你想把这组方法转换成属性,标记它一个readwrite关键词。
- read-only的属性只有一个访问器方法,getter。如果你想把这个方法转换成属性,标记它一个readonly关键词。
- getter方法应该被幂等(如果一个getter被调用两次,第二次的返回的结果应该是和第一次相同的)。然而,这也是可以接受的就是getter每次被调用时都计算结果。

  • 如何采用
    定义一组方法,有资格被转换为一个属性,如下:
-  (NSColor*)backgroundColor;
 - (void)setBackgroundColor:(NSColor*)color;

然后用带着适合的关键字的@property语法糖去声明它们:

@property(copy)NSColor* backgroundColor;

关于属性关键词的信息和一些其他的注意事项,请看Encapsulating Data


三、枚举、宏###

NS_ENUM和NS_OPTION的宏提供了一个简洁的基于C语言的方式去定义枚举和选项。这两个宏在显式地指定枚举类型和大小的和选项方面提高了代码的完成效率。除此之外,这个语法生命枚举的方式是由老的编程式来准确评估的,由新的编程式来解释潜在的类型信息。

使用 NS_ENUM 宏来定义枚举,一组相互独立的值:

typedef NS_ENUM (NSInteger,UITableViewCellStyle) {
    UITableViewCellStyleDefault,
    UITavleViewCellStyleValue1,
    UITavleViewCellStyleValue2,
    UITableViewCellStyleSubtitle
};

NS_ENUM 宏帮助定义了枚举的名字和类型两个类型,在这种情况下给 UITableViewCellStyle 命名为 NSInteger 的类型。枚举的类型应该是 NSInteger。
使用 NS_OPTIONS 宏来定义选项,一组位掩码值可以被组合在一起:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
        UIViewAutoresizingNone                 = 0,
        UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
        UIViewAutoresizingFlexibleWidth        = 1 << 1,
        UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
        UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
        UIViewAutoresizingFlexibleHeight       = 1 << 4,
        UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

像枚举一样,NS_OPTIONS宏定义了名称和类型两个值。然而,选项的类型应该是NSUInteger。

  • 如何采用
    替换你的魅族声明,就像下面这样:
enum {
        UITableViewCellStyleDefault,
        UITableViewCellStyleValue1,
        UITableViewCellStyleValue2,
        UITableViewCellStyleSubtitle
};
typedef NSInteger UITableViewCellStyle;

使用 NS_ENUM 语法:

typedef NS_ENUM(NSInteger, UITableViewCellStyle) {
        UITableViewCellStyleDefault,
        UITableViewCellStyleValue1,
        UITableViewCellStyleValue2,
        UITableViewCellStyleSubtitle
};

当你使用 enum 去定义一组位掩码的时候,可以像下面这样:

enum {
        UIViewAutoresizingNone                 = 0,
        UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
        UIViewAutoresizingFlexibleWidth        = 1 << 1,
        UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
        UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
        UIViewAutoresizingFlexibleHeight       = 1 << 4,
        UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
typedef NSUInteger UIViewAutoresizing;

使用 NS_OPTIONS 宏

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
        UIViewAutoresizingNone                 = 0,
        UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
        UIViewAutoresizingFlexibleWidth        = 1 << 1,
        UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
        UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
        UIViewAutoresizingFlexibleHeight       = 1 << 4,
        UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
  • 关于NS_ENUM 和NS_OPTIONS的小结
    • NS_ENUM:第一个参数用于存储新类型的类型,在64位环境下,UITableViewCellStyle 和 NSInteger 一样有8字节长。你要保证你给出的所有的值能被改类型容纳,否则就会产生错误。第二个参数是新类型的名字。大括号里面和以前i,是你要定义的各种值。这种方法提取了之前各种不同实现的优点,甚至有提示编译器在进行switch判断时检查类型匹配的功能。

    • NS_OPTIONS:位掩码使用。语法和 NS_ENUM 完全相同,但这个宏提示编译值是如何通过位掩码|组合在一起的。同样的,注意值的区间不要超过所使用类型的最大容纳范围。
      参考至:NS_ENUM & NS_OPTIONS


四、对象的初始化###

  • 在 Objective-C 中,对象的初始化是基于一个指定的初始化器的概念,一个初始化方法负责调用的是父类的初始化方法然后会调用自身的对象实例方法。不指定值的初始化被称为便利的初始化。便利初始化通常委托给一个初始化器-最终的终端链在初始化器-而不是自己执行的初始化。

  • 指定的初始化器会帮助并确保继承的初始化器正确初始化所有的实例变量。子类所执行重要的初始化需要覆盖其父类所有指定的初始化。但是它不必去覆盖方便的初始化。更多关于初始化器的信息,看这里Object Initialization

  • 为了澄清指定和指定初始化之间的区别,你应该在 init家庭中 增加 NS_DESIGNATED_INITIALIZER 这个宏,表示它是指定的初始化器。介绍这个宏的几个使用机制:

    • 指定的初始化器必须链接到一个父类的 init 方法中([super init]),这是为父类指定的初始化器。
    • 方便的初始化器(一个类的初始化方法并没有被标记为是指定的初始化方法,一个类必须至少有一个初始化方法作为指定的初始化器)必须委托给另一个初始化器。
    • 如果一个类提供了一个或者更多的指定的初始化方法,它必须实现所有的指定的父类的初始化方法。
    • 如果违反了这些限制,你会收到来自编译器的警告。
      如果你在你的类中使用了 NS_DESIGNATED_INITIALIZER 宏,你需要在你所有指定的初始化方法中都标记这个宏,所有的其他的初始化会被认为是便利的初始化。
  • 怎么使用
    在你的类中确定你指定的初始化方法,然后用 NS_DESIGNATED_INITIALIZER 宏来标记它们,举个例子:

  - (instancetype)init NS_DESIGNATED_INITIALIZER;

五、自动引用计数(ARC)###

  • 自动引用计数是编译器提供的一个自动对 Objective-C 对象进行内存管理的特色。可之前你不得不用 retain,releae 或者autorelease 不同,ARC 预估对象生命周期的需求然后在编译的事后自动插入适当的内存管理需求。编译器也会生成适当的 dealloc 方法。
  • 如何运用
    Xcode提供了一个ARC转换的自动化工具(例如移除retain和release的调用),这会帮助你解决迁移的时候不能自动处理的问题。使用ARC迁移工具,选择 Edit > Refactor > Convert to Objective-C ARC. 迁移器会将所有文件转换成ARC。

你可能感兴趣的:(iOS - 采用现代化的Objective-C)