Search code examples
swift3enumscustomstringconvertible

CustomStringConvertible in enum


I have following enum in a class.

enum Attributes: String, CustomStringConvertible {
    case eventDate
    case eventName
    case eventType
    case country

    var description: String {
        return self.rawValue
    }
}

When I try have the following code, compiler complains with following error.

var attributesList: [String] {
    return [
        Attributes.eventDate, //<-- Compiler error on this row
        Attributes.eventName,
        Attributes.eventType,
        Attributes.country]
}

Cannot convert value of the type 'Attributes' to expected element type 'String'

Shouldn't the "CustomStringConvertible" protocol return the "description"? What is wrong in the above code?


Solution

  • TL;DR - It doesn't work because an array of Attributes cannot be assigned to an array of Strings, they are both mismatched types, and Swift does not do automatic conversion between types, and an explict conversion needs to be specified.


    In Swift, when you initialise an array using an array literal, the following happens under the hood:

    let words = ["hello", "world"]
    
    • The compiler recognises that an array literal is being assigned to a variable named words. Since we have not specified the type of the words, implicitly an array is assumed. The type of the elements underlying the array is determined based on the contents of the array literal.
    • In this case, the array literal is a collection of String types; this is easily understood by the compiler
    • Since the LHS type is an array, the RHS structure is an array literal, and since the LHS type (Array) conforms to a pre-defined protocol called ExpressibleByArrayLiteral which has an associated type constraint to match Element, the compiler will actually be converting our line to the following

    Example:

    let words = [String].init(arrayLiteral: ["hello", "world"]) // we do not call this init directly
    

    This is how initialising with array literals work. In the above example, since we did not specify the type of array, the implicit type setting will work. If we specified a mismatched type, the assignment will fail, since ExpressibleByArrayLiteral requires the associated Element type of the array literal and the actual array you are assigning to to match.

    So the following fails:

    let words:[String] = [1, 2] // array literal has Element=Int, array has Element=String
    

    This also shows that there is no implicit type conversion between Int and String, even though Int conform to CustomStringConvertible.

    In your case, you are trying to assign an array literal consisting of Attributes to an array of String. This is a type mismatch. This is the reason it fails.

    If you state protocol conformance, the following line will work:

    var attributesList: [CustomStringConvertible] {
        return [
            Attributes.eventDate,
            Attributes.eventName,
            Attributes.eventType,
            Attributes.country
        ]
    }
    // note that we have an array of CustomStringConvertible protocol,
    // each element here is still of Attributes type
    // a type conforming to a protocol can be cast to an instance
    // of that protocol automatically
    // The above initialisation works simply because the following
    // also works without any further action required
    // let a:CustomStringConvertible = Attributes.country
    

    If you really want a list of string values, you need to map this to a string explicitly:

    var attributesList1: [String] {
        return [
            Attributes.eventDate,
            Attributes.eventName,
            Attributes.eventType,
            Attributes.country
            ].map { $0.description }
    }
    
    var attributesList2: [String] {
        return [
            Attributes.eventDate.description,
            Attributes.eventName.description,
            Attributes.eventType.description,
            Attributes.country.description
            ]
    }