Swift 2:在带有关联值的枚举的switch语句中使用“default”的方法是否存在?

4

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

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

我想做的是形成一个好的字符串描述,其中包括所有非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 { $0 != nil }
        .map { $0! }
        .joinWithSeparator(", ")
}

问题在于七种可能中有五种完全相同,而我有一堆复制粘贴的代码,我认为这不好。如果我有一百种情况的枚举呢?
有没有办法写出类似这样的代码?
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))
}

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

3个回答

1

虽然我同意保罗的担忧,认为将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{$0.description}.filter{$0 != ""}.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 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{$0.description}.filter{$0 != ""}.joinWithSeparator(", ")
    }
}

这里的教训是枚举的第一个子元素是其所有数据的元组。
但使用 Mirror 更加脆弱(注意我打开了崩溃的可能性)。虽然枚举可能是一个很好的工具,但您仍然可能需要重新考虑此数据结构。

谢谢你,Rob!我明白你的意思了,看起来很清楚!我本来想尝试使用这些递归枚举,但是你和Paul说服我按照我原本打算的去做,并且我完全同意 - 使用更传统的数据结构会更好。非常感谢! - Denis Kim

1

对于现代读者来说,你现在可以绑定同一关联值的多个情况,只要类型匹配。

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

0

不,Swift的模式匹配没有办法匹配具有相同关联值的不同枚举值。这就是你立即问题的答案。

正如Rob所建议的那样,可以将重复的代码从case语句中重构出来,但是单个case语句无法跨枚举值匹配提取相关联的值。


如果你发现自己想要这样做,那么这表明你可能需要重新考虑枚举设计。许多情况之间存在共享的行为和共享的结构,但是枚举情况应该是互斥且独立的。

也许区域、城市、定居点、街道和房屋实际上都是同一种类型的东西?

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)“这些断言是否成立?”

其实,我本来想建议重新考虑在这里使用枚举。一开始看起来很酷,但显然它只是在妨碍事情的进行。 - matt
1
在二叉树中任意嵌套位置让我感到有些奇怪。例如,House(Region(Title("midwest")), City(Region(Title("subsaharan Africa")), House(Title("grandma’s house"))))是什么意思呢?我会从这里开始。 - Paul Cantrell

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