简体   繁体   English

Swift 枚举可以有多个原始值吗?

[英]Can Swift enums have multiple raw values?

I want to associate two raw values to an enum instance (imagine an enum representing error types, I want Error.Teapot to have an Int type property code with value 418, and a String property set to I'm a teapot .)我想将两个原始值关联到一个枚举实例(想象一个代表错误类型的枚举,我希望Error.Teapot具有一个值为 418 的 Int 类型属性code ,以及一个设置为I'm a teapot的 String 属性。)

Note the difference between raw values and associated values here—I want all Teapot instances to have a code of 418, I don't want a unique associated value for each Teapot instance.请注意此处原始值关联值之间的区别——我希望所有Teapot实例的code都是 418,我不希望每个Teapot实例都有一个唯一的关联值。

Is there a better way than adding computed properties to the enum that switch ed on self to look up the appropriate value?有没有比将计算属性添加到switch ed on self以查找适当值的枚举更好的方法?

No, an enum cannot have multiple raw values - it has to be a single value, implementing the Equatable protocol, and be literal-convertible as described in the documentation .不,枚举不能有多个原始值 - 它必须是单个值,实现Equatable协议,并且可以如文档中所述进行文字转换。

I think the best approach in your case is to use the error code as raw value, and a property backed by a prepopulated static dictionary with the error code as key and the text as value.我认为在您的情况下,最好的方法是使用错误代码作为原始值,并使用由预先填充的静态字典支持的属性,错误代码作为键,文本作为值。

You have a couple options.你有几个选择。 But neither of them involve raw values.但它们都不涉及原始值。 Raw values are just not the right tool for the task.原始值并不是完成任务的正确工具。

Option 1 (so-so): Associated Values选项 1(一般):相关值

I personally highly recommend against there being more than one associated value per enum case.我个人强烈建议不要在每个枚举案例中使用多个关联值。 Associated values should be dead obvious (since they don't have arguments/names), and having more than one heavily muddies the water.关联的值应该是显而易见的(因为它们没有参数/名称),并且有一个以上的值会严重混淆水。

That said, it's something the language lets you do.也就是说,这是语言让你做的事情。 This allows you to have each case defined differently as well, if that was something you needed.如果这是您需要的东西,这也允许您对每个案例进行不同的定义。 Example:例子:

enum ErrorType {
    case teapot(String, Int)
    case skillet(UInt, [CGFloat])
}

Option 2 (better): Tuples!选项 2(更好):元组! And computed properties!和计算属性!

Tuples are a great feature of Swift because they give you the power of creating ad-hoc types.元组是 Swift 的一个很棒的特性,因为它们赋予你创建临时类型的能力。 That means you can define it in-line.这意味着您可以在线定义它。 Sweet!甜的!

If each of your error types are going to have a code and a description, then you could have a computed info property (hopefully with a better name?).如果您的每个错误类型都将有一个代码和一个描述,那么您可以拥有一个计算info属性(希望有一个更好的名称?)。 See below:见下文:

enum ErrorType {
    case teapot
    case skillet

    var info: (code: Int, description: String) {
        switch self {
        case .teapot:
            return (418, "Hear me shout!")
        case .skillet:
            return (326, "I'm big and heavy.")
        }
    }
}

Calling this would be much easier because you could use tasty, tasty dot syntax :调用它会容易得多,因为您可以使用美味、美味的点语法

let errorCode = myErrorType.info.code

I created a way of simulating this (No different than what Marcos Crispino suggested on his answer).我创建了一种模拟这个的方法(与 Marcos Crispino 在他的回答中建议的没有什么不同)。 Far from a perfect solution but allows us to avoid those nasty switch cases for every different property we want to get.远非完美的解决方案,但允许我们为我们想要获得的每个不同属性避免那些讨厌的开关案例。

The trick is to use a struct as the "properties/data" holder and using it as a RawValue in the enum itself.诀窍是使用结构作为“属性/数据”持有者,并将其用作枚举本身中的 RawValue。

It has a bit of duplication but it's serving me well so far.它有一些重复,但到目前为止它对我来说很好。 Every time you want to add a new enum case, the compiler will remind you to fill in the extra case in the rawValue getter, which should remind you to update the init?每次要添加新的枚举 case 时,编译器都会提醒您在 rawValue getter 中填写多余的 case,这应该提醒您更新init? which would remind you to create the new static property on the struct.这会提醒您在结构上创建新的静态属性。

Gist 要旨

Code to the Gist:代码要点:

enum VehicleType : RawRepresentable {

    struct Vehicle : Equatable {
        let name: String
        let wheels: Int

        static func ==(l: Vehicle, r: Vehicle) -> Bool {
            return l.name == r.name && l.wheels == r.wheels
        }

        static var bike: Vehicle {
            return Vehicle(name: "Bicycle", wheels: 2)
        }

        static var car: Vehicle {
            return Vehicle(name: "Automobile", wheels: 4)
        }

        static var bus: Vehicle {
            return Vehicle(name: "Autobus", wheels: 8)
        }
    }

    typealias RawValue = Vehicle

    case car
    case bus
    case bike

    var rawValue: RawValue {
        switch self {
        case .car:
            return Vehicle.car
        case .bike:
            return Vehicle.bike
        case .bus:
            return Vehicle.bus
        }
    }

    init?(rawValue: RawValue) {
        switch rawValue {
        case Vehicle.bike:
            self = .bike
        case Vehicle.car:
            self = .car
        case Vehicle.bus:
            self = .bus
        default: return nil
        }
    }
}

VehicleType.bike.rawValue.name
VehicleType.bike.rawValue.wheels
VehicleType.car.rawValue.wheels

VehicleType(rawValue: .bike)?.rawValue.name => "Bicycle"
VehicleType(rawValue: .bike)?.rawValue.wheels => 2
VehicleType(rawValue: .car)?.rawValue.name => "Automobile"
VehicleType(rawValue: .car)?.rawValue.wheels => 4
VehicleType(rawValue: .bus)?.rawValue.name => "Autobus"
VehicleType(rawValue: .bus)?.rawValue.wheels => 8

No, you cannot have multiple raw values associated with an enum.不,您不能有多个与枚举关联的原始值。

In your case, you could have the raw value to be equal to the code, and have an associated value with the description.在您的情况下,您可以使原始值等于代码,并具有与描述相关的值。 But I think the computed properties approach is the best option here.但我认为计算属性方法是这里的最佳选择。

One workaround if you wanted to have many static properties for a YourError could be to import a property list;如果您想为 YourError 拥有许多静态属性,一种解决方法可能是导入属性列表; you could set the root object to a dictionary, with your enum raw value as the key for each object, allowing you to easily retrieve static structured data for the object.您可以将根对象设置为字典,将您的枚举原始值作为每个对象的键,让您轻松检索对象的静态结构化数据。

This has an example of importing and using a plist: http://www.spritekitlessons.com/parsing-a-property-list-using-swift/这有一个导入和使用 plist 的示例: http : //www.spritekitlessons.com/parsing-a-property-list-using-swift/

That might be overkill for simply an error description, for which you could just use a hardcoded static function with a switch statement for your enum values, that returns the error string you need.对于简单的错误描述来说,这可能是矫枉过正,为此您可以只使用硬编码的静态函数和您的枚举值的 switch 语句,返回您需要的错误字符串。 Simply place the static function in the same .swift file as your enum.只需将静态函数放在与枚举相同的 .swift 文件中。

For instance,例如,

static func codeForError(error : YourErrorType) -> Int {
    switch(error) {
        case .Teapot:
            return "I'm a Teapot"
        case .Teacup:
            return "I'm a Teacup"
        ...
        default:
            return "Unknown Teaware Error"
    }
}

This has the benefit (compared to the .plist solution) of better accomodating localization.这具有更好地适应本地化的好处(与 .plist 解决方案相比)。 However, a .plist could just contain a key used for retrieving the proper localization, instead of a error string, for this purpose.但是,为此目的,.plist 可以只包含用于检索正确本地化的键,而不是错误字符串。

For beginning, assuming you want to store a code and a message, you can use a struct for RawValue首先,假设您要存储代码和消息,您可以对RawValue使用一个结构体

struct ErrorInfo {
    let code: Int
    let message: String
}

Next step is to define the enum as being RawRepresentable , and use ErrorInfo as the raw value:下一步是将枚举定义为RawRepresentable ,并使用ErrorInfo作为原始值:

enum MyError: RawRepresentable {
    typealias RawValue = ErrorInfo

    case teapot

What remains is to map between instances of MyError and ErrorInfo :剩下的就是在MyErrorErrorInfo实例之间进行映射:

static private let mappings: [(ErrorInfo, MyError)] = [
        (ErrorInfo(code: 418, message: "I'm a teapot"), .teapot)
    ]

With the above, let's build the full definition of the enum:有了上面的内容,让我们构建枚举的完整定义:

enum MyError: RawRepresentable {
    static private let mappings: [(ErrorInfo, MyError)] = [
    (ErrorInfo(code: 418, message: "I'm a teapot"), .teapot)
    ]

    case teapot

    init?(rawValue: ErrorInfo) {
        guard let match = MyError.mappings.first(where: { $0.0.code == rawValue.code && $0.0.message == rawValue.message}) else {
            return nil
        }
        self = match.1
    }

    var rawValue: ErrorInfo {
        return MyError.mappings.first(where: { $0.1 == self })!.0
    }
}

Some notes:一些注意事项:

  • you could use only the error code for matching, however this might result in inconsistent raw values if the messages differ您只能使用错误代码进行匹配,但是如果消息不同,这可能会导致原始值不一致
  • the amount of boilerplate code required to have raw values of some custom type might not outcome the benefits of using associated values.具有某种自定义类型的原始值所需的样板代码量可能不会产生使用关联值的好处。

Possible work around may to associate custom functions with enum可能的解决方法可能是将自定义函数与枚举相关联

 enum ToolbarType : String{
        case Case = "Case", View="View", Information="Information"
        static let allValues = [Case, View, Information]

        func ordinal() -> Int{
            return ToolbarType.allValues.index(of: self)!
        }
 }

Can be used as可以用作

 for item in ToolbarType.allValues {
        print("\(item.rawValue): \(item.ordinal())")
 }

Output输出

Case: 0
View: 1
Information: 2

Possibly you can have additional functions to associate enum type to different values可能你可以有额外的功能来将枚举类型关联到不同的值

This doesn't particularly answer your question, which was asking to find a better way than switch ing through self to look up the appropriate value but this answer may still be useful for someone looking in the future that needs a simple way to get a string from an enum which is defined as an integer type.这并没有特别回答您的问题,该问题要求找到一种比通过self进行switch以查找适当值更好的方法,但该答案可能对将来需要一种简单方法来获取字符串的人仍然有用来自定义为整数类型的枚举。

enum Error: UInt {
    case Teapot = 418
    case Kettle = 419

    static func errorMessage(code: UInt) -> String {
        guard let error = Error(rawValue: code) else {
            return "Unknown Error Code"
        }

        switch error {
        case .Teapot:
            return "I'm a teapot!"
        case .Kettle:
            return "I'm a kettle!"
        }
    }
}

This way, we can get the errorMessage two ways:这样,我们可以通过两种方式获取errorMessage:

  1. With an integer (eg. that was returned as an error code from a server)使用整数(例如,作为错误代码从服务器返回)
  2. With an enum value (the rawValue we define for the enum)使用枚举值(我们为枚举定义的rawValue

Option 1:选项1:

let option1 = Error.errorMessage(code: 418)
print(option1)  //prints "I'm a teapot!"

Option 2:选项 2:

let option2 = Error.errorMessage(code: Error.Teapot.rawValue)
print(option2)  //prints "I'm a teapot!"    

In modern versions of Swift it's possible to get the string value of an enum case label, even without that enum being declared with a : String rawValue.在现代版本的 Swift 中,即使没有使用: String rawValue 声明该枚举,也可以获得枚举 case 标签的字符串值。

How to get the name of enumeration value in Swift? 如何在 Swift 中获取枚举值的名称?

So there is no longer a need to define and maintain a convenience function that switches on each case to return a string literal.因此不再需要定义和维护一个方便的函数来打开每个 case 以返回一个字符串文字。 In addition, this works automatically for any enum, even if no raw-value type is specified.此外,即使未指定原始值类型,这也会自动适用于任何枚举。

This, at least, allows you to have "multiple raw values" by having both a real : Int rawValue as well as the string used as the case label.这至少允许您通过同时拥有一个 real : Int rawValue 以及用作 case 标签的字符串来拥有“多个原始值”。

I think it just tricky, and I have create my own idea like below:我认为这很棘手,我创建了自己的想法,如下所示:

enum Gender:NSNumber
{
    case male = 1
    case female = 0

    init?(strValue: String?) {
        switch strValue {
        case Message.male.value:
            self = .male
        case Message.female.value:
            self = .female
        default: return nil
        }
    }

    var strValue: String {
        switch self {
        case .male:
            return Message.male.value
        case .female:
            return Message.female.value
        }
    }
}

First of all, enums should only have one raw value.首先,枚举应该只有一个原始值。 However if you want to have something that can use multiple raw values... there is a way to 'hack' this, but you have to make it codable and hashable yourself, implement custom init's etc.但是,如果您想拥有可以使用多个原始值的东西……有一种方法可以“破解”它,但是您必须自己使其可编码和可散列,实现自定义初始化等。

enum MyCustomEnum: Codable, Hashable {

// duplicate every case with associated value of Codable.Type
case myFirstCase, _myFirstCase(Codable.Type)
case mySecondCase, _mySecondCase(Codable.Type)
case myThirdCase, _myThirdCase(Codable.Type)
case unknown(Any), _unknown(Codable.Type, Any) // handles unknown values

// define an allCases value to determine the only values your app 'sees'.
static var allCases: [Self] {
    return [
        .myFirstCase,
        .mySecondCase,
        .myThirdCase
        // unknown(String) // you can add unknown as well, but this is too mask any unknown values.
    ]
}

static func == (lhs: MyCustomEnum, rhs: MyCustomEnum) -> Bool {
    return lhs.stringValue == rhs.stringValue // can be either one of your custom raw values.
}

// add this per raw value. In this case one for Int and one for String
init(rawValue: Int) {
    guard let value = Self.allCases.first(where:{ $0.intValue == rawValue }) else {
        self = ._unknown(Int.self, rawValue)
        return
    }
    switch value {
    case .myFirstCase: self = ._myFirstCase(Int.self)
    case .mySecondCase: self = ._mySecondCase(Int.self)
    case .myThirdCase: self = ._myThirdCase(Int.self)
    default: self = ._unknown(Int.self, rawValue)
    }
}

init(rawValue: String) {
    guard let value = Self.allCases.first(where:{ $0.stringValue == rawValue }) else {
        self = ._unknown(String.self, rawValue)
        return
    }
    switch value {
    case .myFirstCase: self = ._myFirstCase(String.self)
    case .mySecondCase: self = ._mySecondCase(String.self)
    case .myThirdCase: self = ._myThirdCase(String.self)
    default: self = ._unknown(Int.self, rawValue)
    }
}

// add this per raw value. In this case one for Int and one for String
var intValue: Int {
    switch self {
    case .myFirstCase, ._myFirstCase(_): return 1
    case .mySecondCase, ._mySecondCase(_): return 2
    case .myThirdCase, ._myThirdCase(_): return 3
    case .unknown(let value), ._unknown(_, let value): return value as? Int ?? -1 // you can also choose to let intValue return optional Int.
    }
}

var stringValue: String {
    switch self {
    case .myFirstCase, ._myFirstCase(_): return "my first case"
    case .mySecondCase, ._mySecondCase(_): return "my second case"
    case .myThirdCase, ._myThirdCase(_): return "my third case"
    case .unknown(let value), ._unknown(_, let value): return value as? String ?? "not a String" // you can also choose to let stringValue return optional String.
    }
}

// determine the codable type using Mirror
private func getCodableType() -> Codable.Type? {
    let mirrorOfModuleType = Mirror.init(reflecting: self)
    guard let childOfModuleType = mirrorOfModuleType.children.first else { // no children, means no associated values.
        return nil
    }
    let value = childOfModuleType.value // can be either Codable.Type, String or (Codable.Type & String)
    if let rawValue = value as? Codable.Type {
        return rawValue
    } else {
        guard let rawValue = value as? (Codable.Type, String) else {
            // unknown(String), we don't know the rawValue as given, but try in this part of the code to guess what type fits best.
            if self.stringValue != "\(self.intValue)" { // e.g. "1" might match 1 but "1.0" and 1 don't match
                return String.self
            } else {
                return Int.self // return either a default value, or nil. It's your choice.
            }
        }
        return rawValue.0
    }
}

// confine to hashable using getCodableType
func hash(into hasher: inout Hasher) {
    if self.getCodableType() is String.Type {
        hasher.combine(self.stringValue)
    } else { // if you don't call hasher.combine at all, you can expect strange issues. If you do not know the type, choose one that is most common.
        hasher.combine(self.intValue)
    }
}

// confine to Decodable
init(from decoder: Decoder) throws {
    if let rawValue = try? Int.init(from: decoder) {
        self.init(rawValue: rawValue)
    } else if let rawValue = try? String.init(from: decoder) {
        self.init(rawValue: rawValue)
    } else {
        throw DecodingError.valueNotFound(Self.self, DecodingError.Context(codingPath: [], debugDescription: "no matching value was found"))
    }
}

// confine to Encodable using getCodableType
func encode(to encoder: Encoder) throws {
    let rawValue = self.getCodableType()
    if rawValue is String.Type {
        try self.stringValue.encode(to: encoder)
    } else if rawValue is Int.Type {
        try self.intValue.encode(to: encoder)
    } else {
        // getCodableType returns nil if it does not know what value it is. (e.g. myFirstCase without associated value) If you want to support this as well, you can encode using one of your rawValues to the encoder.
        throw EncodingError.invalidValue(Self.self, EncodingError.Context.init(codingPath: [], debugDescription: "this enum does not have a correct value", underlyingError: nil))
    }
}

} }

this code is scalable to any number of raw value as long as they are Codable此代码可扩展到任意数量的原始值,只要它们是可编码的

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM