寫在前面
平常開發中經常用到KVC賦值取值、字典轉模型,但KVC的底層原理又是怎樣的呢?本篇就來帶你走進KVC..
一、KVC初探
①.KVC定義及API
KVC(Key-Value Coding)
是利用NSKeyValueCoding
非正式協議實現的一種機制,對象采用這種機制來提供對其屬性的間接訪問.
寫下KVC
代碼并點擊跟進setValue
會發現NSKeyValueCoding
是在Foundation
框架下的
-
KVC
是通過對NSObject
的擴展來實現的 —— 只要繼承了NSObject
的類都可以使用KVC
-
NSArray、NSDictionary、NSMutableDictionary、NSOrderedSet、NSSet
等也遵守KVC
協議 - 除少數類型(結構體)以外都可以使用
KVC
int main(int argc, const char * argv[]) {
@autoreleasepool {
TCJPerson *person = [TCJPerson alloc];
[person setValue:@"TCJ" forKey:@"name"];
[person setValue:@"CJ" forKey:@"nickName"];
}
return 0;
}
KVC
常用方法,這些也是我們在日常開發中經常用到的
// 通過 key 設值
- (void)setValue:(nullable id)value forKey:(NSString *)key;
// 通過 key 取值
- (nullable id)valueForKey:(NSString *)key;
// 通過 keyPath 設值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;
// 通過 keyPath 取值
- (nullable id)valueForKeyPath:(NSString *)keyPath;
NSKeyValueCoding
類別的其它方法
// 默認為YES。 如果返回為YES,如果沒有找到 set<Key> 方法的話, 會按照_key, _isKey, key, isKey的順序搜索成員變量, 返回NO則不會搜索
+ (BOOL)accessInstanceVariablesDirectly;
// 鍵值驗證, 可以通過該方法檢驗鍵值的正確性, 然后做出相應的處理
- (BOOL)validateValue:(inout id _Nullable * _Nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
// 如果key不存在, 并且沒有搜索到和key有關的字段, 會調用此方法, 默認拋出異常。兩個方法分別對應 get 和 set 的情況
- (nullable id)valueForUndefinedKey:(NSString *)key;
- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
// setValue方法傳 nil 時調用的方法
// 注意文檔說明: 當且僅當 NSNumber 和 NSValue 類型時才會調用此方法
- (void)setNilValueForKey:(NSString *)key;
// 一組 key對應的value, 將其轉成字典返回, 可用于將 Model 轉成字典
- (NSDictionary<NSString *, id> *)dictionaryWithValuesForKeys:(NSArray<NSString *> *)keys;
②.拓展——自動生成的setter和getter方法
試想一下編譯器要為成千上萬個屬性分別生成setter
和getter
方法那不得歇菜了嘛
于是乎蘋果開發者們就運用通用原則給所有屬性都提供了同一個入口——objc-accessors.mm
中setter
方法根據修飾符不同調用不同方法,最后統一調用reallySetProperty
方法
來到reallySetProperty
再根據內存偏移量取出屬性,根據修飾符完成不同的操作
- 在第一個屬性
name
賦值時,此時的內存偏移量為8,剛好偏移isa
所占內存(8字節)來到name
- 在第二個屬性
nickName
賦值時,此時的內存偏移量為16,剛好偏移isa、name
所占內存(8+8)來到nickName
至于是哪里調用的objc_setProperty_nonatomic_copy
?
并不是在objc源碼
中,而在llvm源碼
中發現了它,根據它一層層找上去就能找到源頭
二、KVC使用
相信大部分閱讀本文的小伙伴們都對KVC的使用都比較了解了,但筆者建議還是看一下查漏補缺
typedef struct {
float x, y, z;
} ThreeFloats;
@interface TCJPerson : NSObject{
@public
NSString *myName;
}
@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) NSArray *array;
@property (nonatomic, strong) NSMutableArray *mArray;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic) ThreeFloats threeFloats;
@property (nonatomic, strong) TCJStudent *student;
@end
@interface TCJStudent : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *subject;
@property (nonatomic, copy) NSString *nick;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) NSInteger length;
@property (nonatomic, strong) NSMutableArray *penArr;
@end
①.基本類型
注意一下NSInteger
這類的屬性賦值時要轉成NSNumber
或NSString
TCJPerson *person = [[TCJPerson alloc] init];
// 1:Key-Value Coding (KVC) : 基本類型
[person setValue:@"TCJ" forKey:@"name"];
[person setValue:@19 forKey:@"age"];
[person setValue:@"CJ" forKey:@"myName"];
NSLog(@"%@ - %@ - %@",[person valueForKey:@"name"],[person valueForKey:@"age"],[person valueForKey:@"myName"]);
打印結果:
① - KVC簡介[10226:310799] TCJ - 19 - CJ
②.集合類型
TCJPerson *person = [[TCJPerson alloc] init];
// 2:KVC - 集合類型 -
person.array = @[@"1",@"2",@"3"];
// 由于不是可變數組 - 無法做到
// person.array[0] = @"100";
// 搞一個新的數組 - KVC 賦值就OK
NSArray *array = [person valueForKey:@"array"];
// 用 array 的值創建一個新的數組
array = @[@"100",@"2",@"3"];
[person setValue:array forKey:@"array"];
NSLog(@"%@",[person valueForKey:@"array"]);
// 取出數組以可變數組形式保存,再修改 KVC 的方式
NSMutableArray *ma = [person mutableArrayValueForKey:@"array"];
ma[0] = @"100";
NSLog(@"%@",[person valueForKey:@"array"]);
打印結果:
① - KVC簡介[10226:310799] (
100,
2,
3
)
① - KVC簡介[10226:310799] (
100,
2,
3
)
③.訪問非對象類型——結構體
- 對于非對象類型的賦值總是把它先轉成
NSValue
類型再進行存儲 - 取值時轉成對應類型后再使用
TCJPerson *person = [[TCJPerson alloc] init];
// 3:KVC - 訪問非對象屬性 - 面試可能問到
// 3.1 賦值
ThreeFloats floats = {1., 2., 3.};
NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)];
[person setValue:value forKey:@"threeFloats"];
NSValue *reslut = [person valueForKey:@"threeFloats"];
NSLog(@"非對象類型%@",reslut);
// 3.2 取值
ThreeFloats th;
[reslut getValue:&th] ;
NSLog(@"非對象類型的值%f - %f - %f",th.x,th.y,th.z);
打印結果:
① - KVC簡介[10226:310799] 非對象類型{length = 12, bytes = 0x0000803f0000004000004040}
① - KVC簡介[10226:310799] 非對象類型的值1.000000 - 2.000000 - 3.000000
④.集合操作符
- 聚合操作符
- @avg: 返回操作對象指定屬性的平均值
- @count: 返回操作對象指定屬性的個數
- @max: 返回操作對象指定屬性的最大值
- @min: 返回操作對象指定屬性的最小值
- @sum: 返回操作對象指定屬性值之和
- 數組操作符
- @distinctUnionOfObjects: 返回操作對象指定屬性的集合--去重
- @unionOfObjects: 返回操作對象指定屬性的集合
- 嵌套操作符
- @distinctUnionOfArrays: 返回操作對象(嵌套集合)指定屬性的集合--去重,返回的是 NSArray
- @unionOfArrays: 返回操作對象(集合)指定屬性的集合
- @distinctUnionOfSets: 返回操作對象(嵌套集合)指定屬性的集合--去重,返回的是 NSSet
集合操作符用得少之又少。下面舉個??
TCJPerson *person = [[TCJPerson alloc] init];
NSMutableArray *personArray = [NSMutableArray array];
for (int i = 0; i < 6; i++) {
TCJStudent *p = [TCJStudent new];
NSDictionary* dict = @{
@"name":@"Tom",
@"age":@(18+i),
@"nick":@"Cat",
@"length":@(175 + 2*arc4random_uniform(6)),
};
[p setValuesForKeysWithDictionary:dict];
[personArray addObject:p];
}
NSLog(@"%@", [personArray valueForKey:@"length"]);
/// 平均身高
float avg = [[personArray valueForKeyPath:@"@avg.length"] floatValue];
NSLog(@"%f", avg);
int count = [[personArray valueForKeyPath:@"@count.length"] intValue];
NSLog(@"%d", count);
int sum = [[personArray valueForKeyPath:@"@sum.length"] intValue];
NSLog(@"%d", sum);
int max = [[personArray valueForKeyPath:@"@max.length"] intValue];
NSLog(@"%d", max);
int min = [[personArray valueForKeyPath:@"@min.length"] intValue];
NSLog(@"%d", min);
打印結果:
① - KVC簡介[10544:326204] (
185,
177,
185,
177,
181,
175
)
① - KVC簡介[10544:326204] 180.000000
① - KVC簡介[10544:326204] 6
① - KVC簡介[10544:326204] 1080
① - KVC簡介[10544:326204] 185
① - KVC簡介[10544:326204] 175
⑤.層層嵌套
通過forKeyPath
對實例變量(student)進行取值賦值
TCJPerson *person = [[TCJPerson alloc] init];
TCJStudent *student = [[TCJStudent alloc] init];
student.subject = @"iOS";
person.student = student;
[person setValue:@"葵花寶典" forKeyPath:@"student.subject"];
NSLog(@"%@",[person valueForKeyPath:@"student.subject"]);
打印結果:
① - KVC簡介[10544:326204] 葵花寶典
三、KVC底層原理
由于NSKeyValueCoding
的實現在Foundation
框架中,但它又不開源,我們只能通過KVC官方文檔來了解它
①.設值過程
官方文檔上對Setter方法的過程進行了這樣一段講解
- ①.按
set<Key>:
、_set<Key>:
順序查找對象中是否有對應的方法- 找到了直接調用設值
- 沒有找到跳轉第2步
- ②.判斷
accessInstanceVariablesDirectly
結果- 為YES時按照
_<key>
、_is<Key>
、<key>
、is<Key>
的順序查找成員變量,找到了就賦值;找不到就跳轉第3步 - 為NO時跳轉第3步
- 為YES時按照
- ③.調用
setValue:forUndefinedKey:
。默認情況下會引發一個異常,但是繼承于NSObject
的子類可以重寫該方法就可以避免崩潰并做出相應措施
②.取值過程
同樣的官方文檔上也給出了Getter方法的過程
-
①.按照
get<Key>
、<key>
、is<Key>
、_<key>
順序查找對象中是否有對應的方法- 如果有則調用getter,執行第5步
- 如果沒有找到,跳轉到第2步
-
②.查找是否有
countOf<Key>
和objectIn<Key>AtIndex:
方法(對應于NSArray
類定義的原始方法)以及<key>AtIndexes:
方法(對應于NSArray
方法objectsAtIndexes:
)- 如果找到其中的第一個(
countOf<Key>
),再找到其他兩個中的至少一個,則創建一個響應所有NSArray
方法的代理集合對象,并返回該對象(即要么是countOf<Key> + objectIn<Key>AtIndex:
,要么是countOf<Key> + <key>AtIndexes:
,要么是countOf<Key> + objectIn<Key>AtIndex: + <key>AtIndexes:
) - 如果沒有找到,跳轉到第3步
- 如果找到其中的第一個(
-
③.查找名為
countOf<Key>
、enumeratorOf<Key>
和memberOf<Key>
這三個方法(對應于NSSet
類定義的原始方法)- 如果找到這三個方法,則創建一個響應所有NSSet方法的代理集合對象,并返回該對象
- 如果沒有找到,跳轉到第4步
-
④.判斷
accessInstanceVariablesDirectly
- 為YES時按照
_<key>
、_is<Key>
、<key>
、is<Key>
的順序查找成員變量,找到了就取值 - 為NO時跳轉第6步
- 為YES時按照
-
⑤.判斷取出的屬性值
- 屬性值是對象,直接返回
- 屬性值不是對象,但是可以轉化為
NSNumber
類型,則將屬性值轉化為NSNumber
類型返回 - 屬性值不是對象,也不能轉化為
NSNumber
類型,則將屬性值轉化為NSValue
類型返回
-
⑥.調用
valueForUndefinedKey:
.默認情況下會引發一個異常,但是繼承于NSObject
的子類可以重寫該方法就可以避免崩潰并做出相應措施
四、自定義KVC
根據
KVC
的設值過程、取值過程,我們可以自定義KVC
的setter
方法和getter
方法,但是這一切都是根據官方文檔做出的規則,自定義KVC
只能在一定程度上取代系統KVC
,大致流程幾乎一致:實現了setValue:forUndefinedKey:
、valueForUndefinedKey:
的調用,且accessInstanceVariablesDirectly
無論為true
為false
,都能保持兩次調用
新建一個NSObject+TCJKVC
的分類,.h
開放兩個方法,.m
引入<objc/runtime.h>
-
- (void)cj_setValue:(nullable id)value forKey:(NSString *)key
; -
- (nullable id)cj_valueForKey:(NSString *)key
;
①.自定義setter方法
1.非空判斷
if (key == nil || key.length == 0) return;
2.找到相關方法set<Key>
、_set<Key>
、setIs<Key>
,若存在就直接調用
NSString *Key = key.capitalizedString;
NSString *setKey = [NSString stringWithFormat:@"set%@:",Key];
NSString *_setKey = [NSString stringWithFormat:@"_set%@:",Key];
NSString *setIsKey = [NSString stringWithFormat:@"setIs%@:",Key];
if ([self cj_performSelectorWithMethodName:setKey value:value]) {
NSLog(@"*********%@**********",setKey);
return;
} else if ([self cj_performSelectorWithMethodName:_setKey value:value]) {
NSLog(@"*********%@**********",_setKey);
return;
} else if ([self cj_performSelectorWithMethodName:setIsKey value:value]) {
NSLog(@"*********%@**********",setIsKey);
return;
}
3.判斷是否能夠直接賦值實例變量,不能的情況下就調用setValue:forUndefinedKey:
或拋出異常
NSString *undefinedMethodName = @"setValue:forUndefinedKey:";
IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));
if (![self.class accessInstanceVariablesDirectly]) {
if (undefinedIMP) {
[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
} else {
@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}
return;
}
4.找相關實例變量進行賦值
NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
object_setIvar(self , ivar, value);
return;
} else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
} else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
object_setIvar(self , ivar, value);
return;
} else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
object_setIvar(self , ivar, value);
return;
}
5.調用setValue:forUndefinedKey:或拋出異常
if (undefinedIMP) {
[self cj_performSelectorWithMethodName:undefinedMethodName value:value key:key];
} else {
@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}
在這里筆者存在一個疑問:沒有實現setValue:forUndefinedKey:時,當前類可以響應respondsToSelector這個方法,但是直接performSelector會崩潰,所以改用了判斷IMP是否為空
②.自定義getter方法
1.非空判斷
if (key == nil || key.length == 0) return nil;
2.找相關方法get<Key>
、<key>
,找到就返回(這里使用-Warc-performSelector-leaks
消除警告)
NSString *Key = key.capitalizedString;
NSString *getKey = [NSString stringWithFormat:@"get%@",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(getKey)]) {
return [self performSelector:NSSelectorFromString(getKey)];
} else if ([self respondsToSelector:NSSelectorFromString(key)]) {
return [self performSelector:NSSelectorFromString(key)];
}
#pragma clang diagnostic pop
3.對NSArray
進行操作:查找countOf<Key>
、objectIn<Key>AtIndex
方法
NSString *countOfKey = [NSString stringWithFormat:@"countOf%@",Key];
NSString *objectInKeyAtIndex = [NSString stringWithFormat:@"objectIn%@AtIndex:",Key];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if ([self respondsToSelector:NSSelectorFromString(countOfKey)]) {
if ([self respondsToSelector:NSSelectorFromString(objectInKeyAtIndex)]) {
int num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1];
for (int i = 0; i<num-1; i++) {
num = (int)[self performSelector:NSSelectorFromString(countOfKey)];
}
for (int j = 0; j<num; j++) {
id objc = [self performSelector:NSSelectorFromString(objectInKeyAtIndex) withObject:@(num)];
[mArray addObject:objc];
}
return mArray;
}
}
#pragma clang diagnostic pop
4.判斷是否能夠直接賦值實例變量,不能的情況下就調用valueForUndefinedKey:
或拋出異常
NSString *undefinedMethodName = @"valueForUndefinedKey:";
IMP undefinedIMP = class_getMethodImplementation([self class], NSSelectorFromString(undefinedMethodName));
if (![self.class accessInstanceVariablesDirectly]) {
if (undefinedIMP) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop
} else {
@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}
}
5.找相關實例變量,找到了就返回
NSMutableArray *mArray = [self getIvarListName];
NSString *_key = [NSString stringWithFormat:@"_%@",key];
NSString *_isKey = [NSString stringWithFormat:@"_is%@",Key];
NSString *isKey = [NSString stringWithFormat:@"is%@",Key];
if ([mArray containsObject:_key]) {
Ivar ivar = class_getInstanceVariable([self class], _key.UTF8String);
return object_getIvar(self, ivar);;
} else if ([mArray containsObject:_isKey]) {
Ivar ivar = class_getInstanceVariable([self class], _isKey.UTF8String);
return object_getIvar(self, ivar);;
} else if ([mArray containsObject:key]) {
Ivar ivar = class_getInstanceVariable([self class], key.UTF8String);
return object_getIvar(self, ivar);;
} else if ([mArray containsObject:isKey]) {
Ivar ivar = class_getInstanceVariable([self class], isKey.UTF8String);
return object_getIvar(self, ivar);;
}
6.調用valueForUndefinedKey:
或拋出異常
if (undefinedIMP) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
return [self performSelector:NSSelectorFromString(undefinedMethodName) withObject:key];
#pragma clang diagnostic pop
} else {
@throw [NSException exceptionWithName:@"TCJUnknownKeyException" reason:[NSString stringWithFormat:@"****[%@ %@]: this class is not key value coding-compliant for the key %@.", self, NSStringFromSelector(_cmd), key] userInfo:nil];
}
③.封裝的方法
這里簡單封裝了幾個用到的方法
-
cj_performSelectorWithMethodName:value:key:
安全調用方法及傳兩個參數- (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName value:(id)value key:(id)key { if ([self respondsToSelector:NSSelectorFromString(methodName)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self performSelector:NSSelectorFromString(methodName) withObject:value withObject:key]; #pragma clang diagnostic pop return YES; } return NO; }
-
cj_performSelectorWithMethodName:key:
安全調用方法及傳參- (BOOL)cj_performSelectorWithMethodName:(NSString *)methodName key:(id)key { if ([self respondsToSelector:NSSelectorFromString(methodName)]) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [self performSelector:NSSelectorFromString(methodName) withObject:key]; #pragma clang diagnostic pop return YES; } return NO; }
-
getIvarListName
取成員變量- (NSMutableArray *)getIvarListName { NSMutableArray *mArray = [NSMutableArray arrayWithCapacity:1]; unsigned int count = 0; Ivar *ivars = class_copyIvarList([self class], &count); for (int i = 0; i<count; i++) { Ivar ivar = ivars[i]; const char *ivarNameChar = ivar_getName(ivar); NSString *ivarName = [NSString stringWithUTF8String:ivarNameChar]; NSLog(@"ivarName == %@",ivarName); [mArray addObject:ivarName]; } free(ivars); return mArray; }
KVC中還有一些異常小技巧,在前文中已經提及過,這里再總結一下
五、KVC異常小技巧
①.技巧一 —— 自動轉換類型
-
用
int
類型賦值會自動轉成__NSCFNumber
[person setValue:@18 forKey:@"age"]; [person setValue:@"20" forKey:@"age"]; NSLog(@"%@-%@", [person valueForKey:@"age"], [[person valueForKey:@"age"] class]);
-
用結構體類型賦值會自動轉成
NSConcreteValue
ThreeFloats floats = {1.0, 2.0, 3.0}; NSValue *value = [NSValue valueWithBytes:&floats objCType:@encode(ThreeFloats)]; [person setValue:value forKey:@"threeFloats"]; NSLog(@"%@-%@", [person valueForKey:@"threeFloats"], [[person valueForKey:@"threeFloats"] class]);
②.技巧二 —— 設置空值
有時候在設值時設置空值,可以通過重寫setNilValueForKey
來監聽,但是以下代碼只有打印一次
// Int類型設置nil
[person setValue:nil forKey:@"age"];
// NSString類型設置nil
[person setValue:nil forKey:@"subject"];
@implementation TCJPerson
- (void)setNilValueForKey:(NSString *)key {
NSLog(@"設置 %@ 是空值", key);
}
@end
這是因為setNilValueForKey
只對NSNumber
類型有效
③.技巧三 —— 未定義的key
對于未定義的key我們可以通過重寫setValue:forUndefinedKey:
、valueForUndefinedKey:
來監聽
@implementation TCJPerson
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
NSLog(@"未定義的key——%@",key);
}
- (id)valueForUndefinedKey:(NSString *)key {
NSLog(@"未定義的key——%@",key);
return @"未定義的key";
}
@end
④.技巧四——鍵值驗證
一個比較雞肋的功能——鍵值驗證,可以自行展開做重定向
NSError *error;
NSString *name = @"TCJ";
if (![person validateValue:&name forKey:@"names" error:&error]) {
NSLog(@"%@",error);
}else{
NSLog(@"%@", [person valueForKey:@"name"]);
}
@implementation TCJPerson
- (BOOL)validateValue:(inout id _Nullable __autoreleasing *)ioValue forKey:(NSString *)inKey error:(out NSError *__autoreleasing _Nullable *)outError {
if([inKey isEqualToString:@"name"]){
[self setValue:[NSString stringWithFormat:@"里面修改一下: %@",*ioValue] forKey:inKey];
return YES;
}
*outError = [[NSError alloc]initWithDomain:[NSString stringWithFormat:@"%@ 不是 %@ 的屬性",inKey,self] code:10088 userInfo:nil];
return NO;
}
@end
寫在后面
和諧學習,不急不躁.我還是我,顏色不一樣的煙火.