【问题标题】:Assigning to a property of type SEL using KVC使用 KVC 分配给 SEL 类型的属性
【发布时间】:2013-08-31 09:40:22
【问题描述】:

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,它只是不喜欢我传入的值。当我定义类型为 void* 而不是 SEL 的属性时,它似乎可以工作,但是这不符合我的要求。

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

谁能给我解释一下

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

【问题讨论】:

    标签: objective-c macos cocoa


    【解决方案1】:

    默认setValue:forKey: 实现似乎只支持某些基本类型的子集进行自动装箱/拆箱。请参阅“键值编码编程指南”的"Scalar and Structure Support" chapter 中的表 1 和表 2。这里的含义是,只有BOOLchardoublefloatintlonglong longshort 和它们的未签名对应物得到完全支持,还有@987654334 @s 通过NSValue。其他类型,如SEL和其他指针值,appear to be unsupported

    考虑以下程序:

    #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"];
        }
    }
    

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

    我们如何从这里开始?例如,我们可以覆盖 MyObject 中的 valueForKey:setValueForKey: 以支持对 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 设置选择器的每个类中实现这些方法,并且您还必须与硬编码的键进行比较。解决此问题的一种方法是有风险的,即使用方法调配,并用我们自己的方法替换 NSObject 的 KVC 方法实现,它可以处理 SEL 类型的装箱和拆箱。

    以下程序以第一个示例为基础,大量源自 Mike Ash 的精彩文章 "let's build KVC",并且还使用了来自 this answer on SOSwizzle() 函数。请注意,我为了演示而偷工减料,并且此代码仅适用于具有适当命名的 getter 和 setter 的 SEL 属性,并且不会直接检查实例变量,这与默认的 KVC 实现不同。

    #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
    

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

    【讨论】:

    • 看来 SEL 根本不兼容装箱/拆箱。 Jeffery Thomas 的回答同样正确,但这种解决方法很有帮助。但是,我仍然想知道是否有可能进行任何类型的真正装箱/拆箱。
    • @BrandonHorst 谢谢。请查看我的编辑 - 如果您有兴趣,我添加了一个更通用的解决方法。
    【解决方案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 [&lt;KVCOnSELTester 0x8a74840&gt; 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 [&lt;KVCOnSELTester 0x8a74840&gt; setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key mySelector..

    这与您之前遇到的错误相同。我认为 SEL 不是 KVC。我已经尝试查找此内容,但无处可寻。

    【讨论】:

      猜你喜欢
      • 2011-12-24
      • 1970-01-01
      • 2013-03-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-22
      • 2020-05-28
      相关资源
      最近更新 更多