Swift 2:有没有办法在enum的switch语句中使用'default'关联值?

Swift 2: Is there any way to use 'default' in the switch statement of enum with associated values?

我有一个递归枚举,其中大多数情况具有相同类型的关联值:

indirect enum Location {
    case Title(String?)
    case Region(Location)
    case Area(Location, Location)
    case City(Location, Location)
    case Settlement(Location, Location)
    case Street(Location, Location)
    case House(Location, Location)
}

我想做的是形成一个漂亮的字符串描述,其中将包括所有 non-nil 个标题。

func getStringFromLocation(location: Location) -> String? {
    var parts: [String?] = []

    switch location {
    case .Title(let title): return title
    case .House(let title, let parent):
        parts.append(getStringFromLocation(parent))
        parts.append(getStringFromLocation(title))
    case .Street(let title, let parent):
        parts.append(getStringFromLocation(parent))
        parts.append(getStringFromLocation(title))
    case .Settlement(let title, let parent):
        parts.append(getStringFromLocation(parent))
        parts.append(getStringFromLocation(title))
    case .City(let title, let parent):
        parts.append(getStringFromLocation(parent))
        parts.append(getStringFromLocation(title))
    case .Area(let title, let parent):
        parts.append(getStringFromLocation(parent))
        parts.append(getStringFromLocation(title))
    case .Region(let title):
        parts.append(getStringFromLocation(title))
    }

    return parts
        .filter { [=13=] != nil }
        .map { [=13=]! }
        .joinWithSeparator(", ")
}

问题是七种可能的情况中有五种完全相同,而且我有一堆 copy-pasted 代码,我认为这不是很好。如果我有一百个案例的枚举怎么办?

有什么办法可以写成这样吗?

switch location {
case .Title(let title): 
    parts.append(title)
case .Region(let title):
    parts.append(getStringFromLocation(title))
default (let title, let parent):
    parts.append(getStringFromLocation(parent))
    parts.append(getStringFromLocation(title))
}

...使用一些默认情况来处理所有类似情况?

不,Swift 的模式匹配无法匹配恰好具有相同关联值的不同枚举值。这就是您的直接问题的答案。

正如 Rob 明智地建议的那样,从 case 语句中重构重复的代码是可能的——但是单个 case 语句不可能匹配枚举值 and extract关联值。


您发现自己想要这样做的事实表明您可能需要重新考虑您的枚举设计。许多案例之间存在共享行为和共享结构,但枚举案例应该是相互排斥和独立的。

难道Area, City, Settlement, Street, House 真的是一类东西吗?

indirect enum Location {
    case Title(String?)
    case Region(Location)
    case BinaryLocation(BinaryKind, Location, Location)

    enum BinaryKind {
        case Area
        case City
        case Settlement
        case Street
        case House
    }
}

(我不明白这两个关联位置的含义,但既然你明白了,我建议你想出比 BinaryLocationBinaryKind 更能说明问题的名称。)

这甚至可能根本不适合枚举;例如,这样的事情可能会更好:

protocol Location {
    var description: String { get }
}

struct Title: Location {
    var title: String?

    var description: String {
        return title
    }
}

// ... and one for Region, and then ...

protocol BinaryLocation {
    var child0: Location { get }
    var child1: Location { get }
}

extention BinaryLocation: Location {
    var description: String {
        return "\(child0), \(child1)"
    }
}

// ...and then either individual structs for House, Street, etc., or
// an enum like BinaryKind above.

我不能说,因为我不了解你的全部情况。

可以说的是,您对重复代码的担忧是有道理的,而这种具体的担忧是退后一步并审视您的大局的线索模型。以挑剔的类型严格性问题为线索来重新考虑您的建模选择是“Swift 方式:”

  1. “为什么我会收到这个错误/撞到墙上?”变成……
  2. “编译器为什么这么想?”变成……
  3. (a)“我在我的模型中关于我的问题域结构的断言是什么?” (b) “这些说法站得住脚吗?”

虽然我同意 Paul 的担忧,即以这种方式嵌套 Location 很奇怪,但基本问题是可以解决的。就个人而言,我不会用 default 来解决它,我只是简化代码并使用 Swift 给我们的工具(比如 CustomStringConvertible;我还在你的数据上贴上标签;只有两个具有完全不同含义的 Location 元素太混乱了):

indirect enum Location: CustomStringConvertible {
    case Title(String?)
    case Region(Location)
    case Area(title: Location, parent: Location)
    case City(title: Location, parent: Location)
    case Settlement(title: Location, parent: Location)
    case Street(title: Location, parent: Location)
    case House(title: Location, parent: Location)

    var description: String {

        func format(locs: (Location, Location)) -> String {
            return [locs.0, locs.1].map{[=10=].description}.filter{[=10=] != ""}.joinWithSeparator(", ")
        }

        switch self {
        case .Title(let title): return title ?? ""

        case .Region(let title): return "\(title)"

        case .House(let data):      return format(data)
        case .Street(let data):     return format(data)
        case .Settlement(let data): return format(data)
        case .City(let data):       return format(data)
        case .Area(let data):       return format(data)
        }
    }
}

请注意我是如何将整个元组卸载到 data 中的。您不必在模式匹配中将元组分开。枚举永远不会有多个关联数据。他们总是只有一个:一个元组。 (函数也是如此。所有函数都取一个值,return一个值。那个值可能恰好是一个元组。)

但是如果你真的想摆脱那个重复的return format(data),那么你可以通过Mirror。 (你可以通过Mirror解决相当多的事情。你在做之前应该非常小心。这种情况只是重复输入,而不是重复逻辑。一点点重复输入不是你应该创造很多的东西要删除的复杂性。)

您可以这样做:

var description: String {
    switch self {
    case .Title(let title): return title ?? ""

    case .Region(let title): return "\(title)"

    default:
        let m = Mirror(reflecting: self)
        guard let locs = (m.children.first?.value as? (Location, Location)) else {
            preconditionFailure("Unexpected data in enum. Probably missing a case somewhere.")
        }
        return [locs.0, locs.1].map{[=11=].description}.filter{[=11=] != ""}.joinWithSeparator(", ")
    }
}

这里的教训是,枚举的第一个子项是其所有数据的元组。

但是使用Mirror要脆弱得多(注意我打开了崩溃的可能性)。虽然枚举在这里可能是一个很好的工具,但您仍然可能需要重新考虑这种数据结构。

对于这个老问题的现代读者来说,现在您实际上可以绑定来自多个案例的相同关联值。只要类型匹配。

case .house(let title, let parent), .street(let title, let parent):
    parts.append(getStringFromLocation(parent))
    parts.append(getStringFromLocation(title))