使用反射来确定属性类型或类

15

我想知道是否有可能确定对象属性的类或原始类型。获取所有属性名称和值非常容易。SO答案

那么,在属性没有值或为空值的情况下,有没有办法获取属性的类类型呢?

示例代码

@interface MyObject : NSObject
@property (nonatomic, copy) NSString *aString;
@property (nonatomic, copy) NSDate *aDate;
@property                   NSInteger aPrimitive;
@end

@implementation MyObject
@synthesize aString;
@synthesize aDate;
@synthesize aPrimitive;

- (void)getTheTypesOfMyProperties {
    unsigned int count;
    objc_property_t* props = class_copyPropertyList([self class], &count);
    for (int i = 0; i < count; i++) {
        objc_property_t property = props[i];

        // Here I can easy get the name or value
        const char * name = property_getName(property);

        // But is there any magic function that can tell me the type?
        // the property can be nil at this time
        Class cls = magicFunction(property);
    }
    free(props);
}

@end
2个回答

35

在搜索了苹果关于Objective-C Runtime的文档并参考这个SO答案后,我终于让它工作了。我只是想分享我的结果。

unsigned int count;
objc_property_t* props = class_copyPropertyList([MyObject class], &count);
for (int i = 0; i < count; i++) {
    objc_property_t property = props[i];
    const char * name = property_getName(property);
    NSString *propertyName = [NSString stringWithCString:name encoding:NSUTF8StringEncoding];
    const char * type = property_getAttributes(property);
    NSString *attr = [NSString stringWithCString:type encoding:NSUTF8StringEncoding];

    NSString * typeString = [NSString stringWithUTF8String:type];
    NSArray * attributes = [typeString componentsSeparatedByString:@","];
    NSString * typeAttribute = [attributes objectAtIndex:0];
    NSString * propertyType = [typeAttribute substringFromIndex:1];
    const char * rawPropertyType = [propertyType UTF8String];

    if (strcmp(rawPropertyType, @encode(float)) == 0) {
        //it's a float
    } else if (strcmp(rawPropertyType, @encode(int)) == 0) {
        //it's an int
    } else if (strcmp(rawPropertyType, @encode(id)) == 0) {
        //it's some sort of object
    } else {
        // According to Apples Documentation you can determine the corresponding encoding values
    }

    if ([typeAttribute hasPrefix:@"T@"]) {
        NSString * typeClassName = [typeAttribute substringWithRange:NSMakeRange(3, [typeAttribute length]-4)];  //turns @"NSDate" into NSDate
        Class typeClass = NSClassFromString(typeClassName);
        if (typeClass != nil) {
            // Here is the corresponding class even for nil values
        }
    }

}
free(props);

你的if语句是否多余?如果typeAttribute具有该前缀,那么长度不是自动大于1吗,因为该前缀是2个字符? - rvijay007
2
这是非常好的知识,我很高兴找到了你的答案!这对于将bullsh*t解析为ObjC类非常有用,例如当API开发人员决定将所有内容都作为字符串发送时:(:(:( - benjamin.ludwig
1
感谢您的回答!想要补充一下,根据文档,不同的整数类型被编码方式不同:int -> TI,long -> TL,long long -> TQ。顺便说一句,“TQ”这个并没有在文档中提到。 - user3099609
1
我们确定UTF8Encoding对于属性名称是正确的吗? - calql8edkos
好观点。我不确定这一点。它也可能是ACSII编码。参考资料没有为此函数说明。可能在一些 "关于obj-c运行时的常规信息中说了"(找不到)。https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101 - Arndt Bieberstein
3
这段代码有一个棘手的错误,涉及到id类型 -- id属性的编码只是"T@",因此它将通过最后一个if判断,但[typeAttribute length]-4将为负数,导致问题崩溃。 - Alex Medearis

4
受 @arndt-bieberstein 的 ObjC 回答启发,我用 Swift 3 写了一个解决方案(在 Swift 的早期版本中可能非常相似,如果不是相同的话)。你可以在 Github 上找到它:Github。我正在尝试制作一个 pod,但使用 Swift 3 代码时遇到了问题(可能与 CLI xcodebuild 或 Xcode 8 相关)。无论如何,类方法 func getTypesOfProperties(inClass clazz: NSObject.Type) -> Dictionary<String, Any>? 可以提取任何继承自 NSObject 的 Swift 类的名称和类型。该项目的核心是这些方法,但请在 Github 上查看完整代码:Github
  func getTypesOfProperties(in clazz: NSObject.Type) -> Dictionary<String, Any>? {
        var count = UInt32()
        guard let properties = class_copyPropertyList(clazz, &count) else { return nil }
        var types: Dictionary<String, Any> = [:]
        for i in 0..<Int(count) {
            guard let property: objc_property_t = properties[i], let name = getNameOf(property: property) else { continue }
            let type = getTypeOf(property: property)
            types[name] = type
        }
        free(properties)
        return types
    }

   func getTypeOf(property: objc_property_t) -> Any {
        guard let attributesAsNSString: NSString = NSString(utf8String: property_getAttributes(property)) else { return Any.self }
        let attributes = attributesAsNSString as String
        let slices = attributes.components(separatedBy: "\"")
        guard slices.count > 1 else { return getPrimitiveDataType(withAttributes: attributes) }
        let objectClassName = slices[1]
        let objectClass = NSClassFromString(objectClassName) as! NSObject.Type
        return objectClass
    }

   func getPrimitiveDataType(withAttributes attributes: String) -> Any {
        guard let letter = attributes.substring(from: 1, to: 2), let type = primitiveDataTypes[letter] else { return Any.self }
        return type
    }

   func getNameOf(property: objc_property_t) -> String? {
        guard let name: NSString = NSString(utf8String: property_getName(property)) else { return nil }
        return name as String
    }

这个方法可以提取所有属性的NSObject.Type,这些属性的类类型都继承自NSObject,例如NSDate(Swift3:Date)、NSString(Swift3:String?)和NSNumber。然而,它存储在类型Any中(正如该方法返回的Dictionary值的类型所示)。这是由于一些值类型的限制,例如Int、Int32、Bool。因为这些类型不继承自NSObject,所以在Int上调用.self(例如Int.self)不能返回NSObject.Type,而是返回类型Any。因此,该方法返回Dictionary<String, Any>?而不是Dictionary<String, NSObject.Type>?
您可以像下面这样使用此方法:
class Book: NSObject {
    let title: String
    let author: String?
    let numberOfPages: Int
    let released: Date
    let isPocket: Bool

    init(title: String, author: String?, numberOfPages: Int, released: Date, isPocket: Bool) {
        self.title = title
        self.author = author
        self.numberOfPages = numberOfPages
        self.released = released
        self.isPocket = isPocket
    }
}

guard let types = getTypesOfProperties(inClass: Book.self) else { return }
for (name, type) in types {
    print("'\(name)' has type '\(type)'")
}
// Prints:
// 'title' has type 'NSString'
// 'numberOfPages' has type 'Int'
// 'author' has type 'NSString'
// 'released' has type 'NSDate'
// 'isPocket' has type 'Bool'

您也可以尝试将Any转换为NSObject.Type,这适用于继承自NSObject的所有属性,然后可以使用标准的==运算符检查类型:

func checkPropertiesOfBook() {
    guard let types = getTypesOfProperties(inClass: Book.self) else { return }
    for (name, type) in types {
        if let objectType = type as? NSObject.Type {
            if objectType == NSDate.self {
                print("Property named '\(name)' has type 'NSDate'")
            } else if objectType == NSString.self {
                print("Property named '\(name)' has type 'NSString'")
            }
        }
    }
}

如果您声明了这个自定义的==运算符:
func ==(rhs: Any, lhs: Any) -> Bool {
    let rhsType: String = "\(rhs)"
    let lhsType: String = "\(lhs)"
    let same = rhsType == lhsType
    return same
}

你甚至可以像这样检查值类型的类型:

func checkPropertiesOfBook() {
    guard let types = getTypesOfProperties(inClass: Book.self) else { return }
    for (name, type) in types {
        if type == Int.self {
            print("Property named '\(name)' has type 'Int'")
        } else if type == Bool.self {
            print("Property named '\(name)' has type 'Bool'")
        }
    }
}

限制 目前我还无法支持value types为可选类型的情况。如果您在NSObject子类中声明了一个属性,如下所示:var myOptionalInt: Int?,那么我的解决方案将无法正常工作,因为方法class_copyPropertyList无法找到这些属性。

有人有解决办法吗?


在Obj-C中,我可以使用class_copyPropertyList找到可选项。唯一的问题是NSKeyedUnarchiver似乎在尝试从CoreData读回可选的NSNumber blob时抛出异常。一个常规的NSNumber可以工作,但不是可选的。 - Martin-Gilles Lavoie

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