如何在Objective-C中将只读属性转换为可读写属性?

9
在Objective-C中,如何将只读属性转换为可读写属性?请注意,我无法访问源代码。
原因:我需要在单元测试中模拟这种情况。

这可能看起来像是一个愚蠢的问题,但为什么你需要改变属性呢?不应该有其他动作来驱动属性的状态吗? - Peter M
在我的测试中,我想覆盖一个只读属性,但我无法访问该类的源代码,以便测试不同的情况。 - Boon
好的,那几乎听起来像是你在嘲笑你没有代码的类。 - Peter M
3个回答

10

如果你没有访问类的主要实现块,或者至少包含它的编译单元,就不能像这样改变属性的功能,因为你无法访问该单元外的 ivar。即使你在分类中添加了一个 setter,你也无法影响类的存储,就像你从类的完全外部一样。

然而,你可以使用 KVC。 setValue:forKey: 将绕过 setter 直接访问 ivar (如果可以找到的话)。只要知道名称,你就可以设置任何想要的值,即使属性被声明为 readonly

具体方法如下:

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

@interface Passaquisset : NSObject

@property (copy, readonly, nonatomic) NSString * vanadium;

@end

//Passaquisset.m
#import "Passaquisset.h"

@implementation Passaquisset

@synthesize vanadium;

- (id) init {

    self = [super init];
    if( !self ) return nil;

    vanadium = @"Number 23";

    return self;
}

@end

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

#import "Passaquisset.h"

int main(int argc, const char * argv[])
{

    @autoreleasepool {

        Passaquisset * pq = [Passaquisset new];
        NSLog(@"%@", [pq vanadium]);

        [pq setValue:@"Number 24" forKey:@"vanadium"];

        NSLog(@"%@", [pq vanadium]);

    }
    return 0;
}

就像我说的那样,如果没有同名的setter或ivar(或者有一个附加了下划线_vanadium的ivar),这将失败--实际上会引发异常(KVC非常智能),例如如果属性的值完全是计算得出的:

//Passaquisset.m

#import "Passaquisset.h"

@implementation Passaquisset

/** KVC will fail with this version! **/

- (NSString *)vanadium
{
    return @"Number 23";
}

@end

为了完整起见,让我提一下,如果属性由完全不同名称的ivar支持(例如,@synthesize vanadium = erythronium;),您需要知道ivar的名称才能使用KVC。


很好的回答,猜测中的KVC智能确实很有用! - Gabriele Petronella
喜欢这个解决方案的简洁性。 - Boon

3

您不能简单地将属性设为readwrite,希望访问setter,因为setter本身尚未被合成,因此根本不存在。

您可以考虑的做法是猜测ivar的名称,并在运行时添加setter。

假设您的属性名为foo,并且具有copy属性。

  1. Guess that the name of the ivar. Let's try with _foo.

  2. Prepare a setter

    void fooSetter(id self, SEL _cmd, id newFoo) {
        Ivar ivar = class_getInstanceVariable(self, "_foo");
        id oldFoo = object_getIvar(self, ivar);
        if (oldFoo != newFoo)
             object_setIvar(self, ivar, [newFoo copy]);
    }
    
  3. Add the setter to the class in the resolveInstanceMethod: class method

    + (BOOL) resolveInstanceMethod:(SEL)aSEL {
        if (aSEL == @selector(setFoo:)) {
          class_addMethod(self, @selector(setFoo:), (IMP)fooSetter, "v@:@");
          return YES;
        }
        return [super resolveInstanceMethod:aSel];
    }      
    

目前你已经在运行时添加了setFoo:方法到你的类中,因此你可以通过以下方式访问它

YourClass yourObject = ...;
[yourObject setFoo:whatever];

好主意,做得很好!但是 KVC 将为您完成 ivar 名称猜测和所有其他工作。 (请参见我的答案 ;)。) - jscs
我明白了,这也是个好主意。我不知道 KVC 的“猜测能力”。我认为这两种方法的主要区别在于添加 setter 有点像模拟一个“读写”属性,而 KVC 只会以更直接的方式完成工作。此时,这真的取决于 OP 的需求 ;) - Gabriele Petronella
1
顺便说一下 - 我本来想早点说的 - resolveInstanceMethod: 可能是调用 class_addMethod() 的更好选择位置。这基本上就是它记录的目的。 - jscs
我在考虑交换和下意识地将其标记为那个。 :) - Boon
虽然我猜“clobbering”也意味着您无法访问原始内容... @Boon - jscs
显示剩余3条评论

2

我认为覆盖属性即重新声明它将起作用

在您的.h文件中:

@property (readonly, copy) NSString *yourProperty;
In your .m file:

@interface MyClass ()

// Redeclare property as readwrite
@property (readwrite, copy) NSString *yourProperty;

@end


@implementation MyClass

@synthesize yourProperty;
@end

或者

我没有进行测试,但我认为你需要尝试以下步骤:

[youReadOnlyrProperty retain]

该属性来自于苹果框架中的一个类。类扩展是否可行? - Boon
尝试过类扩展,但是如果没有源代码,它是行不通的。 - Boon
这段代码来自于NSView:“@property(readonly) NSInteger tag;”,然而苹果声称“子类可以重写此属性以为视图提供单独的标记,可能会重新定义该属性为readwrite,以便更轻松地修改它。”但我仍然没有看到简单的方法来实现这一点。 - zzyzy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接