Enum Swift com inicializador personalizado perde o inicializador rawValue

95

Tentei reduzir esse problema à sua forma mais simples com o seguinte.

Configuração

Xcode versão 6.1.1 (6A2008a)

Um enum definido em MyEnum.swift:

internal enum MyEnum: Int {
    case Zero = 0, One, Two
}

extension MyEnum {
    init?(string: String) {
        switch string.lowercaseString {
        case "zero": self = .Zero
        case "one": self = .One
        case "two": self = .Two
        default: return nil
        }
    }
}

e o código que inicializa o enum em outro arquivo MyClass.swift:

internal class MyClass {
    let foo = MyEnum(rawValue: 0)  // Error
    let fooStr = MyEnum(string: "zero")

    func testFunc() {
        let bar = MyEnum(rawValue: 1)  // Error
        let barStr = MyEnum(string: "one")
    }
}

Erro

O Xcode me dá o seguinte erro ao tentar inicializar MyEnumcom seu inicializador de valor bruto:

Cannot convert the expression's type '(rawValue: IntegerLiteralConvertible)' to type 'MyEnum?'

Notas

  1. De acordo com o Guia de Idiomas Swift :

    Se você definir uma enumeração com um tipo de valor bruto, a enumeração receberá automaticamente um inicializador que recebe um valor do tipo do valor bruto (como um parâmetro chamado rawValue) e retorna um membro de enumeração ou nil.

  2. O inicializador personalizado para MyEnumfoi definido em uma extensão para testar se o inicializador de valor bruto do enum estava sendo removido devido ao seguinte caso do Guia de Idiomas . No entanto, ele obtém o mesmo resultado de erro.

    Observe que se você definir um inicializador personalizado para um tipo de valor, não terá mais acesso ao inicializador padrão (ou ao inicializador de membro, se for uma estrutura) para esse tipo. [...]
    Se você deseja que seu tipo de valor customizado seja inicializável com o inicializador padrão e o inicializador de membro, e também com seus próprios inicializadores customizados, escreva seus inicializadores customizados em uma extensão em vez de como parte da implementação original do tipo de valor.

  3. Mover a definição de enum para MyClass.swiftresolver o erro para, barmas não para foo.

  4. Remover o inicializador personalizado resolve os dois erros.

  5. Uma solução alternativa é incluir a função a seguir na definição de enum e usá-la no lugar do inicializador de valor bruto fornecido. Portanto, parece que adicionar um inicializador personalizado tem um efeito semelhante a marcar o inicializador de valor bruto private.

    init?(raw: Int) {
        self.init(rawValue: raw)
    }
  6. Declarar explicitamente a conformidade do protocolo com RawRepresentableem MyClass.swiftresolve o erro embutido para bar, mas resulta em um erro do vinculador sobre símbolos duplicados (porque enums de tipo de valor bruto estão implicitamente em conformidade com RawRepresentable).

    extension MyEnum: RawRepresentable {}

Alguém pode fornecer mais informações sobre o que está acontecendo aqui? Por que o inicializador de valor bruto não está acessível?

Nickgraef
fonte
Você deve registrar um bug sobre isso - os inicializadores padrão devem ter internalescopo (ou pelo menos corresponder ao tipo), não private.
Nate Cook
Estou tendo exatamente o mesmo problema. Depois de criar um inicializador personalizado, o padrão desaparece
Yariv Nissim
Para mim, cheira a inseto.
akashivskyy
2
Obrigado por validar minhas suspeitas. Isso foi arquivado como um bug.
nickgraef
O número 5 fez isso por mim.
Andrew Duncan

Respostas:

25

Este bug é resolvido no Xcode 7 e Swift 2

Alcamla
fonte
24
Respostas deste tipo lucram com um link para o ticket associado para que os futuros visitantes possam verificar o estado da questão.
Raphael de
14
extension TemplateSlotType {
    init?(rawString: String) {
        // Check if string contains 'carrousel'
        if rawString.rangeOfString("carrousel") != nil {
            self.init(rawValue:"carrousel")
        } else {
            self.init(rawValue:rawString)
        }
    }
}

No seu caso, isso resultaria na seguinte extensão:

extension MyEnum {
    init?(string: String) {
        switch string.lowercaseString {
        case "zero": 
            self.init(rawValue:0)
        case "one": 
            self.init(rawValue:1)
        case "two":
            self.init(rawValue:2)
        default: 
            return nil
        }
    }
}
Antoine
fonte
7

Você pode até tornar o código mais simples e útil sem switchcasos, dessa forma você não precisa adicionar mais casos ao adicionar um novo tipo.

enum VehicleType: Int, CustomStringConvertible {
    case car = 4
    case moped = 2
    case truck = 16
    case unknown = -1

    // MARK: - Helpers

    public var description: String {
        switch self {
        case .car: return "Car"
        case .truck: return "Truck"
        case .moped: return "Moped"
        case .unknown: return "unknown"
        }
    }

    static let all: [VehicleType] = [car, moped, truck]

    init?(rawDescription: String) {
        guard let type = VehicleType.all.first(where: { description == rawDescription })
            else { return nil }
        self = type
    }
}
carbonr
fonte
1

Sim, este é um problema irritante. Atualmente, estou trabalhando nisso usando uma função de escopo global que atua como uma fábrica, ou seja,

func enumFromString(string:String) -> MyEnum? {
    switch string {
    case "One" : MyEnum(rawValue:1)
    case "Two" : MyEnum(rawValue:2)
    case "Three" : MyEnum(rawValue:3)
    default : return nil
    }
}
Cinza
fonte
0

Isso funciona para Swift 4 no Xcode 9.2 junto com meu EnumSequence :

enum Word: Int, EnumSequenceElement, CustomStringConvertible {
    case apple, cat, fun

    var description: String {
        switch self {
        case .apple:
            return "Apple"
        case .cat:
            return "Cat"
        case .fun:
            return "Fun"
        }
    }
}

let Words: [String: Word] = [
    "A": .apple,
    "C": .cat,
    "F": .fun
]

extension Word {
    var letter: String? {
        return Words.first(where: { (_, word) -> Bool in
            word == self
        })?.key
    }

    init?(_ letter: String) {
        if let word = Words[letter] {
            self = word
        } else {
            return nil
        }
    }
}

for word in EnumSequence<Word>() {
    if let letter = word.letter, let lhs = Word(letter), let rhs = Word(letter), lhs == rhs {
        print("\(letter) for \(word)")
    }
}

Resultado

A for Apple
C for Cat
F for Fun
mclam
fonte
-1

Adicione isto ao seu código:

extension MyEnum {
    init?(rawValue: Int) {
        switch rawValue {
        case 0: self = .Zero
        case 1: self = .One
        case 2: self = .Two
        default: return nil
        }
    }
}
Tony Swiftguy
fonte
Você pode estender Int em vez disso? Parece que é mais fácil.
ericgu