如何镜像Codable/CodableKeys协议的设计?

4
我正在尝试实现类似Swift的功能,该功能利用在实现Codable的类中定义的枚举上设置的CodableKeys协议。在我的情况下,类是CommandHandler,枚举是CommandIds。它不需要编译器生成的代码,因为枚举将始终被明确指定。以下是我想要实现的简化版本...
protocol CommandId{}
protocol CommandHandler{
    associatedtype CommandIds : CommandId, RawRepresentable
}

class HandlerA : CommandHandler{
    enum CommandIds : String, CommandId{
        case commandA1
        case commandA2
    }
}

class HandlerB : CommandHandler{
    enum CommandIds : String, CommandId{
        case commandB1
        case commandB2
        case commandB3
    }
}

func processHandler<T:CommandHandler>(_ handler:T){
    // Logic to iterate over CommandIds. <-- This is where I get stumped
}

let handlerA = HandlerA()
processHandler(handlerA)

我在这里挣扎于processHandler中的代码,因为我不知道如何从处理程序实例中访问枚举值。
那么我缺少什么呢?获取关联枚举值的代码是什么?

Swift 的 Codable 使用的是协议扩展。 - matt
我已经更新了我的问题,使其更加清晰/简洁。 - Mark A. Donohoe
5个回答

1

好的,我相信我已经准备好了所有的内容,来展示你如何在Swift中实现这个。事实证明,我的修订问题在如何做到这一点方面是正确的边缘。

下面是我用Swift 4编写的示例...

首先,这里是你定义协议所需的内容,使其能够正常工作。从设计的角度来看,这些与CodableKeys和Codable是同义词。

protocol CommandId : EnumerableEnum, RawRepresentable {}

protocol CommandHandler{
    associatedtype CommandIds : CommandId
}

这是一个协议及其相关扩展,用于使枚举的“case”值可枚举。只需使您的枚举符合EnumerableEnum协议,即可获得一个“values”数组。
由于上面的CommandId协议已经应用于相关的枚举,我们通过在其自身定义中也应用EnumerableEnum协议来简化事情。这样,我们只需要将CommandId应用于我们的枚举,就可以同时获得两者。
public protocol EnumerableEnum : Hashable {
    static var values: [Self] { get }
}

public extension EnumerableEnum {

    public static var values: [Self] {

        let valuesSequence = AnySequence { () -> AnyIterator<Self> in

            var caseIndex = 0

            return AnyIterator {
                let currentCase: Self = withUnsafePointer(to: &caseIndex){
                    $0.withMemoryRebound(to: self, capacity: 1){
                        $0.pointee
                    }
                }
                guard currentCase.hashValue == caseIndex else {
                    return nil
                }
                caseIndex += 1
                return currentCase
            }
        }

        return Array(valuesSequence)
    }
}

这里有两个实现我的CommandHandler/CommandId协议的类。
class HandlerA : CommandHandler{

    enum CommandIds : Int, CommandId{
        case commandA1
        case commandA2
    }
}

class HandlerB : CommandHandler{
    enum CommandIds : String, CommandId{
        case commandB1 = "Command B1"
        case commandB2
        case commandB3 = "Yet another command"
    }
}

这是一个接受CommandHandler类型的测试函数。
func enumerateCommandIds<T:CommandHandler>(_ commandHandlerType:T.Type){

    for value in commandHandlerType.CommandIds.values{
        let caseName     = String(describing:value)
        let caseRawValue = value.rawValue

        print("\(caseName) = '\(caseRawValue)'")
    }
}

最后,这是运行测试的结果

enumerateCommandIds(HandlerA.self)
// Outputs
//     commandA1 = '0'
//     commandA2 = '1'

enumerateCommandIds(HandlerB.self)
// Outputs
//     commandB1 = 'Command B1'
//     commandB2 = 'commandB2'
//     commandB3 = 'Yet another command'

这是一条漫长而曲折的道路,但我们终于到达了目的地!感谢每个人的帮助!


0

你可以轻松地使用Swift的CaseIterable协议来实现这一点。

protocol CommandId: CaseIterable {
    func handle()
}

protocol CommandHandler {
    associatedtype CommandIds: CommandId, RawRepresentable
}

class HandlerA: CommandHandler {
    enum CommandIds: String, CommandId {
        case commandA1
        case commandA2

        func handle() {
            print("\(rawValue) is handled")
        }
    }
}

class HandlerB: CommandHandler {
    enum CommandIds: String, CommandId {
        case commandB1
        case commandB2
        case commandB3

        func handle() {
            print("\(rawValue) is handled")
        }
    }
}

func processHandler<T: CommandHandler>(_ handler: T) {
    // Logic to iterate over CommandIds. <-- This is where I get stumped
    T.CommandIds.allCases.forEach({ $0.handle() })
}

let handlerA = HandlerA()
processHandler(handlerA)

0
我脑海中唯一的解决方案是使用associatedtype并将枚举移出协议,做类似于这样的事情:
enum Commands:String {
    case default_command = ""
}

protocol CommandDef {
    associatedtype Commands
}

class MyClassA : CommandDef {
    enum Commands : String {
        case commandA1
        case commandA2 = "Explicit A2"
    }
}

class MyClassB : CommandDef {
    enum Commands : String {
        case commandB1
        case commandB2 = "Explicit B2"
        case commandB3
    }
}

但是您不必(实际上也不能!)将CodableKeys移动到您定义为Codable的类型之外。我一直在尝试深入研究Swift源代码,看看他们是如何做到的,但那里非常复杂。 - Mark A. Donohoe

0
在 Swift 中,协议中不允许有枚举类型。如果这是可能的话,协议将无法引用枚举情况。最终,您必须进行 as 转换,打破协议理念。
也许关联类型最适合您的目的?
enum Commands:String {
    case compliaceType = ""
}

protocol CommandDef {
    associatedtype Commands
}

class MyClassA : CommandDef {

    enum Commands : String {
        case commandA1 = "hi"
        case commandA2 = "Explicit A2"
    }


}

class MyClassB : CommandDef {
    enum Commands : String {
        case commandB2 = "Explicit B2"
    }

}

print(MyClassA.Commands.commandA1)

0

使用Swift语言本身没有办法模仿Codable,因为Codable的实现依赖于编译器生成特殊代码。具体来说,没有协议扩展可以创建默认的CodingKeys枚举,编译器会自动在符合Codable的类型中创建该枚举,除非您自己指定。

这类似于Swift编译器将自动为结构体创建初始化程序(“成员初始化程序”),除非您指定自己的初始化程序。在这种情况下,也没有协议扩展或Swift语言功能可用于复制自动生成的结构体初始化程序,因为它基于元编程/代码生成,即由编译器生成。

有一些工具,例如Sourcery(https://github.com/krzysztofzablocki/Sourcery),允许您实现自己的元编程和代码生成。使用Sourcery,您可以在构建阶段运行脚本,自动生成所需的Command枚举代码,并将其添加到符合CommandHandler的任何类型中。

这本质上模仿了 Codable 如何通过 Swift 编译器生成所需代码的方式。但在这两种情况下,都不是通过像协议扩展等 Swift 语言特性来实现的。相反,这是一个脚本编写的样板源代码,而不必手动编写。

更新修订后的问题


如果你只需要确保有一种方法可以枚举所有的CommandIds枚举类型,那么你可以像这样向CommandId协议添加一个协议要求:

protocol CommandId {
    static var all: [Self] { get }
}

那么实现就需要像这样:

class HandlerA : CommandHandler {
    enum CommandIds : String, CommandId {
        case commandA1
        case commandA2

        static var all: [CommandIds] { return [.commandA1, .commandA2] }
    }
}

而你的处理函数可能看起来像:

func processHandler<T:CommandHandler>(_ handler:T){
    T.CommandIds.all.forEach { // Do something with each command case }
}

值得继续注意的是,对于 Codable,Swift 并没有或使用任何语言功能来枚举所有情况。相反,编译器利用符合 Codable 的类型的所有属性知识为该类型生成特定的 init(from decoder: Decoder) 实现,包括基于已知的属性名称和类型的每个情况的行,例如:

// This is all the code a developer had to write
struct Example: Codable {
  let name: String
  let number: Int
}

// This is all source code generated by the compiler using compiler reflection into the type's properties, including their names and types
extension Example {
  enum CodingKeys: String, CodingKey {
    case name, number
  }

  init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    name = try values.decode(String.self, forKey: .name)
    number = try values.decode(Int.self, forKey: .number)
  }
}

由于Swift是一种大多数静态语言,具有极其有限的运行时反射能力(目前),因此无法使用语言特性在运行时执行这些类型的任务。

但是,没有任何阻止您或任何开发人员使用代码生成方式来完成类似的便利,就像Swift编译器一样。实际上,苹果Swift核心团队的知名成员甚至在我向他展示我在WWDC中面临的一些挑战时鼓励我这样做。

值得注意的是,现在已成为Swift编译器一部分或具有打开拉取请求以添加到Swift编译器中的功能(例如Codable和自动符合Equatable和Hashable)是在使用Sourcery创建和实现的真实Swift项目中首先被创建和实现的。


我已经修改/重写了问题,以使其更清晰/简洁地表达我所追求的内容。 - Mark A. Donohoe
@MarqueIV。已更新答案,希望有所帮助。 - Daniel Hall
谢谢更新。然而,这需要手动将所有键复制到数组中。话虽如此,我一直在尝试类似的东西,其中CommandId枚举的协议扩展会为您枚举值,因此您不必手动创建数组。这很有前途,但我仍在努力解决一些问题。随着我的进展,会有更多信息... - Mark A. Donohoe
是的,它确实需要将所有键手动复制到数组中。正如所指出的那样,这就是为什么Swift需要使用编译器反射和代码生成来完成此操作,而不是语言特性(因为当前不存在执行此操作的语言特性)。如果您在尝试使用Swift语言特性反射到枚举时想不出任何想法,请再考虑一下这个答案!或者,如果您找到了一个很好的解决方案,请在此处分享,以便我们知道您是如何做到的 :) - Daniel Hall
我确实有一种Swift的方法来做到这一点(即枚举值的枚举),只要您使您的枚举遵循一个协议,这很好,因为Codable使其枚举遵循CodableKeys,而这与我们在这里所做的基本相同。稍微整理一下后会有更多内容。 - Mark A. Donohoe

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