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

32
我希望将两个原始值与枚举实例关联起来(想象一个表示错误类型的枚举,我希望Error.Teapot有一个Int类型属性code,其值为418,并且一个字符串属性设置为I'm a teapot)。
请注意这里原始值关联值之间的区别 - 我希望所有Teapot实例都具有418的code,而不是每个Teapot实例都有唯一的关联值。
是否有比添加计算属性到枚举更好的方法,可以在switch中使用self查找适当的值?
11个回答

28

您有几个选择,但都不包括原始值。原始值并不是完成此任务的正确工具。

选项1(一般):关联值

我个人强烈建议每个枚举情况最多只有一个关联值。关联值应该非常明显(因为它们没有参数/名称),而且拥有多个关联值会使事情变得混乱不清。

尽管如此,这是语言能够让您做的事情。如果需要的话,这允许您不同地定义每个情况。例如:

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

选项2(更好):元组!以及计算属性!

元组是Swift的一个很棒的特性,因为它们让你具有创建即席类型的能力。这意味着你可以在一行内定义它。很棒!

如果你的每个错误类型都将拥有一个代码和描述,那么你可以使用计算属性info(希望使用更好的名称?)。请参见下面:

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.")
        }
    }
}

使用这个方法会更加容易,因为你可以使用美味的、美味的点语法:

let errorCode = myErrorType.info.code


谢谢。虽然让Swift允许枚举拥有常量成员变量仍然更好,但这仍然是一个不错的解决方法,尽管会带来一些运行时开销。 - Cheok Yan Cheng
看起来他们对实现这种在像Java这样的语言中非常普遍的功能没有兴趣,很遗憾。- https://forums.swift.org/t/proposal-stored-properties-for-enums/378/20 - Cheok Yan Cheng
@CheokYanCheng 我不能说我同意那将是一个好选择。我确定在Java中这是有道理的,但我不认为它适合Swift的设计哲学。话虽如此,你可以创建一个对象,其中包含枚举作为属性和其他属性并存。这对我来说更有意义。 - jakehawken

20
不,枚举不能有多个原始值 - 它必须是一个单一的值,实现了 Equatable 协议,并且可以像文档描述的那样进行文字转换。文档
我认为在您的情况下,最好的方法是将错误代码用作原始值,并使用由预填充的静态字典支持的属性,其中错误代码是键,文本是值。

3
我创造了一种模拟方法(与Marcos Crispino在他的回答中提出的方法没有区别)。这并不是完美的解决方案,但可以避免我们为每个不同的属性编写繁琐的switch case。
诀窍是使用结构体作为“属性/数据”持有者,并将其用作枚举本身的RawValue。
它有一些重复,但到目前为止它为我服务得很好。每次您想要添加新的枚举情况时,编译器都会提醒您在rawValue getter中填写额外的情况,这应该提醒您更新init?,这将提醒您在结构体上创建新的静态属性。 Gist 代码见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

2
你已经在使用 switch,并且使用频率是之前的两倍;此外,你还增加了两个额外级别的属性。这意味着有更多的代码需要维护和解析。 - green_knight
3
是的,对于两种属性,没有赢家。胜利发生在此之后。如果你有10个属性(这可能是一个问题,但只是为了说明一点),你不需要额外的开关。 :) - Nuno Gonçalves
1
但是,是的,我同意这很“糟糕”。 :D - Nuno Gonçalves

2

如果您希望为YourError添加许多静态属性,一种解决方法是导入一个属性列表; 您可以将根对象设置为字典,并以您的枚举原始值作为每个对象的键,从而使您可以轻松检索对象的静态结构化数据。

以下示例演示了导入和使用plist:http://www.spritekitlessons.com/parsing-a-property-list-using-swift/

对于仅需使用硬编码静态函数和switch语句返回所需错误字符串的错误描述来说,这可能有些繁琐。只需将静态函数放置在与枚举相同的.swift文件中即可。

例如:

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"
    }
}

与.plist解决方案相比,这种方法的好处在于更好地适应本地化。但是,一个.plist文件可以只包含用于检索正确本地化的密钥,而不是一个错误字符串。

2

不,枚举类型不能有多个原始值。

在您的情况下,您可以将原始值设置为代码,并使用描述关联值。但是我认为在这里使用计算属性方法是最佳选项。


一个与该描述相关联的值并不适合我想要的。所有“Teapot”实例应该具有相同的字符串;如果我想要附加例如特定错误实例来源的注释,则会使用关联的String值。 - Robert Atkins

1
可能的解决方法是将自定义函数与枚举关联起来。
 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)!
        }
 }

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

输出

Case: 0
View: 1
Information: 2

也许您可以添加额外的功能,将枚举类型与不同的值关联起来。

重构后的方式:var index: Int { return Self.allCases.firstIndex(of: self)! } - Abhishek Thapliyal

1

首先,假设您想存储代码和消息,您可以使用一个结构体RawValue

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

下一步是将枚举定义为RawRepresentable,并使用ErrorInfo作为原始值:
enum MyError: RawRepresentable {
    typealias RawValue = ErrorInfo

    case teapot

需要做的是将 MyErrorErrorInfo 的实例进行映射:
static private let mappings: [(ErrorInfo, MyError)] = [
        (ErrorInfo(code: 418, message: "I'm a teapot"), .teapot)
    ]

有了上面的内容,让我们构建枚举的完整定义:

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
    }
}

一些注意事项:

  • 您可以仅使用错误代码进行匹配,但如果消息不同,则可能导致不一致的原始值。
  • 为了获得某些自定义类型的原始值,所需的样板代码量可能不如使用关联值带来的好处。

0
这并没有特别回答你的问题,你的问题是要找到一种比通过 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!"
        }
    }
}

这样,我们可以通过两种方式获取错误消息:

  1. 使用整数(例如从服务器返回的错误代码)
  2. 使用枚举值(我们为枚举定义的rawValue

选项1:

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

选项2:

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

0

首先,枚举应该只有一个原始值。但是如果你想要有一些可以使用多个原始值的东西... 有一种“黑客”方法可以实现这一点,但你必须自己实现可编码和可哈希化,实现自定义的初始化等。

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))
    }
}

}

只要它们是可编码的,这段代码就可以扩展到任意数量的原始值


0
在现代版本的Swift中,即使没有使用: String rawValue声明枚举,也可以获取枚举案例标签的字符串值。 如何在Swift中获取枚举值的名称?

因此,不再需要定义和维护一个方便函数来切换每个案例以返回字符串文字。此外,这适用于任何枚举,即使未指定原始值类型。

至少这样可以通过同时拥有真正的: Int rawValue和用作案例标签的字符串来实现“多个原始值”。

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