Objective-C书写通用的getter和setter方法

4
在我的项目中,我有一个带有自定义setter的settings类,这些setter访问NSUserDefaults使一切更简单。这个想法是Settings类拥有这个属性。
@property NSString *name

该类具有自定义getter,从NSUserDefaults中获取名称值,并具有一个setter,将新值保存在那里。因此,在整个项目中,我只与Settings类交互以管理用户定义的首选项。问题在于,编写所有getter和setter(我大约有50个属性)似乎过于重复,我想创建一个setter和一个getter,可以适用于所有变量。我的唯一问题是如何在setter内获取变量的名称。
最终的问题是:是否可能找出调用函数的属性是哪个?
如果您有其他方法,我也会感激,但考虑到我想将所有NSUserDefaults内容保留在一个类中,我想不出另一种不包括编写50个getter和setter的替代方法。
谢谢!

50个属性?听起来你应该考虑将其分成几个类。 - vikingosegundo
6个回答

4
另一种方法可能是这样的。没有属性,只有键值下标。
@interface DBObject : NSObject<NSCoding>
+ (instancetype)sharedObject;
@end

@interface NSObject(SubScription) 
- (id)objectForKeyedSubscript:(id)key;
- (void)setObject:(id)obj forKeyedSubscript:(id <NSCopying>)key;
@end

在实现文件中:

+ (instancetype)sharedObject {
   static DBObject *sharedObject = nil;
   static dispatch_once_t onceToken;
   dispatch_once(&onceToken, ^{
     sharedObject = [[DBObject alloc] init];
   });
   return sharedObject;
}

- (id)objectForKeyedSubscript:(id)key {
   return [[NSUserDefaults standardUserDefaults] objectForKey:key];
 }

- (void)setObject:(id)obj forKeyedSubscript:(id <NSCopying>)key {
   [[NSUserDefaults standardUserDefaults] setObject:obj forKeyedSubscript:key];
   [[NSUserDefaults standardUserDefaults] synchronize];
 }

现在,你可以像这样使用它:
 // You saved it in NSUserDefaults
[DBObject sharedObject][@"name"] = @"John"; 

// You retrieve it from NSUserDefaults
NSLog(@"Name is: %@", [DBObject sharedObject][@"name"]); 

我认为这是最佳的方法,也是我将来使用的方法。


抱歉 Herald,回复晚了,我现在才注意到你的答案。你说得很正确,这确实可行,但它并没有改进标准的NSUserDefaults太多。我仍然需要获取一个共享对象(像standardDefaults一样),并且仍然需要提供一个不检查拼写的键。最终[[NSUserDefaults standardDefaults] setObject:"John" forKey:"name"]; 与[DBObject sharedObject][@"name"] = @"John"; 没有那么大的区别。它稍微短一些,看起来更好看,所以我同意这是一种改进,只是不值得重写所有代码以获取首选项。 - Denis Balko

3
在这种情况下,setter和getter很简单,你可以像这样做:
- (void)setName:(NSString *)name {
   [[NSUserDefaults standardUserDefaults] setObject:name forKey:@"name"];
   [[NSUserDefaults standardUserDefaults] synchronize];
}

- (NSString *)name {
   return [[NSUserDefaults standardUserDefaults] objectForKey:@"name"];
}

如果你想为所有属性使用简单的方法:
- (id)objectForKey:(NSString *)key {
   return [[NSUserDefaults standardUserDefaults] objectForKey:key];
}
- (void)setObject:(id)object forKey:(NSString *)key {
   [[NSUserDefaults standardUserDefaults] setObject:object forKey:key];
   [[NSUserDefaults standardUserDefaults] synchronize];
}

不要创建许多属性,而是创建许多键,每个键都是您想要保存或检索的内容。 键的示例:

static NSString *const kName = @"name";
static NSString *const kLastName = @"lastName";

你说得对,这个方法可以行得通,而且我之前也有一个和你第一版非常相似的方案。但问题在于,如果你有很多属性,就会有2xmany个函数,所以如果出于任何原因需要更改它,维护起来将会很麻烦。这就是为什么我正在寻找一种通用选项,使一个getter和一个setter适用于所有属性(像上面的人们设法实现的那样)。键确实是另一种选择,但它不如干净,而且由于我的代码量很大,这是一个问题。然而,这绝对是一个可行的替代方法。谢谢! - Denis Balko

2
我发现你的问题非常有趣,我对自己说“接受挑战!”。
我在Github上创建了这个项目:this
基本上,你所要做的就是子类化VBSettings类,然后声明属性,像这样:
@interface MySettings : VBSettings

@property (strong, nonatomic) NSString *hello;

@end

将“hello”值保存到NSUserDefaults中,键名为“hello”。使用示例:

MySettings settings = [[MySettings alloc] init];
settings.hello = "World!"; //The value is saved in NSUserDefaults
NSLog(@"%@", settings.hello); //The value is restored from NSUserDefaults.

谢谢您,这看起来很棒,完全达到了我需要的目的。我只想指出,这对其他人可能非常有用,如果是这样的话,也许您可以添加两个方法:clearAll和registerDefaults:@{}。我肯定打算在子类中实现这两个方法,以便用户注销并将默认值轻松可见地放在AppDelegate中。再次感谢! - Denis Balko
谢谢!感谢您的反馈。我真的很想在不久的将来改进这个项目,包括您提到的那两种方法。 - vbgd
我忘了,“挑战完成!” ;) - Denis Balko

0

一种可能性是使用KVO来检测您的属性何时发生变化。

E.g.:

@interface Settings : NSObject

@property NSString *one;
@property NSString *two;

@end

@implementation Settings

- (instancetype)init
{
    self = [super init];
    if (self) {
        [self addObserver:self forKeyPath:@"one" options:NSKeyValueObservingOptionNew context:NULL];
        [self addObserver:self forKeyPath:@"two" options:NSKeyValueObservingOptionNew context:NULL];
    }
    return self;
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"one"];
    [self removeObserver:self forKeyPath:@"two"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
    NSLog(@"Key: %@, Change: %@", keyPath, change);
}

@end

在另一个类中,使用标准的属性访问方式:
Settings *settings = [[Settings alloc] init];
settings.one = @"something for one";

设置对象记录:

键:one,更改:{ 类型 = 1; 新值 = "one的某些内容"; }


这是绕过问题的聪明方法,尽管我一直在使用KVO方面遇到困难,并且希望尽可能避免它。 - Denis Balko

0

你可以尝试使用动态的Getter和Setter声明如此答案中所述。

首先创建通用函数,你想让所有属性都使用它们:

- (id)_getter_
{
    return [[NSUserDefaults standardUserDefaults] objectForKey:NSStringFromSelector(_cmd)];
}

- (void)_setter_:(id)value 
{
    //This one's _cmd name has "set" in it and an upper case first character
    //This could take a little work to parse out the parameter name
    [[NSUserDefaults standardUserDefaults] setObject:object forKey:YourParsedOutKey];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

然后创建动态方法生成器:

+(void)synthesizeForwarder:(NSString*)getterName
{
    NSString*setterName=[NSString stringWithFormat:@"set%@%@:",
          [[getterName substringToIndex:1] uppercaseString],[getterName substringFromIndex:1]];
    Method getter=class_getInstanceMethod(self, @selector(_getter_));
    class_addMethod(self, NSSelectorFromString(getterName), 
                    method_getImplementation(getter), method_getTypeEncoding(getter));
    Method setter=class_getInstanceMethod(self, @selector(_setter_:));
    class_addMethod(self, NSSelectorFromString(setterName), 
                    method_getImplementation(setter), method_getTypeEncoding(setter));
}

然后设置您想要为其创建动态getter和setter的字符串:

+(void)load  
{
    for(NSString*selectorName in [NSArray arrayWithObjects:@"name", @"anything", @"else", @"you", @"want",nil]){
       [self synthesizeForwarder:selectorName];
    }
}

这将为您添加到数组中的任何变量名称创建getter和setter。我不确定当其他类尝试调用这些方法时,这个方法是否能很好地工作,编译器在编译时不会看到它们,因此在尝试使用它们时可能会抛出错误。我只是将另外两个StackOverflow问题合并成了一个答案,以适应您的情况。

动态获取器和设置器。

获取当前方法名称。


看起来不错,但我担心这两个概念的结合。它肯定有潜力,但必须经过测试。不过,为想到这个点子而赞叹。 - Denis Balko

0
据我理解,您不希望该类的用户承受 setObject:forKey: 和 objectForKey: 方法调用所带来的心智负担。
以下是解决方法。我会留下许多空白供您填补。
  1. 在头文件中声明属性,以便调用者可以使用它:

    @property NSString *something;
    @property NSString *somethingElse;
    
  2. 在类文件本身中声明您正在定义属性,以便编译器不会感到不安:

    @dynamic something,somethingElse;
    
  3. 在类文件中实现methodSignatureForSelector函数,如下所示:

    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {if (SelectorIsGetter(aSelector))
       return [NSMethodSignature signatureWithObjCTypes:"@@:"];
     if (SelectorIsSetter(aSelector))
       return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
     return nil;
     }
    

这将告诉系统对于这些选择器调用forwardInvocation:,并且还会告诉它正在进行的调用的形状。

SelectorIsGetterSelectorIsSetter的实现取决于您。您可能会使用NSStringFromSelector(aSelector)获取选择器的名称,然后在名称表中查找它是否与您正在实现的选择器名称匹配:在这种情况下,getter为somethingsomethingElse,setter为setSomething:setSomethingElse:

  • 在类文件中实现forwardInvocation:函数,如下所示:

    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {if (SelectorIsGetter(anInvocation.selector))
       {NSString *s=[self objectForKey:NSStringFromSelector(anInvocation.selector)];
        [anInvocation setReturnValue:&s];
        return;
        };
     if (SelectorIsSetter(anInvocation.selector))
        {NSString *s;
         [anInvocation getArgument:&s atIndex:2];
         [self setObjectForKey:UnmangleName(NSStringFromSelector(anInvocation.selector))];
         return;
         };
     [super forwardInvocation:anInvocation];
     }
    
  • ...其中UnmangleName是一个非常繁琐的函数,它接受一个类似"setSomething:"的字符串,并将其转换为"something"这样的字符串。

    如果你想做更多的事情而不仅仅是NSStrings,这个扩展相对来说是比较简单的。


    这听起来是一个合理的方法,尽管它显然比vladiulianbogdan提供的子类化类要复杂得多。尽管如此,这是一个好主意,谢谢你,它肯定帮助我获得更广泛的可能实现的想法。 - Denis Balko

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