如何在switch语句之外访问Swift枚举的关联值

95
考虑:
enum Line {
    case    Horizontal(CGFloat)
    case    Vertical(CGFloat)
}

let leftEdge             =  Line.Horizontal(0.0)
let leftMaskRightEdge    =  Line.Horizontal(0.05)

我怎么能够直接访问 lefEdge 的关联值,而不使用 switch 语句呢?

let noIdeaHowTo          = leftEdge.associatedValue + 0.5

这甚至无法编译!

我看了一下这些SO问题,但似乎没有答案解决这个问题。

上面的 noIdeaHowTo 无法编译的代码行应该是那个一行代码,但由于 关联值(associated value) 可以是任何类型,我甚至无法看到用户代码如何在枚举本身中编写一个 "通用" 的获取或 associatedValue 方法。

最终,我得到了以下代码,但它很糟糕,并且每次添加 / 修改 case 都需要重新访问代码 ...

enum Line {
    case    Horizontal(CGFloat)
    case    Vertical(CGFloat)

    var associatedValue: CGFloat {
        get {
            switch self {
                case    .Horizontal(let value): return value
                case    .Vertical(let value): return value
            }
        }
    }
}

有指针的吗?


我认为你所做的是正确的(计算属性,也就是说)。枚举可以具有任何类型的关联值,更重要的是可以具有任意数量的关联值。我不明白苹果如何在不强制我们使用大量的“as?”和“if”语句的情况下提供对它们的快捷访问。 - 0x416e746f6e
顺便说一下,如果你的枚举每个情况都与CGFloat相关联,你可以考虑使用原始值而不是关联值。 - 0x416e746f6e
1
原始值是常量,它们必须是唯一的,即不能有两个具有不同值的Horizontal实例。 - Arkku
真。计算属性然后。 - 0x416e746f6e
2
在Swift 2中,您可以在if语句中执行此操作,这比switch更紧凑:if case .Horizontal(let value) = leftEdge { print("value = \(value)") } - vacawama
请查看Edwin的答案(适用于Swift 2和3):https://dev59.com/iF8e5IYBdhLWcg3wTI-L - BonanzaDriver
6个回答

119

正如其他人指出的那样,在Swift 2中现在有点可能:

import CoreGraphics

enum Line {
    case    Horizontal(CGFloat)
    case    Vertical(CGFloat)
}

let min = Line.Horizontal(0.0)
let mid = Line.Horizontal(0.5)
let max = Line.Horizontal(1.0)

func doToLine(line: Line) -> CGFloat? {
    if case .Horizontal(let value) = line {
        return value
    }
    return .None
}

doToLine(min) // prints 0
doToLine(mid) // prints 0.5
doToLine(max) // prints 1

6
你会如何实现这样一种方式,使其适用于任何枚举和任何情况? - Rodrigo Ruiz
2
@RodrigoRuiz 我建议你采用Arkku的答案。你需要一个包含枚举和值的结构体,而不仅仅是一个枚举。 - uliwitness
只是想指出:这与原始的 switch 解决方案本质上没有任何区别,也就是说,你可以将那个 if case 重写为一个 switch,并将 default case 设置为 return .none,以获得完全相同的结果。这两种解决方案都需要你分别添加对不同情况的支持,并且不能提供一种通用的方式来访问“任何情况”的相关值。我仍然强烈建议使用包含纯“类型”枚举和值的 structclass 来解决一般问题。 - Arkku
我在使用Swift 5时遇到了语法错误“模式无法与'EnumName'类型的值匹配”。 - androidguy

34
您可以使用 guard 语句来访问关联值,就像这样。
enum Line {
    case    Horizontal(Float)
    case    Vertical(Float)
}

let leftEdge             =  Line.Horizontal(0.0)
let leftMaskRightEdge    =  Line.Horizontal(0.05)

guard case .Horizontal(let leftEdgeValue) = leftEdge else { fatalError() }

print(leftEdgeValue)

16

我认为你可能试图将enum用于其不打算用于的用途。访问相关值的方法确实是通过switch,就像你所做的那样,其思想是switch始终处理enum的每个可能成员情况。

enum的不同成员可以有不同的相关值(例如,你可以在你的enum Line中拥有Diagonal(CGFloat,CGFloat)Text(String)),因此你必须在访问相关值之前先确认你正在处理哪种情况。例如,请考虑:

enum Line {
    case Horizontal(CGFloat)
    case Vertical(CGFloat)
    case Diagonal(CGFloat, CGFloat)
    case Text(String)
}
var myLine = someFunctionReturningEnumLine()
let value = myLine.associatedValue // <- type?
你怎么能假定从myLine中获取关联值的类型只可能是CGFloatString或两个CGFloat呢?这就是为什么你需要使用switch语句,以便先确定所处的case
在你的情况下,你可能最好使用一个classstruct来表示Line,它可以存储CGFloat值,并具有VerticalHorizontalenum属性。或者你可以将VerticalHorizontal建模为独立的类,Line则成为一个协议(例如)。

2
是的,我认为这需要语言支持。对于这种情况,返回值不仅应该考虑不同类型,而且还应该考虑不同的元数。可能会有一个不同数量的元组,例如:case x ... return (1) case y ... return (0, "stuff") case z ... return (-1, 3.14, ["for", "good" ,"measure"]). 可能最好由编译器直接支持这个功能。嗅探雷达正在靠近... - verec
@verec 我认为对于语言支持的正确答案应该是保持关联值不变,但允许将存储属性与枚举分开添加。也就是说:enum Line { case horizontal, vertical; var value: CGFloat } - 基本上是用于生成 struct 的语法糖,这目前是最好的解决方案。如果关联值和存储属性被彼此排斥,那么使用 init(value: CGFloat) 将看起来像 var line: Line = .horizontal(value: 1) - Arkku
我认为你可能会对这个问题感兴趣:https://stackoverflow.com/q/75903676/4423545 不是因为某些用例,而是出于对该主题的理论兴趣。请查看我的答案,你会发现我们能够处理 CGFloatString 或两个 CGFloat。虽然这些代码不适合在生产中使用,但仍然很有趣,因为它们可以处理不同类型 :) - Andrew_STOP_RU_WAR_IN_UA

8
你可以使用if case let语法来获取关联值,而无需使用switch语句:
enum Messages {
    case ping
    case say(message: String)
}

let val = Messages.say(message: "Hello")

if case let .say(msg) = val {
    print(msg)
}

如果枚举值是 .say,if case let 块中的内容将会执行,并且关联值将作为你在 if 语句中使用的变量名而存在。


6

为什么这不可能已经有答案了,所以这只是一个建议。为什么不像这样实现它呢?我的意思是枚举和结构体都是值类型。

enum Orientation {
    case Horizontal
    case Vertical
}

struct Line {

    let orientation : Orientation
    let value : CGFloat

    init(_ orientation: Orientation, _ value: CGFloat) {

        self.orientation = orientation
        self.value = value
    }
} 

let x = Line(.Horizontal, 20.0)

// if you want that syntax 'Line.Horizontal(0.0)' you could fake it like this

struct Line {

    let orientation : Orientation
    let value : CGFloat

    private init(_ orientation: Orientation, _ value: CGFloat) {

        self.orientation = orientation
        self.value = value
    }

    static func Horizontal(value: CGFloat) -> Line { return Line(.Horizontal, value) }
    static func Vertical(value: CGFloat) -> Line { return Line(.Vertical, value) }
}

let y = Line.Horizontal(20.0)

3

使用Swift 2,可以通过反射获取关联值(只读)。

要使此过程更加简便,请将以下代码添加到您的项目中,并使用EVAssociated协议扩展您的枚举。

    public protocol EVAssociated {
    }

    public extension EVAssociated {
        public var associated: (label:String, value: Any?) {
            get {
                let mirror = Mirror(reflecting: self)
                if let associated = mirror.children.first {
                    return (associated.label!, associated.value)
                }
                print("WARNING: Enum option of \(self) does not have an associated value")
                return ("\(self)", nil)
            }
        }
    }

然后,您可以使用以下代码访问相关值:
    class EVReflectionTests: XCTestCase {
            func testEnumAssociatedValues() {
                let parameters:[EVAssociated] = [usersParameters.number(19),
usersParameters.authors_only(false)]
            let y = WordPressRequestConvertible.MeLikes("XX", Dictionary(associated: parameters))
            // Now just extract the label and associated values from this enum
            let label = y.associated.label
            let (token, param) = y.associated.value as! (String, [String:Any]?)

            XCTAssertEqual("MeLikes", label, "The label of the enum should be MeLikes")
            XCTAssertEqual("XX", token, "The token associated value of the enum should be XX")
            XCTAssertEqual(19, param?["number"] as? Int, "The number param associated value of the enum should be 19")
            XCTAssertEqual(false, param?["authors_only"] as? Bool, "The authors_only param associated value of the enum should be false")

            print("\(label) = {token = \(token), params = \(param)")
        }
    }

    // See http://github.com/evermeer/EVWordPressAPI for a full functional usage of associated values
    enum WordPressRequestConvertible: EVAssociated {
        case Users(String, Dictionary<String, Any>?)
        case Suggest(String, Dictionary<String, Any>?)
        case Me(String, Dictionary<String, Any>?)
        case MeLikes(String, Dictionary<String, Any>?)
        case Shortcodes(String, Dictionary<String, Any>?)
    }

    public enum usersParameters: EVAssociated {
        case context(String)
        case http_envelope(Bool)
        case pretty(Bool)
        case meta(String)
        case fields(String)
        case callback(String)
        case number(Int)
        case offset(Int)
        case order(String)
        case order_by(String)
        case authors_only(Bool)
        case type(String)
    }

上面的代码来自我的项目https://github.com/evermeer/EVReflection https://github.com/evermeer/EVReflection

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