美文网首页
《Effective Objective-C 2.0》读书笔记

《Effective Objective-C 2.0》读书笔记

作者: 锦鲤跃龙 | 来源:发表于2019-08-05 22:19 被阅读0次

第3章:接口与API设计

第15条:用前缀 避免命名空间冲突

  • 选择与你的公司,应用程序或两者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀
  • 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀

顾名思义就是说在自己开发的类需要加前缀,iOS程序员开发工程师普遍使用双字母的前缀,这是不科学的,因为苹果爸爸公司保留使用所有“两字母前缀”的权利,所以自己的前缀应该是三个字母的,不仅仅是类名,还有分类、全局变量…

第16条:提供“全能初始化方法”

在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
若全能初始化方法与超类不同,则需覆写超类中对应的方法
如果超类的初始化方法不适应于子类,那么应该覆写这个超类方法,并在其中抛出异常
举一个生动形象的例子:
Chinese 类

//.h
//  中国人
#import <Foundation/Foundation.h>
@interface Chinese : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@property (nonatomic, assign, readonly) NSUInteger age;
/// 全能初始化对象方法
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age;
/// 全能初始化类方法
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age;
/// 其他初始化对象方法
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName;
@end

//.m
#import "Chinese.h"
@interface Chinese()
@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, assign) NSUInteger age;
@end
@implementation Chinese
/// 全能初始化函数-只有全能初始化函数才能进行赋值操作
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
    if (self = [super init]) {
        self.firstName = firstName;
        self.lastName = lastName;
        self.age = age;
    }
    return self;
}
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
    Chinese *people = [[self alloc] initWithFirstName:firstName lastName:lastName age:age];
    return people;
}
- (instancetype)init {
    return [self initWithFirstName:@"龙的" lastName:@"传人" age:1]; // 调用指定初始化函数赋予其默认值
}
+ (instancetype)chineseWithFirstName:(NSString *)firstName lastName:(NSString *)lastName {
    return [self chineseWithFirstName:firstName lastName:lastName age:1];
}
@end

Student 类继承自 Chinese

//.h
//  中国学生
#import "Chinese.h"
@interface Student : Chinese
@property (nonatomic, strong, readonly) NSArray *homework;
/// 指定初始化函数-需直接调用父类初始化函数
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework;
/// 指定初始化类方法
+ (instancetype)studentWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework;
/// 其他初始化方法
+ (instancetype)studentWithHomework:(NSArray *)homework;
@end

//.m
#import "Chinese.h"
@implementation Student {
    NSMutableArray *p_homework;
}
/// 子类重写父类全能初始化函数-更改默认值!
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age {
    return [self initWithFirstName:firstName lastName:lastName age:age homework:@[]];
}
/// 指定初始化函数-需直接调用父类初始化函数
- (instancetype)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework {
    if (self = [super initWithFirstName:firstName lastName:lastName age:age]) {
        p_homework = homework.mutableCopy;
    }
    return self;
}
/// 指定初始化类方法
+ (instancetype)studentWithFirstName:(NSString *)firstName lastName:(NSString *)lastName age:(NSUInteger)age homework:(NSArray *)homework {
    return [[self alloc] initWithFirstName:firstName lastName:lastName age:age homework:homework];
}
/// 重写系统初始化方法
- (instancetype)init {
    return [self initWithFirstName:@"祖国的" lastName:@"花朵" age:6 homework:@[]];
}
/// 其他初始化方法
+ (instancetype)studentWithHomework:(NSArray *)homework {
    return [self studentWithHomework:homework];
}
@end

第17条:实现 description 方法

在打印我们自己定义的类的实例对象时,在控制台输出的结果往往是这样的:

object = <EOCPerson: 0x7fd9a1600600>

这里只包含了类名和内存地址,它的信息显然是不具体的,远达不到调试的要求。

但是!如果在我们自己定义的类覆写description方法,我们就可以在打印这个类的实例时输出我们想要的信息。
例如:

- (NSString*)description {
     return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}

在这里,显示了内存地址,还有该类的所有属性。
而且,如果我们将这些属性值放在字典里打印,则更具有可读性:

- (NSString*)description {

     return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
   
    @{    @"title":_title,
       @"latitude":@(_latitude),
      @"longitude":@(_longitude)}
    ];
}

输出结果:

location = <EOCLocation: 0x7f98f2e01d20, {

    latitude = "51.506";
   longitude = 0;
       title = London;
}>

我们可以看到,通过重写description方法可以让我们更加了解对象的情况,便于后期的调试,节省开发时间。

在打印我们自己定义的类的实例对象时,在控制台输出的结果往往是这样的:
object = <EOCPerson: 0x7fd9a1600600>

这里只包含了类名和内存地址,它的信息显然是不具体的,远达不到调试的要求。

但是!如果在我们自己定义的类覆写description方法,我们就可以在打印这个类的实例时输出我们想要的信息。
例如:

- (NSString*)description {
     return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}

在这里,显示了内存地址,还有该类的所有属性。
而且,如果我们将这些属性值放在字典里打印,则更具有可读性:

- (NSString*)description {

     return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
   
    @{    @"title":_title,
       @"latitude":@(_latitude),
      @"longitude":@(_longitude)}
    ];
}

输出结果:

location = <EOCLocation: 0x7f98f2e01d20, {

    latitude = "51.506";
   longitude = 0;
       title = London;
}>

我们可以看到,通过重写description方法可以让我们更加了解对象的情况,便于后期的调试,节省开发时间。

第18条尽量使用不可变对象

  • 尽量创建不可变对象
  • 若某属性尽可用于对象内部修改,则在 “class-continuation分类” 中将其由readonly属性扩展为readwrite属性
  • 不要把可变对象的collection作为属性公开,而应提供相关方法,以此修改对象中的可变 collection

在开发自定义类时,在 .h 里声明的属性尽量设置为不可变,只读的属性,外界只能通过特定的方法更改其内容,这对于一个功能的封装性是至关重要的。例如我们之前所声明的 Student 类:

// .h
@interface Student : Chinese
@property (nonatomic, copy, readonly) NSString *school;
@property (nonatomic, strong, readonly) NSArray *homework;

- (void)addHomeworkMethod:(NSString *)homework;
- (void)removeHomeworkMethod:(NSString *)homework;
@end

// .m
@interface Student()
@property (nonatomic, copy) NSString *school;
@end
@implementation Student {
    NSMutableArray *p_homework;
}
- (void)addHomeworkMethod:(NSString *)homework {
    [p_homework addObject:homework];
}
- (void)removeHomeworkMethod:(NSString *)homework {
    [p_homework removeObject:homework];
}
- (instancetype)initWithSchool:(NSString *)school homework:(NSArray *)homework {
    if (self = [self init]) {
        self.school = school;
        p_homework = homework.mutableCopy;
    }
    return self;
}
@end

如此定义外界只能通过固定的方法对对象内的属性进行更新,便于功能的封装,减少 bug 出现的概率。
另外使用不可变对象也增强程序的执行效率

第19条:使用清晰而协调的命名方式

  • 起名时应遵从标准的 Objective-C命名规范,这样创建出来的接口更容易为开发者所理解
  • 方法名要言简意赅,从左至右读起来要像个日常用语的句子才好
  • 方法名里不要使用缩略后的类型名称
  • 给方法起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符

就是说在为自己创建的属性、成员变量、方法、协议等起名要见名知意
我们看一个例子:

先看名字取得不好的:

//方法定义
- (id)initWithSize:(float)width :(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithSize:5.0f :10.0f];

这里定义了Rectangle的初始化方法。虽然直观上可以知道这个方法通过传入的两个参数来组成矩形的size,但是我们并不知道哪个是矩形的宽,哪个是矩形的高。
来看一下正确的例子 :

//方法定义
- (id)initWithWidth:(float)width andHeight:(float)height;

//方法调用
EOCRectangle *aRectangle =[[EOCRectangle alloc] initWithWidth:5.0f andHeight:10.0f];

这个方法名就很好的诠释了该方法的意图:这个类的初始化是需要宽度和高度的。而且,哪个参数是高度,哪个参数是宽度,看得人一清二楚。永远要记得:代码是给人看的。

对于返回值是布尔值的方法,我们也要注意命名的规范:

获取”是否“的布尔值,应该增加“is”前缀:

- isEqualToString:

获取“是否有”的布尔值,应该增加“has”前缀:

- hasPrefix:

第20条:为私有方法名加前缀

  • 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开
  • 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的

对于一个写好的类而言,若为公开方法更改名称,则需要在外部调用此类的方法的地方同样做修改,这样比较麻烦,在类内部实现的私有方法不会有这个问题,所以为私有方法加前缀可更好的区分两者。便于后期开发。用何种前缀取决于开发者的开发习惯,不建议使用下划线开头的前缀,因为这是Apple Dad 专属的方式。作者的习惯是私有方法的前缀是 p_ ,例如:

/// 这是一个私有方法
- (id)p_playAirplaneMethod {
    id xx = @"**";
    return xx;
}

第21条:理解 Objective-C 错误类型

很多语言都有异常处理机制,Objective-C也不例外。@throw
但是,
注意:OC里抛异常很可能会导致内存泄漏
注意:OC里抛异常很可能会导致内存泄漏
注意:OC里抛异常很可能会导致内存泄漏
解释:OC里的ARC机制(Automatic Reference Counting)在默认情况下是“无异常安全”。简单来说,一旦抛出异常,对象很可能就无法正常自动释放了。
所以,

  1. 异常只用于处理严重的错误(fatal error,致命错误)
  2. 对于一些不那么严重的错误(nonfatal error,非致命错误),有两种解决方案:
    • 让对象返回nil或者0(例如:初始化的参数不合法,方法返回nil或0)
    • 使用NSError

在项目中可以自定义一个错误类型模型:

//  .h
//  自定义错误类型
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, LYJErrorCode) {
    LYJErrorCodeUnknow       = -1, //未知错误
    LYJErrorCodeTypeError    = 100,//类型错误
    LYJErrorCodeNullString   = 101,//空字符串
    LYJErrorCodeBadInput     = 500,//错误的输入
};
extern NSString * const LYJErrorDomain;
@interface LYJError : NSError
+ (instancetype)errorCode:(LYJErrorCode)errorCode userInfo:(NSDictionary *)userInfo;
@end

// .m
#import "LYJError.h"
@implementation LYJError
NSString * const LYJErrorDomain = @"LYJErrorDomain";
+ (instancetype)errorCode:(LYJErrorCode)errorCode userInfo:(NSDictionary *)userInfo {
    LYJError *error = [[LYJError alloc] initWithDomain:LYJErrorDomain code:errorCode userInfo:userInfo];
    return error;
}
@end

在调试程序合适的回调中可传入自定义错误信息。

第22条:理解 NSCopying 协议

  • 若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议
  • 如果自定义的对象分为可变版本和不可变版本。那么就要同时实现 NSCopying 协议 与 NSMutableCopying 协议
  • 赋值对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝
  • 如果你写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法

同时,在拷贝对象时,要注意是执行浅拷贝还是深拷贝

那么引出了一个概念:什么是深拷贝?什么是浅拷贝?

  • 深拷贝:内容拷贝(既拷贝新的指针又拷贝出新的Object
  • 浅拷贝:指针拷贝(仅拷贝新的指针指向原来的Object

这里有张很经典的图解:

image

深拷贝在拷贝对象时,会将指针所指的底层数据也拷贝一份。而浅拷贝只是创建了一个新的指针指向要拷贝的内容。一般情况下,尽量使用浅拷贝。
此外,还有一个注意点:
[NSMutableArray copy] 拷贝出 => NSArray (不可变)
[NSArray mutableCopy] 拷贝出 => NSMutableArray(可变)
这种操作可以在可变版本和不可变版本间切换。

相关文章

网友评论

      本文标题:《Effective Objective-C 2.0》读书笔记

      本文链接:https://www.haomeiwen.com/subject/xgwvdctx.html