iOS 中协议和委托传值的用法

虽然之前一直听过和看过委托模式,但是并没有实际的操作。这次来实现一个简单的委托样例。来加深自己的了解。

项目目录结构为:

首先我们创建一个协议:ProtocolA

#import <Foundation/Foundation.h>

@protocol ProtocolA <NSObject>

@required

-(void)sendValue:(NSString*)str;


@end

其中有一个方法sendValue:,由于加上了@required前缀,所以这个方法时必须实现的。如果想要一个方法是可选则实现与否,则要加上@optional

之后我们创建一个类ClassA

//该类遵循ProtocolA,并且实现了它的方法

//ClassA.h
#import <Foundation/Foundation.h>
#import "ProtocolA.h"

@interface ClassA : NSObject<ProtocolA>

@end

//ClassA.m
#import "ClassA.h"

@implementation ClassA

-(void)sendValue:(NSString *)str{
    NSLog(@"我是传过来的值:%@" , str);
}

@end

写到这里,我们大概可以猜到,我们想让ClassA的实例来接受一个委托实现ProtocolA中的方法。

所以我们这里继续创建一个ClassB,让它来发出一个委托,让ClassA代替它实现。

//ClassB.h

#import <Foundation/Foundation.h>
#import "ClassA.h"

@interface ClassB : NSObject
//这里我们创建了一个delegate对象,用于设置委托对象。
@property(weak,nonatomic) id<ProtocolA> delegate;
//创建一个方法来执行委托操作
-(void)dowork;

@end

//ClassB.m

#import "ClassB.h"

@implementation ClassB

//创建a的实例,将a设置为接受委托的对象。
-(void)dowork{
    ClassA *a = [ClassA new];
    self.delegate = a;
    //当响应了委托方法时,执行sendValue:
    if ([self.delegate respondsToSelector:@selector(sendValue:)]) { // 如果协议响应了sendValue:方法
        [self.delegate sendValue:@"Hello"]; // 通知执行协议方法
    }
}

@end

最后在main.m中,执行:

#import <Foundation/Foundation.h>
#import "ClassB.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        ClassB *b = [ClassB new];
        [b dowork];
    }
    return 0;
}

运行结果为:

2017/8/28 posted in  iOS

第二十一条 理解Objective-C的错误模型

Objective-C语言不例外的也有“异常”(exception)机制,但是与其它语言肯定存在差异。我们要重新学习一下。

首先就是,“自动引用计数”(Automatic ReferenceCounting, ARC)在默认情况下不是“异常安全的"(exception safe)。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样要执行这部分代码。需要打开的编译器标志叫做-fobjc-arc-exceptions

但是我们应该注意,Objective-C语言只有在极其罕见的情况下拋出异常,异常拋出之后,无须考虑恢复问题,而且应用程序此时也应该退出。这就是说,不用再编写复杂的“异常安全”代码了。

异常只应该用于极其严重的错误,比如说,你编写了某个抽象基类,它的正确用法是先从中继承一个子类,然后使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。

与其他语言不同,Objective-C中没办法将某个类标识为“抽象 类”。要想达成类似效果,最好的办法是在那些子类必须覆写的超类方法里抛出异常。这样的话,只要有人直接创建抽象基类的实例并使用它,即会拋出异常:

-(void)mustOverrideMethod {
NSString *reason = [NSStringstringWithFormat: 
                    @"%@ must be overridden",
                    NSStringFromSelector(_cmd)];
@throw [NSException
    exceptionWithName:NSInternalInconsistencyException 
    reason:reason 
    userInfo:nil];
}

既然异常只用于处理严重错误(fatal error,致命错误),那么对其他错误怎么办呢?在出 现“不那么严重的错误"(nonfatal error,非致命错误)时,Objective-C语言所用的编程范式为: 令方法返回nil/0,或是使用NSError,以表明其中有错误发生。例如,如果初始化方法无法根据传入的参数来初始化当前实例,那么就可以令其返回nil/0:

-(id)initWithValue:(id)value { 
    if ((self = [super init])){
        if ( /* Value means instance can11 be created */ ) { 
        self = nil;
     } else {
        // Initialize instance
        }
     }
    return self;
}

在这种情况下,如果if语句发现无法用传人的参数值来初始化当前实例(比如这个方法 要求传入的value参数必须是non-nil的),那么就把self设置成nil,这样的话,整个方法的 返回值也就是nil了。调用者发现初始化方法并没有把实例创建好,于是便可确定其中发生了错误。

NSError的用法更加灵活,因为经由此对象,我们可以把导致错误的原因回报给调用者。 NSError对象里封装了三条信息:

  • Error domain(错误范围,其类型为字符串)
    错误发生的范围。也就是产生错误的根源,通常用一个特有的全局变量来定义。比方说,“处理URL的子系统”(URL-handling subsystem)在从URL中解析或取得数据时如果出错了,那么就会使用NSURLErrorDomain来表示错误范围。

  • Error code(错误码,其类型为整数)
    独有的错误代码,用以指明在某个范围内具体发生了何种错误。某个特定范围内可能会发生一系列相关错误,这些错误情况通常采用enum来定义。例如,当HTTP请求出错时,可能会把HTTP状态码设为错误码。

  • Uesr info(用户信息,其类型为字典)
    有关此错误的额外信息,其中或许包含一段“本地化的描述”(localized description), 或许还含有导致该错误发生的另外一个错误,经由此种信息,可将相关错误串成一条 “错误链”(chain of errors)

NSError的一种常见用法是,经由方法的“输出参数”返回给调用者。比如像这样:

-(BOOL)doSomething: (NSError**)error

用例为:

NSError *error = nil;
BOOL ret = [object doSomething:&error];
    if (error) {
//There was an error
}

也可以通过委托协议来传递此错误。有错误发生时,当前对象会把错误信息经由协议中的某个方法传给其委托对象(delegate)。这里不做过多说明。

要点

  • 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
  • 在错误不那么严重的情况下,可以指派“委托方法”(delegate method)来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。
2017/8/28 posted in  第三章 接口与API设计

第二十条 为私有方法名加上前缀

一个类所做的事情,通常要比外面看到的更多。编写类的实现代码时,我们经常要编写在内部使用的方法。这里我们要注意,一定要为这种内部实现的方法加上前缀,这有助于调试,有利于区分私有方法和公共方法。区分私有和公共方法,主要是为了方便修改内部的私有方法和相关实现代码,防止随意修改公共API。

我通常在私有方法前面加上“_p”,例如:


-(void)p_privateMethod{
    /*.....*/
}

我们之所以要加上前缀,是因为OC不像java和c++语言可以在前面将方法声明为私有。每个对象都可以响应任何方法(由于其强大的动态特性造成的)。

加前缀的时候要避免只用一个“_”因为苹果公司在自己的类中就是这么做的,所以我们要避开这种方式,防止造成继承一个类的时候,子类无意间覆写了父类的方法。

要点

  • 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
  • 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
2017/8/28 posted in  第三章 接口与API设计

第十九条 使用清晰而协调的命名方式

我们在使用OC的时候,发现这门语言很繁琐,代码中一般有“in“,”for”,“with”等介词,其他编程语言则很少使用这些他们认为多余的字眼。以下面代码为例子:

NSString *text = @"The quick brown fox jumped over the lazy dog";
NSString *newText =
[text stringByReplacingOccurrencesOfString: @"fox",
                                withString:@"cat"];

但是,Objective-C的命名方式虽然长一点,但是却非常淸晰。

方法与变量名使用了“驼峰式大小写命名法"(camel casing)——以小写字母开头,其后每个单词首字母大写。类名也用驼峰命名法,不过其首字母要大写,而且前面通常还有两三个前缀字母。

方法命名

方法名很长对冉繁琐,但是易于阅读,理解其中的意思。但是如果过长会起到反效果。例如:

-(EOCRectangle*)union:(EOCRectangle*)rectangle // Unclear 
-(float) calculateTheArea // Too verbose

//应该改成
-(EOCRectangle*)unionRectangle:(EOCRectangle*)rectangle 
-(float) area

给方法命名时的注意事项可总结成下面几条规则:

  • 如果方法的返回值是新创建的,那么方法名的首个词应是返回值的类型,除非前面还 有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认 为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当 于原有的对象。这些存取方法应该按照其所对应的属性来命名。
  • 应该把表示参数类型的名词放在参数前面。
  • 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数, 则应该在动词后面加上一个或多个名词。
  • 不要使用str这种简称,应该用string这样的全称。
  • Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功 能,选用has或is当前缀。
  • 将get这个前缀留给那些借由“输出参数”来保存返回值的方法,比如说,把返回值 填充到“C语言式数组”(C_style array)里的那种方法就可以使用这个词做前缀。

类与协议的命名

应该为类与协议的名称加上前缀,以避免命名空间冲突(参见第15条),而且应该像给 方法起名时那样把词句组织好,使其从左至右读起来较为通顺。例如,在NSArray的子类中,有一个用于表示可变数组的类,叫做NSMutableArray, mutable这个词放在array前面, 用以表明这是一种特殊的array(数组)。

例如iOS的UI库UIKit,其中协议与类的命名惯例为:

  • UIView (类)
  • UIViewController(类)
  • UITableView (类)
  • UITableViewController(类)
  • UITableViewDelegate(协议)

最重要的一点就是,命名方式应该协调一致。

要点

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

第十八条 尽量使用不可变对象

我们在设计类的时候,运用属性来封装数据。并且使用属性时候,可以将属性直接设置为“只读”(read-only)。默认情况下是“可读也可写”的。

不过,一般情况下我们要建模的数据未必需要改变。比方说,某数据所表示的对象源自一项只读的网络服务(web service),里面可能包含一系列需要显示在地图上的相关点,像这种对象就没必要改变其内容。即使修改了,新数据也不会推送回服务器.

而且如果把可变对象(mutable object)放入collection之后又修改其内容,那么很容易就会破坏set的内部数据结构,使其失去固有的语义。

我们来举例:

为了将EOCPointOflnterest做成不可变的类,需要把所有属性都声明为readonly:

#import <Foundation/Foundation.h>
@interface EOCPointOfInterest : NSObject
@property (nonatomic, copy, readonly) NSString *identifier; 
@property (nonatomic, copy, readonly) NSString* title;
@property (nonatomic, assign, readonly) float latitude; 
@property (nonatomic, assign, readonly) float longitude;
-(id) initWithldentifier: (NSString*) identifier 
                   title:(NSString*)title 
                   latitude: (float) latitude 
                   longitude: (float) longitude;
@end

这样后如果有人想要改变属性值,那么编译时就会报错。对象中的属性值可以读出,但是无法写入,这就能保证EOCPointOfluterest中的各个数据之间总是相互协调的。

但是我们有时想要修改封装在对象内部的数据,不想令这些数据为外人所改动。这种情况下,通常做法是在对象内部将readonly属性重新声明为readwrite。当然,如果该属性是nonatomic 的,那么这样做可能会产生“竞争条件”(racecondition)。在对象内部写人某属性时,对象外的 观察者也许正读取该属性。若想避免此问题,我们可以在必要时通过“派发队列"(dispatchqueue)等手段,将(包括对象内部的)所有数据存取操作都设为同步操作将属性在对象内部重新声明为readwrite这一操作可于“class-continuation分类”中完成,在公共接口中声明的属性可于此处重新声明,属性的其他特质必须保持不变,而readonly可扩展为readwrite

“class-continuation分类”可以这样写:

// .m文件中
#import "EOCPointOfInterest.h"

@interface EOCPointOfInterest : NSObject
@property (nonatomic, copy, readwrite) NSString *identifier; 
@property (nonatomic, copy, readwrite) NSString* title;
@property (nonatomic, assign, readwrite) float latitude; 
@property (nonatomic, assign, readwrite) float longitude;

@implementation EOCPointOfInterest

/* ... */

@end

现在,只能于EOCPoimOflnterest实现代码内部设置这些属性值了。但是我们其实可以同构KVC键值编码来设置这些属性值。不过,这样显然违背了我们的本意,绕过了提供的API。不推荐这种做法。

我们定义类公共的API时,要注意一件事情:对象里表示各种collection的那些属性究竞应该设成可变的,还是不可变的。例如,我们用某个类来表示个人信息,该类里还存放了一些引用,指向此人的诸位朋友。你可能想把这个人的全部朋友都放在一个“列表"(list)里,并将其做成属性。假如开发者可以添加或删除此人的朋友,那么这个属性就需要用可变的set来实现。在这种情况下,通常应该提供一个readonly属性供外界使用,该属性将返回不可变的set, 而此set则是内部那个可变set的一份拷贝。比方说,下面这段代码就能够实现出这样一个类:

// EOCPerson.h
#import <Foundation/Foundation•h>
@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName; 
@property (nonatomic, copy, readonly) NSString *lastName; 
@property (nonatomic, strong, readonly) NSSet *friends;
-(id)initWithFirstName:(NSString*)firstName
          andLastName:(NSString*)lastName;
-(void)addFriend:(EOCPerson*)person;
-(void)removeFriend:(EOCPerson*)person;

@end

// EOCPerson.m #import "EOCPerson•!!
@interface EOCPerson ()
@property (nonatomic, copy, readwrite) NSString *firstName; 
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
@implementation EOCPerson {
    NSMutableSet *_internalFriends;
}

-(NSSet*)friends {
    return [_internalFriends copy];
}
-(void)addFriend:(EOCPerson*)person {
    [_internalFriends addObject:person];
} 

-(void)removeFriend:(EOCPerson*)person {
    [_internalFriends removeObjectrperson];
}

-(id)initWithFirstName: (NSString*)firstName andLastName:(NSString*)lastName { 
    if ((self = [super init】)){
         _firstName = firstName;
         _lastName = lastName;
         _internalFriends = [NSMutableSet new];
    }
    return self;
}

@end

要点

  • 尽量创建不可变的对象。
  • 若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性
  • 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection
2017/8/26 posted in  第三章 接口与API设计

第十七条 实现description方法

调试程序时,我们一般要将对象的属性NSLog出来:

NSLog(@"object = %@",object);

如果我们输出的对象是数组,或者字典和其它数据是可以将数据打印出来。但是如果打印的是自定义的类,那么输出的对象就类似于:

object = <EOCPerson:0x7fd9a1600600>

上面这种信息很不实用,所以我们要在自己的类里覆写description方法,否则打印信息时就会调用NSObject类所实现的默认方法。此方法定义在NSObject协议里,不过NSObject类也实现了它。

想输出更为有用的信息也很简单,只需覆写description方法并将描述此对象的字符串 返回即可。例如,有下面这个代表个人信息的类:

//EOCPerson.h
#import <Foundation/Foundation.h>

@interface EOCPerson : NSObject

@property(nonatomic, readonly, copy)NSString *firstname;
@property (nonatomic, copy, readonly) NSString *lastName;

-(id) initWithFirstName: (NSString*) firstName lastName:(NSString*) lastName;

@end

//EOCPerson.m
#import "EOCPerson.h"

@implementation EOCPerson

-(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{
    if ((self = [super init])) {
        _firstname = [firstName copy];
        _lastName = [lastName copy];
    }
    return self;
}
//description方法
-(NSString*)description{

    return [NSString stringWithFormat:@"<%@ : %p , \"%@ %@\">" , [self class], self ,_firstname ,_lastName ];
}

@end


那么输出就是:

#import <Foundation/Foundation.h>
#import "EOCPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        EOCPerson *person = [[EOCPerson alloc] initWithFirstName:@"Liang" lastName:@"Zhonghao"];
        NSLog(@"Person = %@" , person);
    }
    return 0;
}

建议:在新实现的 description方法中,也应该像默认的实现那样,打印出类的名字和指针地址,因为这些内容 有时也许会用到。

我们也可以借助字典类型的description方法来将打印何种信息标识出来:


-(NSString*)description{
    return [NSString stringWithFormat:@"<%@ : %p , %@>" , 
    [self class],
     self, 
     @{@"firstname":_firstname,
       @"lastname":_lastName}];
}

输出为:

NSObject协议中还有个方法要注意,那就是debugDescription,此方法的用意与 description非常相似。二者区别在于,debugDescription方法是开发者在调试器(debugger) 中以控制台命令打印对象时才调用的。在NSObject类的默认实现中,此方法只是直接调用了description。我们还拿上个例子来说明:

我们在NSLog下面打一个断点,进入调试模式,之后向调试控制台里输入命令。LLDB的“po”命令 可以完成对象打印(print-object)工作,其输出如下:

当然我们可以把人名放在EOCPerson对象的普通描述信息中,而把更详尽的内容放在调试所用的描述信息里:

-(NSString*)description{
    return [NSString stringWithFormat:@"%@ : %@>" , _firstname ,_lastName ];
}

-(NSString*)debugDescription{
    return [NSString stringWithFormat:@"<%@ : %p , %@>" , [self class], self , @{@"firstname":_firstname,@"lastname":_lastName}];
}

运行之后如下:

你可能不想把类名与指针地址这种额外内容放在普通的描述信息里,但是却希望调试的 时候能够很方便地看到它们,在此情况下,就可以使用这种输出方式来实现。Foundation框架的NSArray类就是这么做的.

要点

  • 实现description方法返回一个有意义的字符串,用以描述该实例。
  • 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法。
2017/8/26 posted in  第三章 接口与API设计