前言
这应该会成为一个系列博文吧,之前就想过要陆陆续续把自己以前学习记录在印象笔记里的一些东西整合一下,然后发出来。都是一些比较零散的但又挺重要的吧,如果对别人也有所帮助,那倒是一件美事,没有的话,留给自己复习也不错~
实例变量与属性
这个是我一开始学习 Objc 比较纠结的问题了。对于一个普通的类,我们去定义一个属性
,例如:
1 | @interface Person : NSObject |
和我们这样去定义一个实例变量
:
1 | @interface Person : NSObject |
究竟有什么区别?
在网上看到一些文章在解释主要的区别是说使用 @property 关键字的属性可以被其他类所访问、修改;而写在大括号里的实例变量却不能被其他类所访问。
其实不然。
之所以会有这样的结论,是因为我们在定义实例变量的时候往往忽略了他的关键字声明,类似于 C++ 实例变量的定义是有以下3种关键字的:
@private 私有成员,只有当前类可以访问;
@protected 受保护成员,只有当前类或子类可以访问(如果没有添加任何修饰则默认为@protected);
@public 公共成员,所有类均可访问;(公共成员的调用使用
“->”
操作符;)
所以像刚刚上面的代码,实例变量就默认设置为 protected
,所以除了子类的其他类午饭访问到实例变量 name。
所以,实例变量和属性之间的区别肯定不是在这里。
我觉得,通俗一点讲:
实例变量 + getter 与 setter 方法 = 属性
例如:
1 | @interface Person : NSObject |
又在 .m文件里完成函数的实现:
1 | - (void)setName:(NSString *)name{ |
那么这样子的话,我们就可以在其他类里:
1 | Person *boy = [[Person alloc] init]; |
这样是不是就和我们定义一个属性一样了呢?
看一些 Objc 的书才知道,在 iOS 刚出现那会儿,我们是需要为输出口同时声明了属性和底层实例变量(实例变量)的,并且要求你必须声明与之对应的实例变量,例如:
1 | @interface Person : NSObject |
但是,自从苹果将默认编译器从 GCC 转换为 LLVM(low level virtual machine) 之后,就不再需要为属性声明实例变量了,或者说将属性与实例变量关联起来。
如果 LLVM 发现一个没有匹配实例变量的属性,它将自动创建一个以下划线开头的实例变量(这个应该都知道)。
也就是说,采用 @property 声明的属性
,其实就是利用了 getter
和 setter
的方法来对实例变量
进行访问修改的而已。
当然采用 @property 声明的属性
所对应的实例方法
可以手动生成也可以自动生成。
自动:如果只声明一个属性a,不使用 @synthesize 实现:编译器会使用_a作为属性的实例变量。
主动:
- 如果声明了一个属性a,使用 @synthesize a 进行实现,但是实现过程中没有指定使用的实例变量,则此时编译器会使用a作为属性的实例变量。
- 如果声明了一个属性a,使用 @synthesize a=_a 进行实现,这个过程已经指定了使用的实例变量:此时会使用指定的实例变量作为属性变量;
并且,当我们不去重载 getter 和 setter 方法时,系统会自动为我们生成,而且 setter 的实现还是根据我们的 @property 的关键字来决定,具体可以看一下,我之前写的博客《聊聊那些iOS内存管理的关键字》,是不是爽到飞起?!
也难怪 iOS5 之后,Apple官方会推荐我们使用属性
来暴露实例变量
给外界了。
_下划线和.点访问
顺着上一个小节的思路,其实这个问题也就很好懂了。
.点访问
,例如 self.a,实际上包括了 setter 或者 getter 方法的调用的。而_下划线访问
,例如 _a 是获取实例变量。并且,由于实际 getter 和 setter 函数,引用计数也会有点区别; 在使用
.点访问
,例如 self.a时是调用一个 getter 方法。会使引用计数加一,而_下划线访问
,例如 _a 不会使用引用技术加一的。setter 还要根据属性的关键字来定。_下划线访问
是获取不到父类的属性,因为它只是对局部变量的访问
所以我感觉使用.点访问
是更好的选择,因为这样可以兼容懒加载(懒加载的核心就是重载 setter 方法),同时也避免了使用下滑线的时候忽略了调用对象的指针。
instancetype 和 id
什么是 instancetype
instancetype
是clang 3.5开始,clang 提供的一个关键字,表示某个方法返回的未知类型的 Objective-C 对象。我们都知道未知类型的的对象可以用 id
关键字表示,那为什么还会再有一个 instancetype
呢?
关联返回类型
根据 Cocoa 的命名规则,满足下述规则的方法:
类方法中,以alloc或new开头
实例方法中,以autorelease,init,retain或self开头
会返回一个方法所在类类型的对象,这些方法就被称为是关联返回类型
的方法。换句话说,这些方法的返回结果以方法所在的类为类型。
例如:
1 | @interface NSObject |
当我们使用如下方式初始化NSArray时:
1 | NSArray *array = [[NSArray alloc] init]; |
按照 Cocoa 的命名规则,语句[NSArray alloc] 的类型就是 NSArray 因为alloc的返回类型属于关联返回类型。同样,[[NSArray alloc]init] 的返回结果也是 NSArray。
instancetype 的作用
如果一个不是关联返回类型的方法,如下:
1 | @interface NSArray |
当我们使用如下方式初始化NSArray时:
1 | [NSArray constructAnArray]; |
根据Cocoa的方法命名规范,得到的返回类型就和方法声明的返回类型一样,是 id
。
但是如果使用 instancetype
作为返回类型,如下:
1 | @interface NSArray |
当使用相同方式初始化NSArray时,得到的返回类型和方法所在类的类型相同,是NSArray*!
总结一下,instancetype
的作用,就是使那些非关联返回类型的方法返回所在类的类型!
显然instancetype
能够确定对象的类型,能够帮助编译器更好的为我们定位代码书写问题,比如:
1 | [[[NSArray alloc] init] mediaPlaybackAllowsAirPlay]; // "No visible @interface for `NSArray` declares the selector `mediaPlaybackAllowsAirPlay`" |
上例中第一行代码,由于 [[NSArray alloc]init] 的结果是 NSArray*,这样编译器就能够根据返回的数据类型检测出 NSArray 是否实现 mediaPlaybackAllowsAirPlay 方法。有利于开发者在编译阶段发现错误。
第二行代码,由于array不属于关联返回类型方法,[NSArray array] 返回的是 id 类型,编译器不知道id类型的对象是否实现了mediaPlaybackAllowsAirPlay方法,也就不能够替开发者及时发现错误。
instancetype 和 id 的异同
相同点
都可以作为方法的返回类型。
不同点
①instancetype可以返回和方法所在类相同类型的对象,id只能返回未知类型的对象;
②instancetype只能作为返回值,不能像id那样作为参数或者定义变量,比如下面的写法:
1 | //err,expected a type |
就是错的,应该写成:
1 | - (void)setValue:(id)value |
最后的话
最近博客更新的有点勤奋啊,哈哈,自己再写的过程中一直会发现自己的一些知识的盲区和遗忘点,自己也有挺大的收获的。接下来想做的是,整理一下以前的代码,开源分享一些自己以前写的小控件。然后就是要期末考试了,哎~