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

23

在Swift中,不可能使用.setValue(..., forKey: ...)方法来设置:

  • 可选类型字段,例如Int?
  • 具有enum类型的属性
  • 一个包含可选对象的数组,例如[MyObject?]

有一种解决方法,就是在对象本身中重写setValue forUndefinedKey方法。

由于我正在编写一个基于反射的通用对象映射器。请参阅EVReflection,我想尽可能地减少这种手动映射。

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

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

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源代码,因为他们知道如何迭代Swift属性(使用反射函数返回MirrorType,不仅包括对象的副本,还包括每个属性的引用),所以如果MirrorType成为开源代码的一部分,你就可以看到他们是如何实现的,并将该方法移植到你的库中。 - s1ddok
好的,我已经能够获取这些值了。现在我想要设置这些值。 - Edwin Vermeer
你能否在没有镜像类型的情况下获取它们? - s1ddok
你需要使用reflect(..)来获取MirrorType。在这个链接的底部可以看到valueForAny方法:https://github.com/evermeer/EVReflection/blob/master/EVReflection/pod/EVReflection.swift - Edwin Vermeer
1
这就是我所说的。你只能通过使用reflect函数和MirrorType来获取值,但你不知道苹果在后台是如何做到的。他们可以在运行时以某种方式迭代属性,直到他们发布源代码,我们才会知道。 - s1ddok
3个回答

15

当我在寻找解决类似问题的方法时,我找到了一种方法- KVO无法设置纯Swift协议字段的值。该协议必须标记为@objc,这在我的代码库中引起了太多的痛苦。

解决方法是使用Objective C运行时查找Ivar,获取字段偏移量,并使用指针设置值。以下代码适用于Swift 2.2的playground:

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)

注意:

编辑:现在有一个名为Runtime的框架,位于https://github.com/wickwirew/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 本身的一部分。

非常酷的代码!你说得对,这是危险的代码。我仍然会玩弄它,看看它的潜力。 - Edwin Vermeer
非常好用!!!我发现了一些有趣的东西(绝对不应该在生产中使用):似乎你可以只使用UnsafeMutablePointer<objc_property_t>(而不是你代码中的UnsafeMutablePointer<Int?>)来创建任何类型的映射器。完全不安全,但非常酷。 - Anton Belousov

2

Swift 5

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

  • swift_reflectionMirror_recursiveCount
  • swift_reflectionMirror_recursiveChildMetadata
  • swift_reflectionMirror_recursiveChildOffset

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

这里有一个使用 Swift 的 KeyValueCoding 协议实现了 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: $0)
                return try body(pointer)
            }
        case .class:
            return try withUnsafePointer(to: &self) {
                try $0.withMemoryRebound(to: UnsafeMutableRawPointer.self, capacity: 1) {
                    try body($0.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: $0) }),
                  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协议,并支持:通过键获取/设置任何属性的值、下标、获取元数据类型、属性列表等功能。


2
很遗憾,在Swift中无法实现这一点。 KVC是Objective-C的东西。纯Swift可选项(Int和Optional的组合)不能与KVC一起使用。对于Int?,最好的做法是替换为NSNumber?,然后KVC将会正常工作。这是因为NSNumber仍然是Objective-C类。这是类型系统的一个悲哀限制。
不过,对于您的枚举,仍有希望。但这不会减少您需要编写的代码量,但它更加清晰,并且在最佳情况下模拟了KVC。
  1. Create a protocol called Settable

    protocol Settable {
       mutating func setValue(value:String)
    }
    
  2. Have your enum confirm to the protocol

    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. Create a method: 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. You can now call self.setEnumValue(".FirstType",forKey:"types")

在Swift 2中,performSelector又回来了,现在你可以使用类似以下的方法:NSTimer.scheduledTimerWithTimeInterval(0.001, target: myObject, selector: Selector(sel), userInfo: [3], repeats: false) 或者 NSThread.detachNewThreadSelector(Selector("myMethod:"), toTarget:myObject, withObject: "myValue")。 - Edwin Vermeer
你确定吗?我刚刚匆忙使用我的Xcode 7 Beta,结果发现没有performSelector方法。 - avismara
请查看发布说明文档(http://adcdownload.apple.com/Developer_Tools/Xcode_7_beta_4/Xcode_7_beta_4_Release_Notes.pdf),其中包含有关此问题的详细信息。 - Edwin Vermeer
显然,它仅适用于watchOS 2及更高版本。 - avismara
是的。灵活性和可读性。假设您有其他一百个枚举。这种设置方式要容易得多。此外,self.setEnumValue(".FirstType",forKey:"types")self.setValue(0, forUndefinedKeyKey:"types")更易于阅读。 - avismara
显示剩余4条评论

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