检查给定的元类型是否为枚举类型

5

给定该方法

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

1
你想要做的事情是不可能的。首先,没有通用的枚举类型,因为枚举之间没有共同之处,所以你无法检查一个类型是否为枚举类型。其次,你无法遍历枚举的各个case,所以你无法通过编程的方式确定一个枚举有多少个case。参考这个问题和答案,但请注意,所有解决方法都要求你的枚举具有特定的原始类型,因此它们不能用于任意的枚举。 - Dávid Pásztor
3
一旦Swift达到ABI稳定状态,你可以通过元数据自己实现这个功能。但在那之前,我不建议这样做。然而,一旦https://github.com/apple/swift-evolution/pull/114(希望)最终出现,你就可以直接使用`ValueEnumerable`。 - Hamish
@Hamish 我明白了,谢谢。顺便问一下,你有没有关于为什么当 T 是一个 enum 时,MemoryLayout<T>.size 报告 0 字节的任何见解?在 Swift 中,即使 RawValue 具有非零大小,enum 是否被认为是零大小类型? - dfrib
@Hamish(对于没有关联值的枚举)似乎单个情况的枚举也会报告大小为“0”;对于多个情况(和指定的RawValue类型;即使是具有多字节大小的Int8)),也会报告单个字节1。对于具有关联值的枚举,似乎更连贯:单例枚举报告单个情况的关联值总和的大小,而多例枚举报告大小,就像联合一样(最大情况大小)加上一个字节。对于不持有关联值的枚举来说,这种奇怪的大小报告方式。 - dfrib
1
@dfri 是的,如果你感兴趣,目前枚举类型的布局细节在这里给出:https://github.com/apple/swift/blob/415cd50ba21ceb08dbae4cabdde9035e89f59be1/docs/ABI/TypeLayout.rst#fragile-enum-layout。请注意,编译器可以使用额外的成员,即不形成枚举类型有效值的位模式(和备用位来形成额外的成员),以存储“鉴别器”(值表示哪种情况),因此对于带有关联值的枚举类型并不总是需要添加额外的字节 :) - Hamish
显示剩余8条评论
3个回答

4

为了好玩,作为一种(变通)hack,我们可以实例化T的一个实例,并使用Mirror进行运行时内省,特别是其displayStyle属性。在继续之前,我们注意到这只用于调试目的。

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

我还要指出,我们实际上在追逐自己的尾巴,因为我们求助于运行时来查询在编译时已知的事情(至少由编译器知道)。


首先,我会将enumCaseCount(...)重命名为isEnum(...),因为这个问题只涉及查询一个元类型是否是enum。如果要查询给定enum的案例数量等类似(有些脆弱)的技巧,请参见: 现在,在isEnum(...)中,泛型占位符T只知道它是符合Hashable的类型,这并没有给我们任何直接的方法来实例化T(如果Hashable蓝图化了一个初始化器init(),我们可以轻松地构造T的一个实例并对其进行运行时内省)。相反,我们将手动为单个T实例分配原始内存(UnsafeMutableRawPointer.allocate(bytes:alignedTo:)),将其绑定到TbindMemory(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

1
我不相信Swift实际上已经定义了任何关于布局兼容性的正式规则(这应该随着ABI稳定性而来),因此从技术上讲,我们无法真正推断出两种类型是否具有布局兼容性(尽管对于具有等效结构的类型等明显情况可以安全地假设)。因此,不幸的是,我不确定是否可能以保证良好定义的方式进行此操作。但请注意,您拥有指向[UInt8]的指针,它只有一个字长(数组将其内容间接存储),因此您不能将其视为MemoryLayout<T>.sizeUInt8的缓冲区。 - Hamish
1
如果您想要一个UInt8缓冲区,可以在数组上调用withUnsafeBufferPointer(_ :) - Hamish
1
查看源代码,传递的 capacity: 既用于将内存临时绑定到 T,也用于重新绑定到原始类型,因此如果步幅不同,则可以将例如 5 个 Pointee 实例绑定到 1 个 T 实例,但然后只重新绑定 1 个 Pointee 实例,这可能会有问题(实际上 withMemoryRebound 的更新文档要求具有相同的大小和步幅)。 - Hamish
1
值得注意的是,新的withMemoryRebound方法(将在4.1中引入)具有实际的_debugPrecondition,即MemoryLayout<Element>.stride == MemoryLayout<T>.stride。因此,总体而言,我认为这是一个相当严格的前提条件。 - Hamish
1
@Hamish 是的,我想要表达的是,即使使用原始指针技巧,我们似乎也无法规避为 T 的实例初始化分配的内存,因为我们仅仅从它是 Hashable 就不能知道有关 T 的初始化器的任何信息。没关系,我很高兴获得反馈和练习,而不是结果答案(这仍然是 UB)。现在很少用 Swift 方式了,所以 Swift 讨论(虽然是作为 SO 评论...)总是受人欢迎的! :) - dfrib
显示剩余12条评论

1
作为其他人提到的,Swift 中没有很好的非 hacky 方法来实现这一点。然而,这是 Sourcery 的示例用例之一,它是一个元编程库(这意味着它分析您的代码以生成附加代码)。您编写一个 Stencil 模板来描述其行为,并在 Xcode 中作为构建阶段执行。它可以自动生成任何在项目中找到的枚举类型的代码。 AutoCases 枚举类型示例

0

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

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
}

虽然从逆向工程Swift实现的知识角度来看,你所做的事情很酷,但在任何人的应用程序中,它都是相当无用的实际解决方案,因为513是一个“魔数”,苹果不必以任何方式支持它。实际上,应用商店有规定要求使用仅有的文档/支持接口,而这并不是其中之一。此外,它非常晦涩,因为没有人知道那个数字来自哪里或如何验证它。所有关于这个的东西都太聪明了,除了作为一种知识性的练习之外,几乎没有什么用处。 - clearlight
1
@Binarian 非常抱歉回复晚了!没有注意到你的评论。这是苹果在其ReflectionMirror API中用于提取类型信息的方法(请查看此处)。 - a7md
@clearlight 感谢您的反馈。如我在上面的评论中所提到的,这是苹果在标准库中使用的内容。因此,这不是我通过试错或任何逆向工程努力得出的结果。关于没有保证这种情况不会在未来发生改变,我不是语言或编译器专家,所以我不太确定。我的猜测是,这是内置于语言中的东西,不会轻易改变。由于我们实际上没有调用任何私有API,我认为在生产中使用这段代码相对安全。 - a7md
@a7md 看起来太过于“底层”,如果没有非常充分的理由,就不适合用于生产代码。这些值可能会在任何时候因任何原因而发生变化,而没有通知。苹果公司没有将它们记录下来或描述为公共接口。虽然它们似乎不太可能改变,但是没有必要承担风险,如果它们在应用程序中被更改,而工程师无法方便地修改应用程序,则可能导致灾难性故障。这只是一种低劣的做法。我并不排斥反向工程和启发式算法的使用,如果必要的话,也可以进行原型设计,但这是维护上的大忌。 - clearlight
@a7md 最后另一个问题是“魔术数字”很俗气,所以你必须创建自己的常量来定义它,因为你无法访问苹果的常量。负责任的生产代码需要添加注释/参考,因为人们需要能够快速知道到哪里查看它的使用方式并检查是否有添加和区别。这必须是最后的选择。 - clearlight
显示剩余5条评论

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