Getting the name of a Swift enum value

That test sounds like its bottleneck would be the hashing of Strings, which, unless you're doing that a lot yourself, isn't representative of your actual workload. Besides which, String(describing:) and string interpolation can be slow as well.

At the moment the speed of getting the name is not important to me, but I can see situations where I might care about the speed of == and switch. I wonder if with raw String-based enums if the performance of those is different from Int-based enums. It seems like it would be.

I may find some time later today to write some performance test code for this.

I've done that a lot, and I had the same question as you a couple of days ago. I think it would be nice if String(describing: someEnum) was documented and guaranteed to work as it currently does in the future.

Here's the String initializer which is used there.
    /// Creates a string representing the given value.
    ///
    /// Use this initializer to convert an instance of any type to its preferred
    /// representation as a `String` instance. The initializer creates the
    /// string representation of `instance` in one of the following ways,
    /// depending on its protocol conformance:
    ///
    /// - If `instance` conforms to the `TextOutputStreamable` protocol, the
    ///   result is obtained by calling `instance.write(to: s)` on an empty
    ///   string `s`.
    /// - If `instance` conforms to the `CustomStringConvertible` protocol, the
    ///   result is `instance.description`.
    /// - If `instance` conforms to the `CustomDebugStringConvertible` protocol,
    ///   the result is `instance.debugDescription`.
    /// - An unspecified result is supplied automatically by the Swift standard
    ///   library.
    ///
    /// For example, this custom `Point` struct uses the default representation
    /// supplied by the standard library.
    ///
    ///     struct Point {
    ///         let x: Int, y: Int
    ///     }
    ///
    ///     let p = Point(x: 21, y: 30)
    ///     print(String(describing: p))
    ///     // Prints "Point(x: 21, y: 30)"
    ///
    /// After adding `CustomStringConvertible` conformance by implementing the
    /// `description` property, `Point` provides its own custom representation.
    ///
    ///     extension Point: CustomStringConvertible {
    ///         var description: String {
    ///             return "(\(x), \(y))"
    ///         }
    ///     }
    ///
    ///     print(String(describing: p))
    ///     // Prints "(21, 30)"
    public init<Subject>(describing instance: Subject)

So what String(describing: someEnum) gives us is "the default representation supplied by the standard library" / "An unspecified result supplied automatically by the Swift standard library".

I wouldn't be surprised if this was already relied upon often. I used this trick for the first time just yesterday, and I'm pretty sure I've done so only because I've seen it before. I too wish there was a standard forward-compatible way to get the name of the case.

My use case involves having compile time constants that can represented both as a number or as string depending on context. An enum is pretty convenient to express this mapping because it enforces the case identifiers are unique and the numeric raw values are unique too, plus you can iterate on the cases.

There’s no need for performance test code, a really simple chunk of code will demonstrate why this isn’t a problem. Here’s a sample program:

enum MyEnum: String {
    case theFirst
    case theSecond
}

print(MemoryLayout<String>.size)
print(MemoryLayout<MyEnum>.size)

This will print::

16
1

This is because raw value enums do not literally store their backing value, but instead use the standard enum representation. The raw values are stored in a side table and looked up as needed.

6 Likes

I've actually hit a case where String(describing: someEnum) doesn't work and it's really scuttled my plans for an important use-case :pensive: I'm not sure how I'm going to resolve it—may have to wind up using code generation sadly...

TLDR: if the enum in question conforms to CodingKey (and possibly other protocols) the protocol hijacks the output of String(describing:), and there doesn't appear to be any way to get the case name.

In my case, I have a JSONRPC API I need to interface with that I don't control. It has a slightly asymmetric API shape between creation & update/read operations. I had to write my own custom serialization code for creation paths (do not want to have almost duplicate models for read vs create), and I really wanted to use my existing CodingKeys enums for my models since those are already designed to map between the model property names and the JSON key values. Since create operations aren't really local-performance-bound (they happen infrequently by user action) I can afford to use Mirror to manually compose the create JSON output based on the shape of the model, but to do that, I need to filter some properties, and transform others. I wanted to use the CodingKeys enum as a currency type for key representation since I already need them (my models need Codable conformance for API deserialization & local caching). Unfortunately, this does not work, because the CodingKey protocol hijacks the enum case description.

I really don't want to manually curate two enums for every model, with the only difference being CodingKey conformance... At this point, the most attractive solution I can come up with is to introduce Sourcery and use it to generate a second set of Keys :sob:

Ah, yes of course, any protocol that adds a var description: String { ... } will override the default representation.

Would this be an alternative to Sourcery for you?
enum E1 {
  case foo
  case bar
}
enum E2: CodingKey {
  case baz
  case qux
}

protocol P {}
extension P {
  var theKey: String { String(describing: self) }
}
extension P where Self: CodingKey {
  var theKey: String { self.stringValue }
}
extension E1: P {}
extension E2: P {}

print(E1.foo.theKey) // foo
print(E2.baz.theKey) // baz

Although I guess x.description and String(describing: x) should never be used for anything else than debugging / testing.

1 Like

Thanks, but the problem with this approach is that as a CodingKey, the case name and the rawValue are not necessarily the same, and this distinction is important, as what I'm ultimately trying to achieve is a construction where the case names are matched against property labels discovered through Mirror, and the property's value assigned to the JSON object with the case's rawValue as the key. Your solution above only gives me access to the rawValue of the case, not the case name.

Or to put it another way, it's precisely the CodingKey's nature as effectively a type-safe "dictionary" of "Swift property name" to "external JSON key name" that I want to leverage in this case, which makes it particularly galling that the CodingKey implementation forces me into unwanted code duplication.

I wound up biting the bullet, introducing Sourcery to my project, and auto-generating a
var caseName: String { get } for every enum conforming to my WritableModelKey protocol. This works, but makes me sad :pensive:

I know that transforming between strings and types isn't considered "safe" but this still feels like something that should be a first-class operation, especially considering that String is the property label currency type in Mirror. There are just too many places where being able to access the caseName of an enum case programmatically is useful for odd cases like this.

I suppose the "clean" way to achieve this is to write my own JSON Encoder, but that's a really heavy solution to something that I can achieve with a 20-line function using Mirror :expressionless:

It also wouldn't help at all with other use-cases that have been mentioned.

I don't like to advertise, but a user of a library I'm working on had this exact issue. While I agree that the standard library could probably vend it's own way to give this information to users, I have an interim solution here: Accessing name of enum with a raw value · Issue #9 · Azoy/Echo · GitHub. (I believe everything in this solution is ABI stable on Darwin platforms.) This just looks at the enum metadata to get it's name.

You could probably ask the runtime to get the case name, but keep in mind things can change without notice, so use at your own risk:

@_silgen_name("swift_EnumCaseName")
func _getEnumCaseName<T>(_ value: T) -> UnsafePointer<CChar>?

func getEnumCaseName<T>(for value: T) -> String? {
    if let stringPtr = _getEnumCaseName(value) {
        return String(validatingUTF8: stringPtr)
    }
    return nil
}

enum Foo {
    case bar1
    case bar2(Int)
}

let b1 = Foo.bar1
let b2 = Foo.bar2(0)

print(getEnumCaseName(for: b1)) // bar1
print(getEnumCaseName(for: b2)) // bar2
2 Likes

I wish more people knew this. I’ve seen so many examples of people using enum raw-values on huuuuuge enums, thinking that the conversion was free, when it isn’t.

The inheritance-like syntax certainly doesn’t help the issue. It’s one of my biggest pet peeves in this language.

Just to satisfy my curiosity, how huge is “huuuuuge” :sweat_smile: and how large is this lookup penalty?

I don’t think I ever assumed it was free, but I also don’t have a good mental model of the relative expense either.

Use of unresolved identifier 'reflect'

Where is reflect from?

Any moderately large enum switch compiles down, usually, to a jump table. This is not terribly expensive, but it does defeat the branch target predictor as the jump is highly unpredicable. It also incurs a code size cost to compile that jump table, and the cost of constructing the raw value.

I was under the same impression initially but CustomStringConvertible gets refined by LosslessStringConvertible, which does have semantics in its description (as in it requires T(t.description) == t)

1 Like

Wow that's some black magic I knew nothing about. You wouldn't happen to have a similar incantation to get a String (property name) from a KeyPath? Eg:

struct Foo {
    var color: Color
}
let kp = \Foo.color
???(kp) == "color" 

Rob

If it’s an @objc property, then I think you can access it via ._kvcKeyPathString (but keep in mind this is an undocumented API).

There’s an open bug to add an API to get the string representation of a (Swift) key path: [SR-5220] Expose API to retrieve string representation of KeyPath · Issue #4085 · apple/swift-corelibs-foundation · GitHub

@suyashsrijan

Using your code with @_silgen_name("swift_EnumCaseName") works wonders. However, will it cause me to fail Apple's app review for an iOS app?

E.g. is this considered a private API.

As far as I am aware, compiler internal attributes are not the same as using private APIs from Apple SDKs and so it won’t result in rejection.