如何把运行期警告转移到编译期的几个小技巧

记得之前有篇帖子说过,应该尽量让运行期的错误,提升到编译期,这样就可以在编译时期解决错误,不需要在进行繁琐,费时的bug调试。但是在Objective-C中,我们如何做到呢。今天我就把我已经知道的一些ios的关键字和一些特殊的语法总结一下,以便大家来更好的优化代码,避免更多的错误。


1、__nullable__nonnull

Xcode6.3中,苹果引入了一个oc的两个修饰符,__nullable__nonnull。从字面上不难看出__nullable表示变量可以是nil,__nonnull表示对象不“应该”为nil。

(注:此处措辞为‘应该’,因为这个修饰符的提示是警告级别,而非错误级别,这个效果很像swift的?和!)。

这样我们可以把这个修饰符,用到我们实际的代码之中了。在我实际的测试中发现,这两个修饰符对于临时变量不起检测作用,只对参数有用,但是这也足够了。所以当我们做api的时候,应该尽量通过这种方式,对api的使用者进行提示。

例如:

-(void)testObj:(id __nonnull)obj;   
-(void)testString:(NSString* __nonnull)obj;   



其实还有两个不带下划线的修饰符nonnullnullable,意思跟带下划线的一样,只不过可以方便的用于属性。

例如:

@property (nonatomic, copy, nonnull) NSArray * items;



通常,我们不需要写__nullablenullable。因为默认的情况下,是可空的。

但是苹果为什么还要发明这两个修饰符呢,我也说了,是通常不需要,在一种情况下是需要写的。

苹果推出了两个宏NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END,这个两个头文件用于类的声明的时候,两个宏之间的所有的指针都被假定为nonnull,只有手动设置nullable,才会变成可空类型。所以大家知道nullable的作用了吧


小技巧:当我们需要某个指针参数或者属性不能为空的时候,可以用__nonnull修饰符来修饰参数,当编译器知道参数为空的时候,会发出警告,尤其是我们给别人提供api的时候。


2<>泛型

Xcode7中,我们可以使用泛型了,这无疑让我们的开发爽多了。

虽然对于id<>这种写法早就支持了,但是我们远不止满足于此。(关于id<>的写法不熟悉的人,来看此贴也是为时尚早)

对于容器类,我们需要指定一个泛型。这样可以让我们insert的时候有类型检查,下标取值的时候,可以弹出对应的属性,这样的放错能力,就比以前强多了。

这种写法可以用于容器类型的任何指针(属性,局部变量,函数参数等)

例如:

NSMutableArray *<NSData *>  mDatas = [[NSMutableArray alloc]init];


这样mDataaddObject的时候就会有类型检查,当传入的参数不是NSData类型,就会有警告。

NSMutableDictionary<NSNumber *,NSNumber *>* map = [[NSMutableDictionary alloc]init];

我曾尝试这种写法,但是结果是,当我setObject的时候,它对key仍然没有类型检查。

只是限定了copying协议,当然对value是有限制的。如果value不是NSNumber*类型,将会发出警告,所以将就着用吧。


当然这些仅仅是前奏,我们还关心泛型是否可以自定义。答案是可以。

例如:

@interface CalType<T>:NSObject
+(BOOL)compare:(T)a b:(T)b;
@end

@implementation CalType
+(BOOL)compare:(id)a b:(id)b
{
    return [a isEqual:b];
}
@end


我尝试着写了一个泛型,然后我在调用端,写了如下代码:

  [CalType<NSString *> compare:@"xixi" b:@(3)];


令我满意的事情发生了,在写完参数b的时候,编译器发出了警告,告诉我无法将number类型变成string

当然我只是举了一个最简单的例子,到底怎么用,大家去发挥。

单同时我也发现了为什么上述的字典的key约束不住。

我个人理解的是(当然不一定对),当你的泛型再次指定协议的时候,泛型就不管用了。

比如T<Copying>这时候,我设定TNSString*,但是我传入任何NSCopying的对象,都不会报错。


小技巧:当我们需要使用容器类,并且想要让它存储指定类型的时候,请用泛型。当然更高级的用法,你们随意。



3__covariant__contravariant

当我们指定泛型以后,不同泛型之间的指针是相互赋值的。(当然这是显然的,否则泛型就没用了)

但是我们做更复杂的程序的时候,

比如:

 

CalType<NSString *>* c1;
 CalType<NSMutableString *>* c2;

我们知道,由于多态性NSString的指针指向NSMutableString的对象是没有问题的。

但是在泛型里面,就没有那么智能。

例如:

c1 = c2;

c2 = c1;

无论哪种写法都是错误的。


如果我们希望c1=c2是不显示警告的(这很好理解,我们通常希望这样不显示警告,因为NSString是父类,具有多态性),我们就要使用关键字__covariant,在泛型里面称为协变性。

例如:

@interface CalType<__covariant T>:NSObject
@end;



这样

c1=c2将不会报错。c2=c1会报错。

(注:系统NSArray是修饰过协变性的,大家可以看h文件)


当然与之相对的是逆变性,关键字为__contravariant,作法与协变性相同。

效果就是c1=c2会报错,c2=c1不会报错。

我们一般的需求是很难用到逆变性的。除非当你不像让子类指针指向父类对象是产生警告,你可以用。



小技巧:当你想在泛型的影响下,进行多态性时,请使用协变性。逆变性看看你们有没有使用场景。



4__kindof

其实在tableview的时候经常会有个痛点,不知道你们有没有,就是返回cell的时候经常要强转。

于是__kindof解决了这个问题。

- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;


所以它的意思,显而易见,就是如果被__kindof修饰的指针,将不会进行类型检查,所以不需要强转(其实相当于自动强转)。

这种写法你也可以用到属性中:

比如UIViewsubviews

@property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews;


当你UIButton *button = view.subviews.lastObject时,将不会再有警告。这有点像逆变。


小技巧:当你不想让子类指针指向父类对象是产生警告时,用__kindof修饰类型。



5instancetype

我们发现以前我们初始化方法,返回值基本都是id类型。

这样编译器无法知道我们的具体类型,如果你的方法调用错误,你将无法得知,最后就会崩溃。

如果将你的init方法,返回值写为instanceof那么,你将只能调用该类具有的方法,否则会发出警告。

这点其实很实用。


小技巧:几乎任何时候,你都可以让你的init方法返回instanceof



6NS_DESIGNATED_INITIALIZER

其实我在之前的某些设计模式帖子里面说过,我们应该避免过度使用继承,除非它真的满足Liskov替换原则。

因为集成为增大程序的复杂性,增加维护成本,但是我们如若非要用继承,我们应该“规规矩矩”的。

比如我们的初始化函数。

当我们有多个初始化函数,然而又要多态的时候,我们应该保证使用者调用哪一个函数都可以让程序正常的。

(我们不能,告诉使用者,只能使用某某某初始化函数,这种代码是不负责任的,我们不应该限定调用方,因为如果有些地方复用的情况多的话,会“不经意”被调用的,你限定使用者是废话,你的代码是不安全的,举个例子:比如你继承了UIViewController,那么你应该让使用者既可以调用initWithNib又可以调用init)

那么这样就有一个大麻烦,我们可能有多个init函数,我们都需要重写,那就很容易出错。

那么我们应该怎么办呢?

那么我们应该定义一个规则,就是所有的初始化函数,都调用同一个初始化函数,然后在做自己不同的操作。

我们称这个共同调用的初始化函数叫,设计初始化函数(DESIGNATED_INITIALIZER)。

这样我们子类扩展的时候,只需要重写设计初始化函数就可以让父类多态的时候是正常的。

如果我们子类定义一个新的设计初始化函数来约束它的子类也是可以的。

Xcode给我们提供了一个宏NS_DESIGNATED_INITIALIZER

例如:

- (instancetype)init NS_DESIGNATED_INITIALIZER;


我们可以用NS_DESIGNATED_INITIALIZER来标识我们的设计初始化函数。

这样有两点好处。

第一,可以让使用者明白,该函数是设计初始化函数。

第二,当你的子类实现部分,没有重写该函数,会有警告的。


当然这个知识点,有些复杂。我给大家总结一下。

第一,你应该从此以后,始终遵循该原则。

第二,当你重写了父类的设计初始化函数之后,你可以保证调用方用任何父类的初始化方法,创建子类是正常的。

第三,如果你想创建新的设计初始化函数,来约束你的子类,你的做法是,首先重写你的父类的设计初始化方法(A),然后创建新的设计初始化方法(B),并且在h文件加上限定宏,然后在你新的设计初始化方法(B)的实现部分先调用父类表名的设计初始化方法(A),然后写与(A)异同的部分。


小技巧:尽量严格遵循Cocoa类设计的原则,合理编写设计初始化方法。



今天先总结到这儿了,以后如果再有新的小技巧,我会继续分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值