多次调度 Swift 尝试:通用参数 'T' 的参数冲突('Cat' 与 'Dog')

Multiple Dispatch Swift attempt: Conflicting arguments to generic parameter 'T' ('Cat' vs. 'Dog')

我正在观看关于 Julia 的关于多重调度的视频,并且很好奇我是否可以在 Swift 中编写类似的东西。我看到 Swift 依赖于 Julia 似乎在 运行 时间确定类型的编译器,但我也发现了一些我不理解的东西 Swift.

为什么下面的函数 encounters 在 2 个参数是同一类型的 Pet 时有效,但当一个是 Cat 另一个是 Dog 时却无效?

例如下面的两个函数工作

encounters(Arturo, Gabby) // both Cat
encounters(Cc, Bb) // both Dog

但是这些会导致编译器错误

Conflicting arguments to generic parameter 'T' ('Cat' vs. 'Dog')

encounters(Arturo, Bb) // Cat and Dog
encounters(Bb, Arturo) // Dog and Cat
protocol Pet: Equatable {
    var name: String { get }
}
struct Cat: Pet {
    let name: String
}
struct Dog: Pet {
    let name: String
}

let Arturo = Cat(name: "Arturo")
let Gabby = Cat(name: "Gabby")
let Bb = Dog(name: "Bb")
let Cc = Dog(name: "Cc")

func encounters<T: Pet>(_ a: T, _ b: T) {
    var verb: String
    switch (a, b) {
    case is (Cat, Dog):
        verb = meet(a as! Cat, b as! Dog)
    case is (Dog, Dog):
        verb = meet(a as! Dog, b as! Dog)
    case is (Cat, Cat):
        verb = meet(a as! Cat, b as! Cat)
    case is (Dog, Cat):
        verb = meet(a as! Dog, b as! Cat)
        
    default:
        fatalError()
    }
    
    print("\(a.name) meets \(b.name) and \(verb)")
}

func meet(_ a: Cat, _ b: Cat) -> String {
    return "Slinks"
}

func meet(_ a: Cat, _ b: Dog) -> String {
    return "Hisses"
}

func meet(_ a: Dog, _ b: Dog) -> String {
    return "Howles"
}

func meet(_ a: Dog, _ b: Cat) -> String {
    return "Barks"
}

如果您要创建一个类型符合协议的通用函数,那么您需要一直使用该协议,您不能在函数内部使用符合类型,因为可以创建任何自定义类型遵守协议。

这是一个仅使用协议的示例解决方案

protocol Pet {
    var name: String { get }
    var same: String { get }
    var other: String { get }
}

符合协议的猫狗

struct Cat: Pet {
    var name: String
    var other: String { "Slinks" }
    var same: String { "Hisses" }
}
struct Dog: Pet {
    let name: String
    var other: String { "Barks" }
    var same: String { "Howles" }
}

然后函数变成

func encounters<T: Pet, U: Pet>(_ a: T, _ b: U) {
    let verb: String
    if T.self == U.self {
        verb = a.same
    } else {
        verb = a.other
    }
    print("\(a.name) meets \(b.name) and \(verb)")
}

例子

let arturo = Cat(name: "Arturo")
let gabby = Cat(name: "Gabby")
let bb = Dog(name: "Bb")
let cc = Dog(name: "Cc")

encounters(arturo, bb)
encounters(bb, arturo)
encounters(gabby, arturo)
encounters(cc, bb)

Arturo meets Bb and Slinks
Bb meets Arturo and Barks
Gabby meets Arturo and Hisses
Cc meets Bb and Howles

我不会采用这种方法,但它不起作用的原因是你将 Equatable 附加到 Pet。您的意思可能是“宠物应该与其他宠物具有可比性”之类的意思,但那不是那个意思。这意味着符合 Pet 的类型本身必须是 Equatable。如果你删除 Equatable,这就像克劳斯所说的那样没有通用的写法:

protocol Pet { ... }
func encounters(_ a: Pet, _ b: Pet) { ... }

这么多 as! 让 Swift 开发者感到紧张,这是不必要的。 as! 没有错,但它为编译器无法捕获的简单错误打开了大门。这种风格让编译器帮助你多一点。它仍然无法捕获丢失的案例。

func encounters(_ a: Pet, _ b: Pet) {
    var verb: String
    switch (a, b) {
    case let (a as Cat, b as Dog):
        verb = meet(a, b)
    case let (a as Dog, b as Dog):
        verb = meet(a, b)
    case let (a as Cat, b as Cat):
        verb = meet(a, b)
    case let (a as Dog, b as Cat):
        verb = meet(a, b)

    default:
        fatalError()
    }

    print("\(a.name) meets \(b.name) and \(verb)")
}

我绝对不喜欢这里的fatalError。如果创建了一些新的 Pet,这段代码和我假设您编写的 Julia 都会崩溃。那不是很好。但我认为这不是这个问题的主要部分。在 Swift 中,您会在默认分支中放置一些内容。在 Julia 中,您将为 meet(a::Pet, b::Pet).

添加一个更通用的多方法

如果真的只有两种可能的宠物,那么您真的应该考虑枚举而不是动态调度,但我再次认为这不是问题所在。

就像我说的,我不太喜欢这种方法。我认为它非常冗长,并且增加了很多错误的地方。如果你想要这种动态类型查找,我会在数据中进行动态类型查找。

struct Meeting {
    let lhs: Pet.Type
    let rhs: Pet.Type
    let verb: String

    func matches(_ lhs: Pet, _ rhs: Pet) -> Bool {
        self.lhs == type(of: lhs) && self.rhs == type(of: rhs)
    }
}

let meetings = [
    Meeting(lhs: Cat.self, rhs: Dog.self, verb: "Hisses"),
    Meeting(lhs: Dog.self, rhs: Dog.self, verb: "Howles"),
    Meeting(lhs: Cat.self, rhs: Cat.self, verb: "Slinks"),
    Meeting(lhs: Dog.self, rhs: Cat.self, verb: "Barks"),
]

func encounters(_ a: Pet, _ b: Pet) {
    let verb = meetings
        .first(where: { [=12=].matches(a, b) })?.verb
        ?? "passes by"
    print("\(a.name) meets \(b.name) and \(verb)")
}

即使您需要函数,也可以将其扩展为将函数存储为数据(这是实现动态调度的一种非常强大的方式)。

不只是一个动词,而是向会议添加一个动作功能:

struct Meeting {
    let lhs: Pet.Type
    let rhs: Pet.Type
    let action: (Pet) -> String  // Function that takes a Pet and gives a String

    func matches(_ lhs: Pet, _ rhs: Pet) -> Bool {
        self.lhs == type(of: lhs) && self.rhs == type(of: rhs)
    }
}

现在它开始看起来更像您的 multi-methods,将所有专用逻辑放在一个地方:

let meetings = [
    Meeting(lhs: Cat.self, rhs: Dog.self, action: { "Hisses at \([=14=].name)" }),
    Meeting(lhs: Dog.self, rhs: Dog.self, action: { "Howles at \([=14=].name)" }),
    Meeting(lhs: Cat.self, rhs: Cat.self, action: { _ in "Slinks by" }),
    Meeting(lhs: Dog.self, rhs: Cat.self, action: { "Barks at \([=14=].name)" }),
]

并且encounters称它为:

func encounters(_ a: Pet, _ b: Pet) {
    let action = meetings
        .first(where: { [=15=].matches(a, b) })?.action
        ?? { _ in "passes by" }

    let verb = action(b)
    
    print("\(a.name) meets \(b.name) and \(verb)")
}

它并不完全像 multimethods 那样强大,但在 compile-time.

处更容易推理类型安全性