【问题标题】:Check if a given metatype is an enum检查给定的元类型是否为枚举
【发布时间】:2018-06-16 01:34:53
【问题描述】:

给定方法

func enumCaseCount<T: Hashable>(ofType type: T.Type) -> Int {
    // Needed check if type is an enum type

   return 3
}

如下使用

private enum SimpleEnum: String {
    case a = "A"
    case b = "B"
    case c = "C"
}

enumCaseCount(ofType: SimpleEnum.self)

知道如何检查给定的元类型是否为枚举吗?


可以通过这种方式测试类

class Test {}
Test.self is AnyClass // returns true

【问题讨论】:

  • 您想要做的事情是不可能的。首先,没有通用的枚举类型,因为枚举没有共同点,所以你无法检查一个类型是否是枚举。其次,您无法遍历枚举的案例,因此您将无法以编程方式确定枚举有多少案例。请参阅this Q&A,但请注意所有解决方法都要求您的枚举具有特定的原始类型,因此它们不适用于任何任意枚举。
  • 一旦 Swift ABI 稳定,您可以 climb through the metadata yourself 执行此操作。不过,我不建议这样做。但是,一旦github.com/apple/swift-evolution/pull/114(希望)终于出现,您就可以使用ValueEnumerable
  • @Hamish 很奇怪,我从未见过ValueEnumerable 提案,尽管该公关已经存在了很长一段时间;看起来很整洁。不过最近更新了:你认为我们会看到“很快”吗? (已经在 Swift 4.1 中?)
  • @dfri 不幸的是,假设它确实被接受(它甚至还没有被安排审查),它probably won't surface until at least Swift 5。 Swift 4.1 基本完成了(现在只是 bug 修复),所以 def 不会进入那个。
  • @dfri 是的,如果你有兴趣,这里给出了枚举的当前布局细节:github.com/apple/swift/blob/…。请注意,编译器可以使用额外的居民,即不形成枚举的有效值的位模式(以及形成额外居民的备用位)来存储“鉴别器”(值表示的情况),因此额外的字节不会'不必总是为具有关联值的枚举添加:)

标签: swift enums metatype


【解决方案1】:

为了好玩,作为(解决方法)hack,我们可以实例化T 的实例并使用Mirror 对其执行运行时自省,特别是它的displayStyle 属性。在我们继续之前,我们注意到我们只会将其用于调试目的

游乐场和调试器使用镜像。

我还要指出,当我们求助于运行时来查询编译时已知的事物(至少是编译器)时,我们真的在追尾。


无论如何,首先,我将enumCaseCount(...) 重命名为isEnum(...),因为这个问题只涉及查询元类型是否为enum。对于查询给定enum 的案例数量的类似(有点脆弱)黑客,请参阅:

现在,isEnum(...) 中的通用占位符 T 只知道它是符合 Hashable 的类型,这并没有为我们提供任何直接的方法来实例化 T 的实例(如果 @ 987654341@ 蓝图,比如说,一个初始化器init(),我们可以很容易地构造一个T 的实例并对其执行运行时自省)。相反,我们将求助于为单个 T 实例 (UnsafeMutableRawPointer.allocate(bytes:alignedTo:)) 手动分配原始内存,将其绑定到 T (bindMemory(to:capacity:)),最后释放内存 (deallocate(bytes:alignedTo:))完成了对指向绑定内存的指针引用的实例的运行时自省。至于运行时自省,我们简单地使用Mirror来检查它的displayStyle是否为enum

func isEnum<T: Hashable>(_: T.Type) -> Bool {
    var result = false
    // Allocate memory with size and alignment matching T.
    let bytesPointer = UnsafeMutableRawPointer.allocate(
        bytes: MemoryLayout<T>.size,
        alignedTo: MemoryLayout<T>.alignment)
    // Bind memory to T and perform introspection on the instance
    // reference to by the bound memory.
    if case .some(.`enum`) = Mirror(reflecting:
        bytesPointer.bindMemory(to: T.self, capacity: 1).pointee)
        .displayStyle {
        print("Is an enum")
        result = true
    } else { print("Is not an enum") }
    // Deallocate the manually allocate memory.
    bytesPointer.deallocate(bytes: MemoryLayout<T>.size,
                            alignedTo: MemoryLayout<T>.alignment)
    return result
}

示例用法:

enum SimpleEnum { case a, b, c }

enum SimpleStrEnum: String {
    case a = "A"
    case b = "B"
    case c = "C"
}

enum SimpleExplicitIntEnum: Int { case a, b, c }

struct SimpleStruct: Hashable {
    let i: Int
    // Hashable
    var hashValue: Int { return 0 }
    static func ==(lhs: SimpleStruct, rhs: SimpleStruct) -> Bool { return true }
}

print(isEnum(SimpleEnum.self))            // true
print(isEnum(SimpleStrEnum.self))         // true
print(isEnum(SimpleExplicitIntEnum.self)) // true
print(isEnum(SimpleStruct.self))          // false

【讨论】:

  • 我不相信 Swift 实际上已经定义了任何关于布局兼容性的正式规则(这应该伴随 ABI 稳定性),所以从技术上讲,我们无法真正推断两种类型是否布局兼容(尽管可以安全地假设诸如具有等效结构的类型之类的明显情况)。所以不幸的是,我不确定是否有可能以一种有保证的明确方式来解决这个问题。尽管请注意您有一个指向[UInt8] 的指针,它只有一个字长(数组间接存储其内容),因此您不能将其视为MemoryLayout&lt;T&gt;.size UInt8s 的缓冲区。
  • (如果你想要UInt8s的缓冲区,你可以在数组上调用withUnsafeBufferPointer(_:)
  • 查看at the source,传递的capacity:既用于临时将内存绑定到T,也用于重新绑定到原始类型,因此如果步幅不同,您可以绑定例如5 Pointee 的实例到 T 的 1 个实例,但是你只会重新绑定 Pointee 的 1 个实例,这可能会出现问题(实际上 withMemoryRebound 的更新文档要求相同的大小 和 跨步)。
  • 另外值得注意的是the new withMemoryRebound method(将在4.1中引入)有一个实际的_debugPreconditionMemoryLayout&lt;Element&gt;.stride == MemoryLayout&lt;T&gt;.stride。所以总的来说,我会说这是一个非常严格的先决条件。
  • @Hamish 是的,我想它的要点是我们似乎无法绕过实际初始化分配的内存到T 的实例,即使使用原始指针技巧 - 因为我们对 T 的初始化程序一无所知,只是因为它是 Hashable。不用担心,我很高兴得到反馈和练习,而不是得到的答案(仍然是 UB)。这些天我很少使用 Swift 的方式,所以总是很感激 Swift 讨论(尽管作为 SO cmets ...)! :)
【解决方案2】:

正如其他人所提到的,在 Swift 中没有很好的非 hacky 方式来做到这一点。但是,它是 Sourcery 的示例用例之一,这是一个元编程库(这意味着它会分析您的代码以生成其他代码)。您编写一个 Stencil 模板来描述其行为,并在 Xcode 中作为构建阶段执行。它可以为您项目中找到的任何枚举自动生成此代码。

AutoCases enum example

【讨论】:

    【解决方案3】:

    要检查某个类型是否实际上是 Enum,您可以使用:

    func isEnum<T>(_ type: T.Type) -> Bool {
        let ptr = unsafeBitCast(T.self as Any.Type, to: UnsafeRawPointer.self)
        return ptr.load(as: Int.self) == 513
    }
    

    您可以像这样简单地使用它:

    enum MyEnum {}
    struct MyStruct {}
    class MyClass {}
    
    isEnum(MyEnum.self)   // this returns true
    isEnum(MyStruct.self) // this returns false
    isEnum(MyClass.self)  // this returns false
    

    在你的例子中,你会像这样使用它:

    func enumCaseCount<T: Hashable>(ofType type: T.Type) -> Int {
       isEnum(T.self) // returns true if enum
       return 3
    }
    

    【讨论】:

    • 这很有趣。为什么是枚举指针 513,任何文档/视频?是官方行为还是未来随时可以改变?
    猜你喜欢
    • 2011-12-24
    • 1970-01-01
    • 1970-01-01
    • 2013-03-20
    • 1970-01-01
    • 2020-12-24
    • 1970-01-01
    • 2019-01-24
    • 2011-06-23
    相关资源
    最近更新 更多