简短的回答是不可能从另一个文件访问静态变量。这与试图从其他地方引用函数局部变量完全相同。该名称不可用。在 C 中,对象* 的“可见性”分为三个阶段,称为“链接”:外部(全局)、内部(仅限于单个“翻译单元”——松散地,单个文件)和“否”(函数本地)。当您将变量声明为static
时,它被赋予内部链接;没有其他文件可以通过名称访问它。您必须制作某种访问器功能才能公开它。
扩展的答案是,因为无论如何我们都可以使用一些 ObjC 运行时库技巧来模拟类级变量,所以我们可以制作一些通用的仅测试代码,您可以有条件地编译。不过,这并不是特别简单。
在我们开始之前,我会注意到这仍然需要一种方法的个性化实现;由于链接的限制,没有办法解决这个问题。
第一步,声明方法,一个用于设置,然后一组用于类似valueForKey:
访问:
// ClassVariablesExposer.h
#if UNIT_TESTING
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#define ASSOC_OBJ_BY_NAME(v) objc_setAssociatedObject(self, #v, v, OBJC_ASSOCIATION_ASSIGN)
// Store POD types by wrapping their address; then the getter can access the
// up-to-date value.
#define ASSOC_BOOL_BY_NAME(b) NSValue * val = [NSValue valueWithPointer:&b];\
objc_setAssociatedObject(self, #b, val, OBJC_ASSOCIATION_RETAIN)
@interface NSObject (ClassVariablesExposer)
+ (void)associateClassVariablesByName;
+ (id)classValueForName:(char *)name;
+ (BOOL)classBOOLForName:(char *)name;
@end
#endif /* UNIT_TESTING */
这些方法在语义上更像是一个协议而不是一个类别。第一个方法必须在每个子类中被覆盖,因为您要关联的变量当然会不同,并且由于链接问题。objc_setAssociatedObject()
对引用变量的位置的实际调用必须在声明变量的文件中。
但是,将此方法放入协议中需要为您的类提供额外的标头,因为虽然协议方法的实现必须放在主实现文件中,但 ARC 和您的单元测试需要查看您的类符合的声明协议。麻烦。您当然可以使该NSObject
类别符合协议,但无论如何您都需要一个存根以避免“不完整的实现”警告。我在开发这个解决方案时做了这些事情,并认为它们是不必要的。
第二组,访问器,作为类别方法工作得很好,因为它们看起来像这样:
// ClassVariablesExposer.m
#import "ClassVariablesExposer.h"
#if UNIT_TESTING
@implementation NSObject (ClassVariablesExposer)
+ (void)associateClassVariablesByName
{
// Stub to prevent warning about incomplete implementation.
}
+ (id)classValueForName:(char *)name
{
return objc_getAssociatedObject(self, name);
}
+ (BOOL)classBOOLForName:(char *)name
{
NSValue * v = [self classValueForName:name];
BOOL * vp = [v pointerValue];
return *vp;
}
@end
#endif /* UNIT_TESTING */
完全通用,尽管它们的成功使用确实取决于您对上面宏的使用。
接下来,定义您的类,覆盖该设置方法以捕获您的类变量:
// Milliner.h
#import <Foundation/Foundation.h>
@interface Milliner : NSObject
// Just for demonstration that the BOOL storage works.
+ (void)flipWaterproof;
@end
// Milliner.m
#import "Milliner.h"
#if UNIT_TESTING
#import "ClassVariablesExposer.h"
#endif /* UNIT_TESTING */
@implementation Milliner
static NSString * featherType;
static BOOL waterproof;
+(void)initialize
{
featherType = @"chicken hawk";
waterproof = YES;
}
// Just for demonstration that the BOOL storage works.
+ (void)flipWaterproof
{
waterproof = !waterproof;
}
#if UNIT_TESTING
+ (void)associateClassVariablesByName
{
ASSOC_OBJ_BY_NAME(featherType);
ASSOC_BOOL_BY_NAME(waterproof);
}
#endif /* UNIT_TESTING */
@end
确保您的单元测试文件导入了该类别的标头。此功能的简单演示:
#import <Foundation/Foundation.h>
#import "Milliner.h"
#import "ClassVariablesExposer.h"
#define BOOLToNSString(b) (b) ? @"YES" : @"NO"
int main(int argc, const char * argv[])
{
@autoreleasepool {
[Milliner associateClassVariablesByName];
NSString * actualFeatherType = [Milliner classValueForName:"featherType"];
NSLog(@"Assert [[Milliner featherType] isEqualToString:@\"chicken hawk\"]: %@", BOOLToNSString([actualFeatherType isEqualToString:@"chicken hawk"]));
// Since we got a pointer to the BOOL, this does track its value.
NSLog(@"%@", BOOLToNSString([Milliner classBOOLForName:"waterproof"]));
[Milliner flipWaterproof];
NSLog(@"%@", BOOLToNSString([Milliner classBOOLForName:"waterproof"]));
}
return 0;
}
我已将项目放在 GitHub 上:https ://github.com/woolsweater/ExposingClassVariablesForTesting
另一个需要注意的是,您希望能够访问的每种 POD 类型都需要自己的方法:classIntForName:
、classCharForName:
等。
虽然这很有效,而且我总是喜欢玩 ObjC,但我认为它可能太聪明了一半;如果你只有一个或两个这些类变量,最简单的建议就是为它们有条件地编译访问器(制作一个 Xcode 代码片段)。如果您在一个类中有很多变量,我的代码可能只会为您节省时间和精力。
不过,也许你可以从中得到一些用处。我希望这是一个有趣的阅读,至少。
*仅表示“链接器已知的事物”——函数、变量、结构等——不是 ObjC 或 C++ 意义上的。