6

ARC 已启用。

我有一个具有 SEL 类型属性的类:@property SEL mySelector;

它是合成的:@synthesize mySelector;

然后我尝试使用 KVC 为其分配一个值:

SEL someSelector = @selector(doSomething:)
NSValue* someSelectorValue = [NSValue value:someSelector withObjCType:@encode(SEL)];
[target setValue:someSelectorValue forKey:@"mySelector"];

我收到错误消息:

[<LACMyClass 0x101b04bc0> setValue:forUndefinedKey:]:
this class is not key value coding-compliant for the key mySelector.

这显然不是真的 - 该类是 KVC 兼容的,它只是不喜欢我传入的值。当我定义 type 的属性void*而不是 时,它似乎可以工作SEL,但这不能满足我的要求。

除了使用之外value:withObjCType:,我还尝试valueWithBytes:objCType:valueWithPointer:

谁能给我解释

  1. 发生了什么,以及
  2. 如何正确执行此操作?
4

2 回答 2

8

setValue:forKey:默认实现似乎只支持原始类型的某个子集进行自动装箱/拆箱。请参阅“键值编码编程指南”的“标量和结构支持”一章中的表 1 和表 2 。这里的含义是,只有BOOL, char, double, float, int, long, long long, , 和它们的未签名对应物与s viashort一起得到完全支持。其他类型,例如和其他指针值,似乎不受支持structNSValueSEL

考虑以下程序:

#import <Foundation/Foundation.h>

@interface MyObject : NSObject

@property (nonatomic) SEL mySelector;
@property (nonatomic) void *myVoid;
@property (nonatomic) int myInt;
@property (nonatomic,unsafe_unretained) id myObject;

@end

@implementation MyObject
@end

int main(int argc, char *argv[]) {
    @autoreleasepool {
        SEL selector = @selector(description);
        NSValue *selectorValue = [NSValue valueWithPointer:selector];
        NSValue *voidValue = [NSValue valueWithPointer:selector];
        NSValue *intValue = @1;
        __unsafe_unretained id obj = (__bridge id)(const void *)selector;
        MyObject *object = [[MyObject alloc] init];

        // The following two calls succeed:
        [object setValue:intValue forKey:@"myInt"];
        [object setValue:obj forKey:@"myObject"];

        // These two throw an exception:
        [object setValue:voidValue forUndefinedKey:@"myVoid"];
        [object setValue:selectorValue forKey:@"mySelector"];
    }
}

我们可以很好地设置intand属性 - 甚至使用和桥接强制转换来让我们传递选择器值。但是,不支持尝试设置两种指针类型中的任何一种。id__unsafe_unretained

我们如何从这里开始?例如,我们可以覆盖valueForKey:setValueForKey:inMyObject以支持对任一类型进行拆箱SEL或拦截特定键。后一种方法的一个例子:

@implementation MyObject

- (id)valueForKey:(NSString *)key
{
    if ([key isEqualToString:@"mySelector"]) {
        return [NSValue valueWithPointer:self.mySelector];
    }

    return [super valueForKey:key];
}

- (void)setValue:(id)value forKey:(NSString *)key
{
    if ([key isEqualToString:@"mySelector"]) {
        SEL toSet;
        [(NSValue *)value getValue:&toSet];
        self.mySelector = toSet;
    }
    else {
        [super setValue:value forUndefinedKey:key];
    }
}

@end

在使用中,我们发现它按预期工作:

[object setValue:selectorValue forKey:@"mySelector"];
NSString *string = NSStringFromSelector(object.mySelector);
NSLog(@"selector string = %@", string);

这会将“选择器字符串 = 描述”记录到控制台。

当然,这有可维护性的问题,因为您现在必须在需要使用 KVC 设置选择器的每个类中实现这些方法,并且您还必须与硬编码的键进行比较。解决此问题的一种方法是有风险的,即使用方法调配并用我们自己的替换 replaceNSObject的 KVC 方法实现,它确实处理SEL类型的装箱和拆箱。

以下程序基于第一个示例构建,很大程度上源自 Mike Ash 出色的“让我们构建 KVC”文章,并且还使用了SO 上这个答案中Swizzle()的函数。请注意,我出于演示的目的偷工减料,并且此代码仅适用于具有适当命名的 getter 和 setter 的属性,并且不会直接检查实例变量,这与默认的 KVC 实现不同。SEL

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface MyObject : NSObject

@property (nonatomic) SEL mySelector;
@property (nonatomic) int myInt;

@end

@implementation MyObject
@end

@interface NSObject (ShadyCategory)
@end

@implementation NSObject (ShadyCategory)

// Implementations of shadyValueForKey: and shadySetValue:forKey: Adapted from Mike Ash's "Let's Build KVC" article
// http://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
// Original MAObject implementation on github at https://github.com/mikeash/MAObject
- (id)shadyValueForKey:(NSString *)key
{
    SEL getterSEL = NSSelectorFromString(key);
    if ([self respondsToSelector: getterSEL]) {
        NSMethodSignature *sig = [self methodSignatureForSelector: getterSEL];
        char type = [sig methodReturnType][0];
        IMP imp = [self methodForSelector: getterSEL];
        if (type == @encode(SEL)[0]) {
            return [NSValue valueWithPointer:((SEL (*)(id, SEL))imp)(self, getterSEL)];
        }
    }
    // We will have swapped implementations here, so this call's NSObject's valueForKey: method
    return [self shadyValueForKey:key];
}

- (void)shadySetValue:(id)value forKey:(NSString *)key
{
    NSString *capitalizedKey = [[[key substringToIndex:1] uppercaseString] stringByAppendingString:[key substringFromIndex:1]];
    NSString *setterName = [NSString stringWithFormat: @"set%@:", capitalizedKey];
    SEL setterSEL = NSSelectorFromString(setterName);
    if ([self respondsToSelector: setterSEL]) {
        NSMethodSignature *sig = [self methodSignatureForSelector: setterSEL];
        char type = [sig getArgumentTypeAtIndex: 2][0];
        IMP imp = [self methodForSelector: setterSEL];
        if (type == @encode(SEL)[0]) {
            SEL toSet;
            [(NSValue *)value getValue:&toSet];
            ((void (*)(id, SEL, SEL))imp)(self, setterSEL, toSet);
            return;
        }
    }

    [self shadySetValue:value forKey:key];
}

@end

// Copied from: https://stackoverflow.com/a/1638940/475052
void Swizzle(Class c, SEL orig, SEL new)
{
    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);
    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}

int main(int argc, char *argv[]) {
    @autoreleasepool {
        Swizzle([NSObject class], @selector(valueForKey:), @selector(shadyValueForKey:));
        Swizzle([NSObject class], @selector(setValue:forKey:), @selector(shadySetValue:forKey:));

        SEL selector = @selector(description);
        MyObject *object = [[MyObject alloc] init];
        object.mySelector = selector;
        SEL fromProperty = object.mySelector;
        NSString *fromPropertyString = NSStringFromSelector(fromProperty);
        NSValue *fromKVCValue = [object valueForKey:@"mySelector"];
        SEL fromKVC;
        [fromKVCValue getValue:&fromKVC];
        NSString *fromKVCString = NSStringFromSelector(fromKVC);

        NSLog(@"fromProperty = %@ fromKVC = %@", fromPropertyString, fromKVCString);

        object.myInt = 1;
        NSNumber *myIntFromKVCNumber = [object valueForKey:@"myInt"];
        int myIntFromKVC = [myIntFromKVCNumber intValue];
        int myIntFromProperty = object.myInt;
        NSLog(@"int from kvc = %d from propety = %d", myIntFromKVC, myIntFromProperty);

        selector = @selector(class);
        NSValue *selectorValue = [NSValue valueWithPointer:selector];
        [object setValue:selectorValue forKey:@"mySelector"];
        SEL afterSettingWithKVC = object.mySelector;
        NSLog(@"after setting the selector with KVC: %@", NSStringFromSelector(afterSettingWithKVC));

        [object setValue:@42 forKey:@"myInt"];
        int myIntAfterSettingWithKVC = object.myInt;
        NSLog(@"after setting the int with KVC: %d", myIntAfterSettingWithKVC);
    }
}

该程序的输出展示了它的装箱和拆箱功能:

2013-08-30 19:37:14.287 KVCSelector[69452:303] fromProperty = description fromKVC = description
2013-08-30 19:37:14.288 KVCSelector[69452:303] int from kvc = 1 from propety = 1
2013-08-30 19:37:14.289 KVCSelector[69452:303] after setting the selector with KVC: class
2013-08-30 19:37:14.289 KVCSelector[69452:303] after setting the int with KVC: 42

调酒当然不是没有风险的,所以要小心!

于 2013-08-31T00:38:55.037 回答
2

该错误试图告诉您该类不符合密钥 mySelector。

STAssertNoThrow([target setValue:nil forKey:@"mySelector"], nil);

也应该失败,问题在于mySelector不带拳击SEL


我很好奇,所以我只运行了这个测试:

@interface KVCOnSELTester : NSObject
@property (assign, nonatomic) SEL mySelector;
@property (assign, nonatomic) int myInteger;
@end

@implementation KVCOnSELTester
@end

@implementation KVCOnSELTests

- (void)testKVCOnSEL
{
    KVCOnSELTester *target = [KVCOnSELTester new];

    STAssertNoThrow((target.myInteger = 1), nil);
    STAssertNoThrow([target setMyInteger:1], nil);
    STAssertNoThrow([target setValue:nil forKey:@"myInteger"], nil);

    STAssertNoThrow((target.mySelector = @selector(setUp)), nil);
    STAssertNoThrow([target setMySelector:@selector(setUp)], nil);
    STAssertNoThrow([target setValue:nil forKey:@"mySelector"], nil);
}

@end

STAssertNoThrow([target setValue:nil forKey:@"myInteger"], nil)扔了[target setValue:nil forKey:@"myInteger"] raised [<KVCOnSELTester 0x8a74840> setNilValueForKey]: could not set nil as the value for the key myInteger..

这正确地说明了nil不适合myInteger.

STAssertNoThrow([target setValue:nil forKey:@"mySelector"], nil)扔了[target setValue:nil forKey:@"mySelector"] raised [<KVCOnSELTester 0x8a74840> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key mySelector..

这与您之前遇到的错误相同。我认为 SEL 不是 KVC。我试过查这个,但我一无所获。

于 2013-08-30T23:38:43.547 回答