使用反射设置对象属性而不使用 setValue forKey

Using reflection to set object properties without using setValue forKey

在 Swift 中无法使用 .setValue(..., forKey: ...)

对此有一种解决方法,即覆盖对象本身的 setValue forUndefinedKey 方法。

因为我正在编写一个基于反射的通用对象映射器。参见 EVReflection 我想尽可能减少这种手动映射。

是否有其他方法可以自动设置这些属性?

可以在我的库中的单元测试中找到解决方法 here 这是代码:

class WorkaroundsTests: XCTestCase {
    func testWorkarounds() {
        let json:String = "{\"nullableType\": 1,\"status\": 0, \"list\": [ {\"nullableType\": 2}, {\"nullableType\": 3}] }"
        let status = Testobject(json: json)
        XCTAssertTrue(status.nullableType == 1, "the nullableType should be 1")
        XCTAssertTrue(status.status == .NotOK, "the status should be NotOK")
        XCTAssertTrue(status.list.count == 2, "the list should have 2 items")
        if status.list.count == 2 {
            XCTAssertTrue(status.list[0]?.nullableType == 2, "the first item in the list should have nullableType 2")
            XCTAssertTrue(status.list[1]?.nullableType == 3, "the second item in the list should have nullableType 3")
        }
    }
}

class Testobject: EVObject {
    enum StatusType: Int {
        case NotOK = 0
        case OK
    }

    var nullableType: Int?
    var status: StatusType = .OK
    var list: [Testobject?] = []

    override func setValue(value: AnyObject!, forUndefinedKey key: String) {
        switch key {
        case "nullableType":
            nullableType = value as? Int
        case "status":
            if let rawValue = value as? Int {
                status = StatusType(rawValue: rawValue)!
            }
        case "list":
            if let list = value as? NSArray {
                self.list = []
                for item in list {
                    self.list.append(item as? Testobject)
                }
            }
        default:
            NSLog("---> setValue for key '\(key)' should be handled.")
        }
    }
}

不幸的是,这在 Swift 中是不可能的。

KVC 是 Objective-C 的东西。纯 Swift 可选值(Int 和 Optional 的组合)不适用于 KVC。使用 Int? 的最佳做法是用 NSNumber? 替换,这样 KVC 就可以工作了。这是因为 NSNumber 仍然是 Objective-C class。这是类型系统的一个可悲的限制。

尽管如此,对于您的枚举,仍有希望。但是,这不会减少您必须执行的编码量,但它更简洁,并且在最好的情况下模仿了 KVC。

  1. 创建一个名为 Settable

    的协议
    protocol Settable {
       mutating func setValue(value:String)
    }
    
  2. 让您的枚举确认协议

    enum Types : Settable {
        case  FirstType, SecondType, ThirdType
        mutating func setValue(value: String) {
            if value == ".FirstType" {
                self = .FirstType
            } else if value == ".SecondType" {
                self = .SecondType
            } else if value == ".ThirdType" {
                self = .ThirdType
            } else {
                fatalError("The value \(value) is not settable to this enum")
            }
       }
    }
    
  3. 创建方法:setEnumValue(value:value, forKey key:Any)

    setEnumValue(value:String forKey key:Any) {
        if key == "types" {
          self.types.setValue(value)
       } else {
          fatalError("No variable found with name \(key)")
       }
    }
    
  4. 您现在可以调用 self.setEnumValue(".FirstType",forKey:"types")

当我试图解决一个类似的问题时,我找到了解决这个问题的方法——KVO 无法设置纯 Swift 协议字段的值。该协议必须标记为@objc,这给我的代码库带来了太多痛苦。 解决方法是使用 objective C 运行时查找 Ivar,获取字段偏移量,然后使用指针设置值。 此代码适用于 Swift 2.2:

的游乐场
import Foundation

class MyClass
{
    var myInt: Int?
}

let instance = MyClass()

// Look up the ivar, and it's offset
let ivar: Ivar = class_getInstanceVariable(instance.dynamicType, "myInt")
let fieldOffset = ivar_getOffset(ivar)

// Pointer arithmetic to get a pointer to the field
let pointerToInstance = unsafeAddressOf(instance)
let pointerToField = UnsafeMutablePointer<Int?>(pointerToInstance + fieldOffset)

// Set the value using the pointer
pointerToField.memory = 42

assert(instance.myInt == 42)

备注:

编辑: https://github.com/wickwirew/Runtime 现在有一个名为 Runtime 的框架,它提供 Swift 4+ 内存的纯 Swift 模型布局,允许它安全地计算 ivar_getOffset 的等价物,而无需调用 Obj C 运行时。这允许设置如下属性:

let info = try typeInfo(of: User.self)
let property = try info.property(named: "username")
try property.set(value: "newUsername", on: &user)

在等效功能成为 Swift 本身的一部分之前,这可能是一个很好的前进方式。

Swift 5

要使用纯 swift 类型设置和获取属性值,您可以使用带有共享函数的内部 ReflectionMirror.swift 方法:

  • swift_reflectionMirror_recursiveCount
  • swift_reflectionMirror_recursiveChildMetadata
  • swift_reflectionMirror_recursiveChildOffset

我们的想法是获取有关对象的每个 属性 的信息,然后通过其指针偏移将值设置为所需的值。

KeyValueCoding 协议的示例代码 Swift 实现 setValue(_ value: Any?, forKey key: String) 方法:

typealias NameFreeFunc = @convention(c) (UnsafePointer<CChar>?) -> Void

struct FieldReflectionMetadata {
    let name: UnsafePointer<CChar>? = nil
    let freeFunc: NameFreeFunc? = nil
    let isStrong: Bool = false
    let isVar: Bool = false
}

@_silgen_name("swift_reflectionMirror_recursiveCount")
fileprivate func swift_reflectionMirror_recursiveCount(_: Any.Type) -> Int

@_silgen_name("swift_reflectionMirror_recursiveChildMetadata")
fileprivate func swift_reflectionMirror_recursiveChildMetadata(
    _: Any.Type
    , index: Int
    , fieldMetadata: UnsafeMutablePointer<FieldReflectionMetadata>
) -> Any.Type

@_silgen_name("swift_reflectionMirror_recursiveChildOffset")
fileprivate func swift_reflectionMirror_recursiveChildOffset(_: Any.Type, index: Int) -> Int

protocol Accessors {}
extension Accessors {
    static func set(value: Any?, pointer: UnsafeMutableRawPointer) {
        if let value = value as? Self {
            pointer.assumingMemoryBound(to: self).pointee = value
        }
    }
}

struct ProtocolTypeContainer {
    let type: Any.Type
    let witnessTable = 0
    
    var accessors: Accessors.Type {
        unsafeBitCast(self, to: Accessors.Type.self)
    }
}

protocol KeyValueCoding {
}

extension KeyValueCoding {
    
    private mutating func withPointer<Result>(displayStyle: Mirror.DisplayStyle, _ body: (UnsafeMutableRawPointer) throws -> Result) throws -> Result {
        switch displayStyle {
        case .struct:
            return try withUnsafePointer(to: &self) {
                let pointer = UnsafeMutableRawPointer(mutating: [=10=])
                return try body(pointer)
            }
        case .class:
            return try withUnsafePointer(to: &self) {
                try [=10=].withMemoryRebound(to: UnsafeMutableRawPointer.self, capacity: 1) {
                    try body([=10=].pointee)
                }
            }
        default:
            fatalError("Unsupported type")
        }
    }
    
    public mutating func setValue(_ value: Any?, forKey key: String) {
        let mirror = Mirror(reflecting: self)
        guard let displayStyle = mirror.displayStyle
                , displayStyle == .class || displayStyle == .struct
        else {
            return
        }
        
        let type = type(of: self)
        let count = swift_reflectionMirror_recursiveCount(type)
        for i in 0..<count {
            var field = FieldReflectionMetadata()
            let childType = swift_reflectionMirror_recursiveChildMetadata(type, index: i, fieldMetadata: &field)
            defer { field.freeFunc?(field.name) }
            guard let name = field.name.flatMap({ String(validatingUTF8: [=10=]) }),
                  name == key
            else {
                continue
            }
            
            let clildOffset = swift_reflectionMirror_recursiveChildOffset(type, index: i)
            
            try? withPointer(displayStyle: displayStyle) { pointer in
                let valuePointer = pointer.advanced(by: clildOffset)
                let container = ProtocolTypeContainer(type: childType)
                container.accessors.set(value: value, pointer: valuePointer)
            }
            break
        }
    }
}

此方法适用于 classstruct,并支持可选、枚举和继承(对于 类)属性:

// Class

enum UserType {
    case admin
    case guest
    case none
}

class User: KeyValueCoding {
    let id = 0
    let name = "John"
    let birthday: Date? = nil
    let type: UserType = .none
}

var user = User()
user.setValue(12345, forKey: "id")
user.setValue("Bob", forKey: "name")
user.setValue(Date(), forKey: "birthday")
user.setValue(UserType.admin, forKey: "type")

print(user.id, user.name, user.birthday!, user.type) 
// Outputs: 12345 Bob 2022-04-22 10:41:10 +0000 admin

// Struct

struct Book: KeyValueCoding {
    let id = 0
    let title = "Swift"
    let info: String? = nil
}

var book = Book()
book.setValue(56789, forKey: "id")
book.setValue("ObjC", forKey: "title")
book.setValue("Development", forKey: "info")

print(book.id, book.title, book.info!) 
// Outputs: 56789 ObjC Development

如果您害怕将 @_silgen_name 用于共享功能,您可以使用 dlsym 动态访问它,例如:dlsym(RTLD_DEFAULT, "swift_reflectionMirror_recursiveCount")

更新

有一个 swift 包 (https://github.com/ikhvorost/KeyValueCoding) 完全实现了纯 Swift 的 KeyValueCoding 协议,它支持: get/set 任何值属性 通过键、下标、获取元数据类型、属性列表等。