【问题标题】:is there a way to have type-safe constants in Objective-C?有没有办法在 Objective-C 中使用类型安全的常量?
【发布时间】:2015-06-15 19:40:43
【问题描述】:

我正在查看一个充满

的代码库
NSString *const kTabChart = @"Charts";
NSString *const kTabNews = @"News";

然后

setSelectedTab:(NSString *)title;
...
someThingElse:(NSString *)title;

所以这些弱类型的 NSString 绕着代码走得很远,这让我很恼火。枚举在某种程度上会更好,但枚举不会以编程方式提供名称,我不想在同一个枚举中定义来自不同视图的所有不相关的选项卡名称{}

不知道有没有更好的办法?我梦想有一种方法让它像这样

@interface PageTitle:NSSting;
PageTitle kTabChart = /some kind of initializer with @"Chart"/;
PageTitle kTabNews = /some kind of initializer with @"News"/;

我怀疑这与整个“不是编译时常量”约束不能很好地配合,但我想知道是否有技巧/模式/黑客来定义我自己的类类型的常量。

【问题讨论】:

  • 在你的 PageTitle 中添加类方法,返回你的常量。顺便说一句。你不能继承 NSString。
  • 可以子类化NSString,但通常是一个糟糕的选择,完成并不容易。
  • 这就是枚举的真正用途。而且那些用于 UI 的字符串也不属于那里。

标签: objective-c oop enums constants


【解决方案1】:

您应该使用 NS_TYPED_ENUM,因为这会使其成为 rawVlaue。更多信息https://developer.apple.com/documentation/swift/objective-c_and_c_code_customization/grouping_related_objective-c_constants

【讨论】:

    【解决方案2】:

    当然,只要考虑子类化。首先是我们的类,它是NSString的子类:

    @interface StringConstants : NSString
    
    extern StringConstants * const kOptionApple;
    extern StringConstants * const kOptionBlackberry;
    
    @end
    

    所以我们为它定义了StringConstants 和几个全局常量。要在没有任何警告的情况下实现该类,只需要进行一些转换:

    @implementation StringConstants
    
    StringConstants * const kOptionApple = (StringConstants *)@"Apple";
    StringConstants * const kOptionBlackberry = (StringConstants *)@"Blackberry";
    
    @end
    

    还有我们的一组常量。让我们测试一下:

    - (void) printMe:(StringConstants *)string
    {
       NSLog(@"string: %@", string);
    }
    
    - (void) test
    {
       [self printMe:kOptionApple]; // Code completion offers the constants
       [self printMe:@"Rhubarb"];   // Warning: Incompatible pointer types
       [self printMe:(StringConstants *)@"Custard"]; // OK
    }
    

    您只会收到警告,代码将运行,与其他类似类型的错误一样。

    您当然可以重复该模式并为每组字符串生成一个“类”。

    HTH


    附录:安全(现在相信我)和弱枚举

    cmets 提出了有关上述代码本质上是危险的担忧,它不是。然而,在一般情况下,提出的问题是有效的,这里是安全的设计

    注意:这是直接输入到 SO 中的。请原谅不可避免的拼写和语法错误,以及可能缺乏良好的呈现、明确的解释弧、丢失位、冗余位等。

    首先让我们将缺少的 cmets 添加到上面的代码中,从实现开始:

    // The following *downcasts* strings to be StringConstants, code that
    // does this should only appear in this implementation file. Use in
    // other circumstances would effectively increase the number of "enum"
    // values in the set, which rather defeats the purpose of this class!
    //
    // In general downcasting should only be performed after type checks to
    // make sure it is safe. In this particular case *by design* it is safe.
    
    StringConstants * const kOptionApple = (StringConstants *)@"Apple";
    

    这里有两个不同的问题

    1. 它是否安全 - 是的设计,相信我(现在);和
    2. 添加了额外的“枚举”值

    第二个被测试代码中第二个缺失的注释覆盖:

    [self printMe:(StringConstants *)@"Custard"]; // OK :-( - go ahead, graft
                                                  // in a new value and shoot
                                                  // yourself in the foot if
                                                  // you must ;-)
    

    弱者enum

    首先处理第二个问题,不出所料,这个“枚举”并不是万无一失的——您可以随时添加其他值。为什么不出所料?因为您也可以在 (Objective-)C 中做到这一点,所以不仅语言不是强类型,enum 类型也是最弱的。考虑:

    typedef enum { kApple, kBlackberry } PieOptions;
    

    PieOptions 的有效值有多少?使用 Xcode/Clang: 2^32, not 2. 以下是完全有效的:

    PieOptions po = (PieOptions)42;
    

    虽然您不应该编写如此明显错误的代码,但在整数和 enum 值之间进行转换的需求很常见 - 例如在 UI 控件的标记字段中存储“枚举”值时 - 因此存在错误空间。 C 风格的枚举必须与 discipline 一起使用,这样使用有助于提高程序的正确性和可读性。

    同样,StringConstants 必须与规则一起使用,不能强制转换任意字符串 - 相当于上面的 42 个示例 - 并且规则与标准枚举具有相似的优点和缺点。

    遵循不将任意字符串转换为StringConstants 的简单规则; StringConstants 实现本身only允许的东西;这种类型为您提供了一个完全安全的“字符串值枚举”,如果使用不当,则会出现编译时警告。

    如果你相信我,你现在可以停止阅读......


    附录:深入挖掘(只是好奇,否则我们不信任你)

    要理解为什么StringConstants 是完全安全的(即使添加附加值也不是真的不安全,尽管它当然可能会导致程序逻辑失败),我们将讨论一些关于面向对象本质的问题编程,动态类型和Objective-C。下面的一些内容对于理解为什么StringConstants 是安全的并不是绝对必要的,但你是一个有好奇心的人,不是吗?

    对象引用转换在运行时不做任何事情

    从一个对象引用类型到另一个对象引用类型的 cast 是一个编译时语句,该引用应被视为目标类型的对象。它在运行时对实际引用的对象没有影响 - 该对象具有实际类型并且它不会改变。在面向对象的模型中向上转换,从一个类到它的超类之一,总是安全的,向下转换,在相反的方向,可能不 (not is not) 是安全的。出于这个原因,在可能不安全的情况下,向下转换应该受到测试的保护。例如给出:

    NSArray *one = @[ @{ @"this": @"is", @"a" : @"dictionary" } ];
    

    代码:

    NSUInteger len = [one.firstObject length]; // error, meant count, but NO compiler help at all -> runtime error
    

    将在运行时失败。 firstObject 的结果类型是id,这意味着任何对象类型,编译器将允许您在类型为id 的引用上调用任何方法。这里的错误是不检查数组边界并且检索到的引用实际上是一个字典。更防弹的方法是:

    if (one.count > 0)
    {
       id first = one.firstObject;
       if ([first isKindOfClass:[NSDictionary class]])
       {
          NSDictionary *firstDict = first; // *downcast* to improve compile time checking
          NSLog(@"The count of the first item is %lu", firstDict.count);
       }
       else
          NSLog(@"The first item is not a dictionary");
    }
    else
       NSLog(@"The array his empty");
    

    (不可见的)演员表非常安全,受到isKindOf: 测试的保护。不小心在上面的代码片段中输入了firstDict.length,你得到一个编译时错误。

    但是,只有在向下转换可能无效时,您才需要这样做,如果它不可能是无效的,则不需要测试。

    为什么你可以在引用类型上调用任何方法为id

    这就是 Objective-C 的动态运行时消息查找发挥作用的地方。 编译器 在编译时尽其所能检查类型错误。然后在运行时进行另一项检查——被引用的对象是否支持被调用的方法?如果它没有生成 runtime 错误 - 就像上面的 length 示例一样。当对象引用键入为id 时,这是一条指示编译器根本不执行编译时检查并将其全部留给运行时检查的指令。

    运行时检查不是检查被引用对象的类型,而是检查它是否支持请求的方法,这导致我们...

    鸭子,NSProxy,继承等。人。

    鸭子?!

    在动态类型中有一句话:

    如果它长得像鸭子,游泳像鸭子,叫起来像鸭子,那么它就是鸭子。

    在 Objective-C 术语中,这意味着在运行时,如果引用的对象支持 A 类型的方法集,那么它实际上是 A 类型的对象,而不管它的实际类型是什么。

    Objective-C 中很多地方都用到了这个特性,一个值得注意的例子是NSProxy

    NSProxy 是一个抽象超类,它为充当其他对象或尚不存在的对象的替代对象的对象定义了一个 API。通常,发往代理的消息被转发到真实对象或使代理加载(或将其自身转换为)真实对象。 NSProxy 的子类可用于实现透明的分布式消息传递(例如,NSDistantObject)或用于创建成本高昂的对象的延迟实例化。

    有了NSProxy,你可能会认为你有一个NSDictionary——它像字典一样“看起来、游泳和嘎嘎”——但实际上你根本没有。重点是:

    1. 没关系;和
    2. 绝对安全(如果是模数编码错误)

    您可以将这种将一个对象替换为另一个对象的能力视为继承的概括 - 后者您始终可以使用子类实例代替超类实例,前者您可以使用任何对象代替另一个对象只要它“看起来、游泳和嘎嘎叫”就像它所代表的物体。

    我们实际上已经走得比我们需要的更远,鸭子们并不真正需要理解StringConstants,所以让我们继续吧:

    什么时候字符串是NSString的实例?

    可能从不...

    NSStringclass cluster 实现 - 一个类的集合,它们都响应 NSString 所做的同一组方法,即它们都像 NSString 一样嘎嘎作响。现在这些类可能NSString 的子类,但在Objective-C 中实际上不需要它们。

    此外,如果您认为您有一个 NSString 的实例,那么您实际上可能有一个 NSProxy 的实例... 但这没关系。(它可能会影响性能,但它不会影响安全性或正确性。)

    StringConstantsNSString 的子类,所以它肯定是NSString,除了NSString 实例可能不存在 - 每个字符串实际上都是集群中某个其他类的实例,它本身可能是也可能不是NSString 的子类。 不过没关系!

    只要StringConstants quack 的实例像NSStrings 那样 NSStrings - 我们在实现中定义的所有实例都这样做字符串(某种类型,可能是__NSCFConstantString)。

    给我们留下的问题是StringConstants 常量声音的定义?与以下问题相同:

    什么时候知道向下转换总是安全的?

    先举例说明什么时候不是:

    如果您有一个类型为 NSDictionary * 的引用,则 不知道是否安全 将其转换为 NSMutableDictionary * 首先测试引用是否为可变字典。

    编译器将始终让您进行强制转换,然后您可以在编译时调用变异方法而不会出错,但在运行时会出现错误。在这种情况下,您必须在投射前进行测试。

    请注意,由于所有这些鸭子,标准测试 isKindOf: 实际上是保守的。您实际上可能有一个对象的引用,该对象像 NSMutableDictionary 一样嘎嘎作响,但不是它的实例 - 所以强制转换是安全的,但测试会失败。

    一般来说,是什么让这种强制转换不安全?

    简单,不知道引用对象是否响应NSMutableDictionary所做的方法...

    所以,如果您确实知道引用必须响应您要转换的类型的所有方法,那么转换是总是安全的 em> 并且不需要测试。

    那么你如何知道引用必须响应目标类型的所有方法?

    一种情况很简单:如果您有一个类型为 T 的引用,您可以安全地引用类型为 S 的引用,而无需任何检查,只要满足以下条件:

    1. ST 的子类 - 所以它像 T 一样嘎嘎叫;
    2. S 不向T 添加实例状态(变量);
    3. S 不向 T 添加实例行为(新方法、覆盖等);
    4. S 不会覆盖任何类行为

    S 可以在不违反这些要求的情况下添加新的类方法(而不是覆盖)和全局变量/常量。

    换句话说S被定义为:

    @interface S : T
    
    // zero or more new class methods
    
    // zero or more global variables or constants
    
    @end
    
    @implementation S
    
    // implementation of any added class methods, etc.
    
    @end
    

    我们做到了……

    或者我们,还有人在读吗?

    1. StringConstants设计构造的,因此可以将字符串实例强制转换为它。这应该在实现中完成,在其他地方潜入额外的“枚举”常量违背了这个类的目的。
    2. 它是安全,实际上它甚至不可怕 :-)
    3. 从未创建过StringConstants 的实际实例,每个常量都是某个字符串类的实例,在编译时安全地伪装成StringConstants 实例。
    4. 它提供编译时检查字符串常量是否来自预先确定的一组值,它实际上是一个“字符串值枚举”。

    另一个附录:执行纪律

    您不能完全自动执行在 Objective-C 中安全编码所需的规则。

    特别是您不能让编译器阻止程序员将任意整数值转换为enum 类型。事实上,由于 UI 控件的标签字段等用途,在某些情况下需要允许此类转换 - 它们不能完全禁止。

    StringConstants 的情况下,我们不能让编译器在任何地方阻止字符串转换,除了在类本身的实现中,就像enum 可以嫁接额外的“枚举”文字。这条规则需要纪律。

    但是,如果缺乏纪律,编译器可以帮助阻止除强制转换之外的所有方法,这些方法可用于创建 NSString 值,因此 StringConstant 值是一个子类。换句话说,所有initXstringX 等变体都可以在StringConstant 上标记为不可用。只需在@interface 中列出它们并添加NS_UNAVAILABLE

    您不需要 这样做,并且上面的答案不需要,但是如果您需要对您的学科的帮助,您可以在下面添加声明 - 此列表是通过简单地复制从NSString.h 以及快速搜索和替换。

    + (instancetype) new NS_UNAVAILABLE;
    + (instancetype) alloc NS_UNAVAILABLE;
    + (instancetype) allocWithZone:(NSZone *)zone NS_UNAVAILABLE;
    
    - (instancetype) init NS_UNAVAILABLE;
    
    - (instancetype) copy NS_UNAVAILABLE;
    - (instancetype) copyWithZone:(NSZone *)zone NS_UNAVAILABLE;
    
    - (instancetype)initWithCharactersNoCopy:(unichar *)characters length:(NSUInteger)length freeWhenDone:(BOOL)freeBuffer NS_UNAVAILABLE;
    - (instancetype)initWithCharacters:(const unichar *)characters length:(NSUInteger)length NS_UNAVAILABLE;
    - (instancetype)initWithUTF8String:(const char *)nullTerminatedCString NS_UNAVAILABLE;
    - (instancetype)initWithString:(NSString *)aString NS_UNAVAILABLE;
    - (instancetype)initWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) NS_UNAVAILABLE;
    - (instancetype)initWithFormat:(NSString *)format arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0) NS_UNAVAILABLE;
    - (instancetype)initWithFormat:(NSString *)format locale:(id)locale, ... NS_FORMAT_FUNCTION(1,3) NS_UNAVAILABLE;
    - (instancetype)initWithFormat:(NSString *)format locale:(id)locale arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0) NS_UNAVAILABLE;
    - (instancetype)initWithData:(NSData *)data encoding:(NSStringEncoding)encoding NS_UNAVAILABLE;
    - (instancetype)initWithBytes:(const void *)bytes length:(NSUInteger)len encoding:(NSStringEncoding)encoding NS_UNAVAILABLE;
    - (instancetype)initWithBytesNoCopy:(void *)bytes length:(NSUInteger)len encoding:(NSStringEncoding)encoding freeWhenDone:(BOOL)freeBuffer NS_UNAVAILABLE;
    
    + (instancetype)string NS_UNAVAILABLE;
    + (instancetype)stringWithString:(NSString *)string NS_UNAVAILABLE;
    + (instancetype)stringWithCharacters:(const unichar *)characters length:(NSUInteger)length NS_UNAVAILABLE;
    + (instancetype)stringWithUTF8String:(const char *)nullTerminatedCString NS_UNAVAILABLE;
    + (instancetype)stringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) NS_UNAVAILABLE;
    + (instancetype)localizedStringWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2) NS_UNAVAILABLE;
    
    - (instancetype)initWithCString:(const char *)nullTerminatedCString encoding:(NSStringEncoding)encoding NS_UNAVAILABLE;
    + (instancetype)stringWithCString:(const char *)cString encoding:(NSStringEncoding)enc NS_UNAVAILABLE;
    
    - (instancetype)initWithContentsOfURL:(NSURL *)url encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
    - (instancetype)initWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
    + (instancetype)stringWithContentsOfURL:(NSURL *)url encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
    + (instancetype)stringWithContentsOfFile:(NSString *)path encoding:(NSStringEncoding)enc error:(NSError **)error NS_UNAVAILABLE;
    
    - (instancetype)initWithContentsOfURL:(NSURL *)url usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;
    - (instancetype)initWithContentsOfFile:(NSString *)path usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;
    + (instancetype)stringWithContentsOfURL:(NSURL *)url usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;
    + (instancetype)stringWithContentsOfFile:(NSString *)path usedEncoding:(NSStringEncoding *)enc error:(NSError **)error NS_UNAVAILABLE;
    

    【讨论】:

    • 嗯...子类化 NSString 是一件困难的事情,即使对于这个“简单”的任务。将 NSString 转换为您的子类并不能使其成为一个子类,因此它的指针现在正在散布关于其内容的谎言,这为未来打开了一大堆蠕虫。此外,NSString 是一个类簇,使得子类化变得更加困难。我认为这种方法虽然是一种“快速修复”,但从长远来看会使事情变得更糟,是对语言的严重滥用,应该被视为带有大红旗的大 NO-NO。
    • @Eiko - 在这种情况下,您不必担心。从来没有任何StringConstant 的实例,它们都只是NSString(或使用类集群中的任何实际类)。问一个它的类型,它不会说StringConstant - 它的指针不会比NSString * 更能说明“谎言”。整个 Objective-C 都是这样工作的(尤其是 id 和类集群)。这给你的是编译时警告,而根本不改变运行时动态类型。
    • @Eiko - 顺便说一句,如果你 真的 想进一步锁定它,请添加 -init+new+alloc -copy 和所有其他 {init, copy,alloc,string}* 方法标记为NS_UNAVAILABLE,因此无法创建实例。 (只需将它们从NSString 中复制出来,添加NS_UNAVAILABLE,然后放在标题中 - 不需要实现。
    • 实际上有很大的不同。每个 'NSString' 实际上都是它的一个实例或子类,但 StringConstant 不是。这是向下转换到错误的类。这绝对不是 NSString、id 或 Obj-C 中的任何其他内容的工作方式。查看字符串常量,它们通常是 __NSCFConstantString->__NSCFString->NSMutableString->NSString 类型。每只狗都是动物,但在这里你说有些动物是猫,尽管它不是。
    • 为什么用@interface@implementation 覆盖常量?没用。
    【解决方案3】:

    你考虑过#define macro吗?

    #define kTabChart @"Charts"
    

    在编译的预处理步骤中,编译器会将所有kTabChart 替换为您想要的常量。

    如果您想要自己的自定义类的常量,那么您必须使用 const,正如用户 @JeremyP 在链接答案中所说的那样

    【讨论】:

    • 宏不是类型安全的,应该使用 rarley 并且通常是一个非常糟糕的选择。
    • 嗯我不知道——我只是认为它是类型安全的,因为编译器不会让你分配给宏。你能给我一个例子来说明你可以在哪里修改宏的基础类型吗?我很难理解它
    • #define 没有类型,它本质上只是一个文本替换。在这种情况下,编译器可以进行类型确定,但通常不会。在示例中#define x 3 x 和 3 可以用作 char、short、long、float 等。NSString *const kTabChart = @"Charts"; kTabChart 直接具有类型。
    • 所以 kTabChart 是类型安全的,但如果你不指定底层类型,一般 #define 不是?
    • 类型安全意味着编译器可以验证类型是否匹配。一般来说,不鼓励使用#define,许多语言并不像 Swift 那样支持它。
    猜你喜欢
    • 2022-01-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-12
    • 2019-12-18
    • 2011-03-17
    • 1970-01-01
    相关资源
    最近更新 更多