[精通Objective-C]键值编程

[精通Objective-C]键值编程

参考书籍:《精通Objective-C》【美】 Keith Lee

目录

  • 精通Objective-C键值编程

    • 目录
    • 键值编码KVC
    • 键值观察KVO

键值编码KVC

键值编码API可以直接访问类的属性:

@interface Hello : NSObject
@property NSString* greeting;
@end

@implementation Hello
-(id)init{
    if ((self = [super init])) {
        _greeting = @"Hello";
    }
    return self;
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Hello *hello = [Hello new];
        // 相当于hello.greeting = @"Hi"
        [hello setValue:@"Hi" forKey:@"greeting"];
        // 相当于NSLog(@"%@",hello.greeting)
        NSLog(@"%@",[hello valueForKey:@"greeting"]);
    }
    return 0;
}

使用键值编码访问属性与标准的属性访问方法相比,是基于配置的属性访问,可以降低耦合性,简化代码,易于维护和拓展。

键值编码使用键和键路径访问属性。键是用于标识属性的字符串。键路径指明了需要遍历的对象属性序列。键值编码可以使用点语法的键路径:

@interface Hello : NSObject
@property NSString *greeting;
@end

@implementation Hello
-(id)init{
    if ((self = [super init])) {
        _greeting = @"Hello";
    }
    return self;
}
@end

@interface Person : NSObject
@property Hello *hello;
@end

@implementation Person

-(id)init{
    if ((self = [super init])) {
        _hello = [Hello new];
    }
    return self;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person new];
        // 相当于person.hello.greeting = @"Hi"
        [person setValue:@"Hi" forKeyPath:@"hello.greeting"];
        // 相当于NSLog(@"%@",person.hello.greeting)
        NSLog(@"%@",[person valueForKeyPath:@"hello.greeting"]);
    }
    return 0;
}

使用NSObject类遵守的协议MSKeyValueCoding中的valueForUndefinedKey:方法可以处理未定义键情况:

@implementation Person
...
-(id)valueForUndefinedKey:(NSString *)key{
    // 如果输入键为hi,则返回hello属性,否则抛出异常
    if((nil != key) && ([@"hi" isEqualToString:key])){
        return self.hello;
    }
    [NSException raise:NSUndefinedKeyException format:@"key %@ not defined", key];
    return nil;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person new];
        // 可以访问
        NSLog(@"%@",[person valueForKeyPath:@"hi.greeting"]);
        // 会抛出异常
        NSLog(@"%@",[person valueForKeyPath:@"Hi.greeting"]);
    }
    return 0;
}

键值编码还未检验属性值提供了基础设施,检查方法的选择器为validate<key>:error:,其中是属性名称。

@implementation Person
...
-(BOOL)validateHello:(id *)value error:(NSError * __autoreleasing *)error{
    if (*value == nil) {
        if (error != NULL) {
            *error = [NSError errorWithDomain:@"Invalid Property Value (nil)" code:1 userInfo:nil];
        }
        return NO;
    }
    return YES;
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person new];
        Hello *hello = [Hello new];
        [hello setValue:@"Hi" forKey:@"greeting"];

        NSError *error;
        BOOL valid = [person validateValue:&hello forKey:@"hello" error:&error];
        if (valid) {
            [person setValue:hello forKey:@"hello"];
            NSLog(@"%@",[person valueForKeyPath:@"hello.greeting"]);
        }
    }
    return 0;
}

键值观察KVO

添加和删除观察对象:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [Person new];
        Hello *hello = [Hello new];

        // 添加观察对象
        [person addObserver:hello forKeyPath:@"hello" options:NSKeyValueObservingOptionNew context:NULL];
        // 删除观察对象
        [person removeObserver:hello forKeyPath:@"hello"];
    }
    return 0;
}

键值观察与通知类(NSNotification)的区别:

1.通知类能够封装通用消息,使用更广泛。而键值观察仅支持对象属性更改通知功能。因此处理纯属性更改情况时,KVO API会更简单。

2.通知类使用交互的广播模型,无须接受对象注册通知功能,即可向一个以上的对象发送消息,即支持同步传递通知,也支持异步传递通知。而键值观察使用点对点的交互模型。

3.通知机制中,发送者和接受者没有直接的双向通信,必须注册通知才能进行双向通信。而键值观察中,观察者也能够向被观察者发送消息。

4.通知名称必须具备唯一性,苹果官方文档规定了一系列命名约定将通知名称冲突的可能性降到最低。而属性名称是在类中使用的(命名空间为类),而且被观察者与观察者直接绑定,所以不会出现命名冲突。

下面是一个实现KVO的示例:

首先创建一个用于被观察的Person类:

#import <Foundation/Foundation.h>

@interface Person : NSObject

@property(readonly) NSString *fullName; // 只读属性,源自firstName和lastName
@property NSString *firstName;
@property NSString *lastName;

-(id)initWithFirstName:(NSString *)fname lastName:(NSString *)lname;

@end
#import "Person.h"
#define CodersErrorDomain @"CodersErrorDomain"
#define kInvalidValueError 1

@implementation Person

-(id)initWithFirstName:(NSString *)fname lastName:(NSString *)lname{
    if ((self = [super init])) {
        _firstName = fname;
        _lastName = lname;
    }
    return self;
}

-(NSString *)fullName{
    return [NSString stringWithFormat:@"%@ %@",self.firstName, self.lastName];
}

// 键值检查方法
-(BOOL)validateLastName:(id *)value error:(NSError * __autoreleasing *)error{
    // 检查lastName是否为空
    if (*value == nil) {
        if (error != nil) {
            NSDictionary *reason = @{NSLocalizedDescriptionKey:@"Last name cannot be nil"};
            *error = [NSError errorWithDomain:CodersErrorDomain code:kInvalidValueError userInfo:reason];
        }
        return NO;
    }
    // 检查空值
    NSUInteger length = [[(NSString *)*value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length];
    if (length == 0) {
        if (error != nil) {
            NSDictionary *reason = @{NSLocalizedDescriptionKey:@"Last name cannot be nil"};
            *error = [NSError errorWithDomain:CodersErrorDomain code:kInvalidValueError userInfo:reason];
        }
        return NO;
    }
    return YES;
}

// 注册依赖键,fullName的值依赖于firstName和lastName,这两个属性有变动时,应该通知fullName属性的观察者
+(NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"firstName",@"lastName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

@end

接下来创建一个Coder类,作为Person类的观察者

#import <Foundation/Foundation.h>

@class Person;
@interface Coder : NSObject

@property Person *person;
@property NSMutableArray *languages; // 有序集合

@end
#import "Coder.h"

@implementation Coder

// 下面4个方法用于处理有序的一对多关系属性
-(NSUInteger)countOfLanguages{
    return [self.languages count];
}

-(NSString *)objectInLanguagesAtIndex:(NSUInteger)index{
    return [self.languages objectAtIndex:index];
}

-(void)insertObject:(NSString *)object inLanguagesAtIndex:(NSUInteger)index{
    [self.languages insertObject:object atIndex:index];
}

-(void)removeObjectFromLanguagesAtIndex:(NSUInteger)index{
    [self.languages removeObjectAtIndex:index];
}

// 当被观察属性的值发生改变时,被观察对象就会调用观察对象中的本方法
-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context{
    NSString *newValue = change[NSKeyValueChangeNewKey];
    NSLog(@"Value changed for %@ object, key path: %@, new value :%@", [object className], keyPath, newValue);
}
@end

再创建一个Coders类,测试无序一对多关系属性:

#import <Foundation/Foundation.h>

@class Coder;
@interface Coders : NSObject

@property NSSet *developers; // 无序集合

@end
#import "Coders.h"

@implementation Coders

// 以下3个方法用于处理无序一对多关系属性
-(NSUInteger)countOfDevelopers{
    return [self.developers count];
}

-(NSEnumerator *)enumeratorOfDevelopers{
    return [self.developers objectEnumerator];
}

-(Coder *)memberOfDevelopers:(Coder *)object{
    return [self.developers member:object];
}

@end

最后在main.m中进行测试:

#import <Foundation/Foundation.h>
#import "Person.h"
#import "Coder.h"
#import "Coders.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 创建一个Person对象,并用键值编码获取该实例的属性值
        Person *curly = [[Person alloc] initWithFirstName:@"Curly" lastName:@"Howard"];
        NSLog(@"Person first name:%@",[curly valueForKey:@"firstName"]);
        NSLog(@"Person full name:%@",[curly valueForKey:@"fullName"]);

        // 创建两个Coder对象,并用键值编码获取它们的person和languages属性
        NSArray *langs1 = @[@"Objective-C",@"C"];
        Coder *coder1 = [Coder new];
        coder1.person = curly;
        coder1.languages = [langs1 mutableCopy];
        NSLog(@"\nCoder name:%@\n\t languages:%@",[coder1 valueForKeyPath:@"person.fullName"],[coder1 valueForKey:@"languages"]);
        Coder *coder2 = [Coder new];
        coder2.person = [[Person alloc] initWithFirstName:@"Larry" lastName:@"Fine"];
        coder2.languages = [@[@"Objective-C",@"C++"] mutableCopy];
        NSLog(@"\nCoder name:%@\n\t languages:%@",[coder2 valueForKeyPath:@"person.fullName"],[coder2 valueForKey:@"languages"]);

        // 将Coder对象注册为Person对象的观察者,修改Person对象中被观察的属性,观察者将会收到通知
        [curly addObserver:coder1 forKeyPath:@"fullName" options:NSKeyValueObservingOptionNew context:NULL];
        curly.lastName = @"Fine";
        [curly removeObserver:coder1 forKeyPath:@"fullName"];

        // 创建一个Coders对象,并用操作符@count计算Coders对象中集合的对象总数
        Coders *bestCoders = [Coders new];
        bestCoders.developers = [[NSSet alloc] initWithArray:@[coder1,coder2]];
        NSSet* coders = [bestCoders valueForKey:@"developers"];
        NSLog(@"Number of coders = %@",[coders valueForKeyPath:@"@count"]);

        // 用空字符串测试键值编码检验方法
        NSError *error;
        NSString *emptyName = @"";
        BOOL valid = [curly validateValue:&emptyName forKey:@"lastName" error:&error];
        if (!valid) {
            NSLog(@"Error:%@",([error userInfo])[NSLocalizedDescriptionKey]);
        }
    }
    return 0;
}

运行结果:

2016-07-20 17:02:59.635 Coders[23278:214550] Person first name:Curly
2016-07-20 17:02:59.636 Coders[23278:214550] Person full name:Curly Howard
2016-07-20 17:02:59.636 Coders[23278:214550]
Coder name:Curly Howard
     languages:(
    "Objective-C",
    C
)
2016-07-20 17:02:59.637 Coders[23278:214550]
Coder name:Larry Fine
     languages:(
    "Objective-C",
    "C++"
)
2016-07-20 17:02:59.637 Coders[23278:214550] Value changed for Person object, key path: fullName, new value :Curly Fine
2016-07-20 17:02:59.637 Coders[23278:214550] Number of coders = 2
2016-07-20 17:02:59.637 Coders[23278:214550] Error:Last name cannot be nil
时间: 2024-07-30 02:53:44

[精通Objective-C]键值编程的相关文章

键值编码和通用编程

键值编码(KVC)是Cocoa一个通用的用于获取和设定值的一个协议.在编程中,“通用”这词是用于描述一种可以适用于不同情境的实现方式.通用代码可以减少项目中代码总量并使得软件可以处理程序员无法预知的情景.Cocoa至始至终都很重视通用.可复用代码. KVC版看起来需要更多代码输入.让就让我选择一个更能体现KVC价值的场景. 首先,我们定义一个类: @interface CDCPerson : NSObject {     NSString * firstName;     NSString *

obj-c编程17:键值观察(KVO)

说完了前面一篇KVC,不能不说说它的应用KVO(Key-Value Observing)喽.KVO类似于ruby里的hook功能,就是当一个对象属性发生变化时,观察者可以跟踪变化,进而观察或是修正这个变化,这是通过回调观察者注册的回调函数来完成的.要使用键值观察,必须满足3个条件: 1 被观察对象必须对所观察属性使用符合KVC标准的存取器方法: 2 观察者必须实现接受通知的方法(回调方法):-observeValue:forKeyPath:ofObject:change:context:,该方法

大数据技术之_19_Spark学习_02_Spark Core 应用解析+ RDD 概念 + RDD 编程 + 键值对 RDD + 数据读取与保存主要方式 + RDD 编程进阶 + Spark Core 实例练习

第1章 RDD 概念1.1 RDD 为什么会产生1.2 RDD 概述1.2.1 什么是 RDD1.2.2 RDD 的属性1.3 RDD 弹性1.4 RDD 特点1.4.1 分区1.4.2 只读1.4.3 依赖1.4.4 缓存1.4.5 CheckPoint第2章 RDD 编程2.1 RDD 编程模型2.2 RDD 创建2.2.1 由一个已经存在的 Scala 集合创建,即集合并行化(测试用)2.2.2 由外部存储系统的数据集创建(开发用)2.3 RDD 编程2.3.1 Transformatio

PHP编程实现多维数组按照某个键值排序的方法

1.array_multisort()函数对多个数组或多维数组进行排序. //对数组$hotcat按照count键值大小降序进行排序: $hotcat =array(  array('1501'=>array('catid'=>'1546','catname'=>'数组排序 一级','count'=>'588')),  array('1501'=>array('catid'=>'1546','catname'=>'数组排序二级','count'=>'588'

自增长的聚集键值不会扩展(scale)

如何选择聚集键值的最佳实践是什么?一个好的聚集键值应该有下列属性: 范围小的(Narrow) 静态的(Static) 自增长的(Ever Increasing) 我们来具体看下所有这3个属性,还有在SQL Server里为什么自增长值实际上是不会扩展的. 范围小的(Narrow) 聚集键值应该i越小越好.为什么?因为它要占用空间,聚集键值也在每个非聚集索引的叶子曾作为逻辑指针.如果你的聚集键值很广,你的非聚集索引也会很大.如果你定义了非唯一非聚集索引(Non-Unique Non-Cluster

Objective-C(十七、KVC键值编码及实例说明)——iOS开发基础

结合之前的学习笔记以及参考<Objective-C编程全解(第三版)>,对Objective-C知识点进行梳理总结.知识点一直在变,只是作为参考,以苹果官方文档为准~ 十七.键值编码 KVC 关于KVC的知识点将通过下列例子来展开说明: Person.h文件,Person类拥有name和age两个成员变量 @interface Person : NSObject { @private NSString *_name; NSInteger _age; } - (void)setAge:(NSIn

【插件开发】—— 8 IPreferenceStore,插件的键/值存储!

前文回顾: 1 插件学习篇 2 简单的建立插件工程以及模型文件分析 3 利用扩展点,开发透视图 4 SWT编程须知 5 SWT简单控件的使用与布局搭配 6 SWT复杂空间与布局搭配 7 SWT布局详解 前几篇讲解了SWT的基本知识.这篇作为穿插,讲述一下工作终于到的一个问题,并且借着这个机会,好好的学习了一下! 先描述下我遇到的问题吧: 由于对插件的了解也并不全面,很多知识点都不知道.学习其他人的开源源码时,我遇到了下面的报错: 1 org.eclipse.jface.resource.Data

实现键值对存储(二)——以现有键值对存储为模型

本文是<实现键值对存储>系列译文的第二篇 原文来自Emmanuel Goossaert (CodeCapsule.com) 本文中,开头我会解释使用现有模型而非重头开始此项目的原因.我会阐述一系列选择键值对存储模型的标准.最后我将对一些广为人知的键值对存储项目做一个概述,并用这些标准选择其中一些作为模型.本文将包含: 1. 不重新发明轮子 2. 备选模型和选择标准 3. 选择的键值对存储的概述 4. 参考文献 1. 不重新发明轮子 键值对存储已经被人们唱好至少30年了[1].最著名的一个项目是

ios中键值编码kvc和键值监听kvo的特性及详解

总结: kvc键值编码  1.就是在oc中可以对属性进行动态读写(以往都是自己赋值属性)           2. 如果方法属性的关键字和需要数据中的关键字相同的话                  3. 动态设置:setValue:属性值 forKey:属性名(用于简单的路径)/setValue:属性值 forKeyPath:属性名(用于复杂的路径)kvo键值监听  永久性的监听item属性值的改变,如果改变就从新设置             1.监听方法:[addObserver:self