现在的位置: 首页 > 综合 > 正文

Effective Object-C 2.0 第一章(条目1和2)

2017年12月15日 ⁄ 综合 ⁄ 共 5499字 ⁄ 字号 评论关闭
文章目录

1 让自己习惯于Object-C

       Object-C通过全新的语法结构使C具有面向对象的特性。Object-C语法使用很多的方括号和极长的方法名,这通常被认为是有点冗余。虽然这使得源代码可读性更强,但是对于C++或者java阵营的开发者来说掌握起来有点困难。

       Object-C很容易掌握,但是有不少intricacies需要理解,同时也有很多特性通常被忽略了。同样地,也有一些特性被滥用或者没有完全理解,从而导致代码的难维护,难调试。本章包含了最基础的话题,其余的章节覆盖了语言和框架的特定领域。

条目1:熟悉Object-C的来源(roots)

       Object-C与C++,Java等面向对象语言有相似之处,但是也有不少的不同。如果你对某一种面向对象的语言很熟练,那么你会懂得使用很多范例。可是,Object-C使用消息结构而不是函数调用的语法显得有些怪异。Object-C是从SmallTalk从演化而来,而SmallTalk是采用消息结构的始祖。消息和函数调用的区别大致如下:

//message (Object-C)

Object *obj = [Object new];

[obj performWith: param1  and: param2];

 

//function Call (C++)

Object *obj = new Object;

obj->perform(param1, param2);

       关键的不同在于消息结构中,是由运行时决定要执行的代码。而函数调用是由编译器决定要执行的代码。当多态以函数调用的方式被引入,使用的是通过运行时查虚函数表的方式。然而对于消息来说,一切查找始终是在运行时。事实守上,编译器甚至都不关心被发送消息的对象类型。对象类型也是在运行时查找得到,使用的是被成为动态绑定的技术,这种技术在条目11会详述。

       Object-C运行时组件做了绝大多数的工作,编译器做的很少。运行时包含了实现Object-C面向对象特性的一切数据结构和方法。例如,运行时包含了所有的内存管理方法。更重要的是,运行时以动态库的方式与你的代码结合在一起。因此,无论何时,只要运行时库有更新,你的应用程序会立刻从中受益。在编译器做很多工作的一门语言要想从中获益,就必须重新编译自己的应用程序。

       Object-C是C的超集,因此所有在C中可以使用的特性都可以在Object-C中使用。因此,为了很好的掌握Object-C,你必须同时懂得C和Object-C的核心概念。特别地,理解C模型的内存管理会帮助你理解Object-C的内存管理以及引用计数的工作机制。这表明指针是被用来指代一个Object-C对象的。声明一个对OC对象引用的指针,所使用的语法如下:

NSString *someString = @”The String”;

       这种语法,更多的是从C中学习而来的。通过声明一个someString的变量,它的类型是NSString*。 这也意味着它是一个指向NSString的指针。所有的Object-C对象都必须以这种方式声明变量,因为OC对象的内存仅仅是在堆中分配。声明一个栈对象是不合法的:

NSString stackString;

//error

       someString指向NSString对象的堆内存。这意味着如果需要创建另外一个对象指向相同的内存位置的话就不要复制,只需要将两个指针指向同一个对象即可:

NSString *someString = @”The String”;

NSString *anotherString = someString;

       这里仅有一个NSString实例对象,但是有两个指针指向它。这两个指针都是NSString*类型,意味着当前的栈帧分配了两个指针的栈空间来存放着两个指针。这两个栈空间内容一样,都是堆内存对象的地址。

图表1.1 图示了这种内存布局

 

 

堆内存需要用户直接进行管理,而栈帧中分配的栈内存会在当前栈帧pop出去被自动清理。

Object-C对堆内存管理进行了一层抽象,因此你并需要显式的调用malloc和free对对象进行分配和释放。Object-C运行时通过一种引用计数的内存管理方式对其进行抽象。

Object-C中,你有时候会遇到一些分配在栈内存的不带“*”的变量。这些变量并不持有Object-C对象。例如来自于CoreGraphics框架的CGRect结构:

CGRect frame

frame.origin.x=0.0f;

frame.origin.y=10.0f

frame.size.width=100.0f;

frame.size.height=150.0f;

CGRect是一个C结构体,它的定义大致如下:

struct CGRect{

       CGPointorigin;

       CGSize  size;

};

typdef struct CGRect CGRect;

       这些类型遍布于系统框架中,被用在使用Object-C对象会很影响性能的地方。创建对象比结构体会有一些其他额外的耗费,例如分配和释放堆内存的代价。当需要紧紧保存非对象类型(int,float,double,char,etc)时候,像CGRect这样的结构体通常会被使用。

       在使用OC开发东西之前,我建议你首先阅读以下C语言的相关内容,以熟悉相关的语法。否则,如果你深入到Object-C内部,你可能会有不少地方的语法困惑。

要注意的内容

       Object-C是C的超集,加入了面向对象的特性。Object-C使用消息机制,并进行动态绑定,这意味着对象的类型是在运行时决定。运行时而不是编译时决定消息要执行的具体代码。

       理解C的核心概念会帮助你写出更高效的OC代码。特别的,你需要理解内存模型和指针。

条目2:尽力减少在头文件引入其他的头文件

       像C,C++一样,Object-C也使用头文件和实现文件。当创建一个OC的类,标准的做法是创建一个以类的名字命名的以.h结尾的头文件和以.m结尾的实现文件。当你创建一个类,它可能看起来像这样:

//EOCPerson.h

#import <Foundation/Foundation.h>

 

@interface EOCPerson: NSObject

@property (nonatomic, copy) NSString*firstName;

@property (nonatomic, copy) NSString*lastName;

@end

 

//EOCPerson.m

#import “EOCPerson.h”

@implementation EOCPerson

@end

 

只要你曾从事过OC开发,Foundation.h头文件基本都会被引入。除了这,你可能还需要引用超类所在的框架的头文件。例如,如果你进行ios开发,你可能会子类化UIViewController。这些类的头文件会引入UIKit.h。

       正如上面那样,这个类可以工作的很好。它导入整个Foundation框架。考虑到它从Foundation框架的其中一个类继承,ECOPerson的使用者也极有可能使用Foundation框架的其他类。同样对于UIViewController来说,用户也可能会使用UIKit的很多类。

       随着时间流逝,你可能需要创建一个叫EOCEmployer的新类,然后你确定EOCPerson应该会使用它,因此你继续在EOCPerson中添加一个熟悉:

//EOCPerson.h

#import <Foundation/Foundation.h>

 

@interface EOCPerson: NSObject

@property (nonatomic, copy) NSString*firstName;

@property (nonatomic, copy) NSString*lastName;

@property (nonatomic,copy) EOCEmployer *employer;

@end

       这样有一个问题,在编译引入EOCPerson类的文件时,EOCEmployer类并不可见。让所有想引入EOCPerson类的文件都去引入EOCEmployer文件并不合适,因此通常的做法是添加如下在EOCPerson.h的最开头。

#import “EOCEmploryer.h”

       这当然可以工作,但是是一种坏的编程实践。在编译EOCPerson的时候,你并需要支持EOCEmployer类的全部细节,你需要知道它是一个类并且存在就可以了。幸运的是,有一种方法可以告诉编译器这些:

@class EOCEmployer;

       这叫做类的前向声明。通过这种方式EOCPerson类可能看起来像这样:

//EOCPerson.h

#import <Foundation/Foundation.h>

 

@class EOCEmployer;

 

@interface EOCPerson: NSObject

@property (nonatomic, copy) NSString*firstName;

@property (nonatomic, copy) NSString*lastName;

@property (nonatomic,copy) EOCEmployer *employer;

@end

 

//EOCPerson.m

#import “EOCPerson.h”

@implementation EOCPerson

@end

       EOCPerson的实现文件(即.m文件)需要引入EOCEmployer的头文件,因为他需要EOCEmployer的类的函数的实现细节。因此EOCPerson类的实现文件看起来像这样:

//EOCPerson.m

#import “EOCPerson.h”

#import “EOCEmployer.h”

 

@implementation EOCPerson

@end

 

       推迟文件到需要的时候再导入可以减少类的使用者需要引入的文件的范围。在这个例子中,如果EOCPerson.h引入了EOCEmployer.h,那么所有使用EOCPerson.h的地方都会引入了EOCEmployer.h。如果这个导入的链条层次很长,你可能会导入很多根本不需要的文件,这无疑会增加编译时间。

       使用前向声明同时可以解决相互引用的问题。考虑这样一种情况,如果EOCEmployer有添加和删除Employer的方法,方法的声明看起来像这样:

- (void) addEmployer:(EOCPerson*) person;

- (void) removeEmployer:(EOCPerson*) person;

这次EOCPerson也需要对EOCEmployer可见。可是如果通过头文件引入的方式会产生鸡生蛋,蛋生鸡的问题。当其中一个头文件被解析的时候,它需要另外的文件,同样另外的文件又需要当前的文件。使用#import而不是#include不会导致进入编译的无限循环,但是最后总有一个文件没有正常编译。如果不相信我,你可以试验一下。

有时候,可是你仍需要在一个头文件中引入另外的头文件。你必须引入你继承的超类的头文件。与此相似,你也必须引入你所遵循的协议的头文件,他们必须被提前定义,而且不能使用前向声明技术。编译器需要能够看到协议里面的方法而不仅仅是通过前向声明声称协议存不存在。

例如,假设一个矩形类继承于一个Shape类,而且遵循协议使它可以被画出来。

 

//EOCRectangle.h

#import“EOCShape.h”
#import “EOCDrawable.h”

 

@interfaceEOCRectangle : EOCShape<EOCDrawable>

@property(nonatomic, assign) float width;

@ property(nonatomic, assign) float height;

@end

其他的引入不可避免,对于这样的协议,把它们单独放在一个协议头文件中是明智的做法。如果EOCDrawable协议是一个很大的头文件的一部分,那么你不得不导入此头文件的全部,毫无疑问,这回大大增加编译时间。

也就是说,并不是所有的协议,例如delegate协议,需要在自己的独立的文件中。在这些情况下,the protocol make sense only when defined alongside the class forwhich it is a delegate. 在这些情况下,将协议声明放在类扩展通常是一种更好的做法。这意味着包含协议的头文件可以在实现文件中引入,而不是在公有.h文件中引入。

当开始引入头文件之前,总是问一下自己它是否是真正的必要的。如果import可以通过前向声明完成,那就选择它。如果import是被使用在一个property,实例变量,协议等可以被引入到类扩展中,请选择它。这样做可以大大减少编译时间同时减少文件依赖。依赖可能会产生维护的问题,仅仅暴露代码的小一部分到公有API中应该是我们都想要做的事情。

要记住的:

总是在最需要的情况下才去引入头文件。这通常意味着在头文件中进行前向声明,在实现文件中包含头文件。这样做会尽可能的避免类的交叉引入。

有时候前向声明是不可能的,例如遵循协议。在这种情况下,如果可能的话,考虑将协议遵循的声明移到类扩展中。否则,定义一个单独的协议文件来负责被引入。

抱歉!评论已关闭.