【问题标题】:Multiple Dispatch Swift attempt: Conflicting arguments to generic parameter 'T' ('Cat' vs. 'Dog')多个 Dispatch Swift 尝试:泛型参数“T”的参数冲突(“Cat”与“Dog”)
【发布时间】:2020-10-11 13:51:10
【问题描述】:

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

为什么下面的函数encounters 会在两个参数属于同一类型的 Pet 时工作,但在一个是 Cat 而另一个是 Dog 时却不行?

例如下面的两个函数起作用

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

但这些会导致编译器错误

泛型参数“T”的参数冲突(“Cat”与“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"
}

【问题讨论】:

  • 在这个例子中没有必要使用泛型,如果你要与显式类型进行比较,你可以简单地使用func encounters(_ a: Pet, _ b: Pet)
  • @ClausJørgensen 我确实尝试过,但我收到错误Protocol 'Pet' can only be used as a generic constraint because it has Self or associated type requirements
  • 那是因为您在协议中添加了:Equatable,这在大多数情况下并不是您的意思。这意味着符合的类型本身必须是 Equatable 的。这并不意味着 Pet 本身就是 Equatable。协议不能符合其他协议,因此 Pet 不可能是 Equatable。无论如何,您在此代码中不依赖 Equatable。
  • 请注意,需要添加fatalError() 默认值是一个线索,表明您在这里走错了路并与语言作斗争。在某些情况下fatalError 是合适的,但在这种情况下,它表明您已经在类型系统之外徘徊,并且正在运行时检查预期在编译时检查的内容。如果您添加了 Snake 类型的 Pet(例如,可以在完全不同的模块中添加),此代码将崩溃。

标签: swift generics protocols multiple-dispatch


【解决方案1】:

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

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

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 遇到 Bb 和 Slinks
Bb 遇见 Arturo 和 Barks
加比遇见阿图罗和希斯
Cc 遇见 Bb 和 Howles

【讨论】:

  • 感谢 Joakim,这种方法唯一的好处是,如果我们添加更多动物,“其他”可能需要根据动物而有所不同。
  • 例如,如果一只猫在吃老鼠时发出的声音与它遇到狗时发出的声音不同,那么您需要为每个带有 (mouse: Mouse) 和 (dog: Dog) 参数的特定方法分别或更通用(宠物:宠物),但这个需要包含所有动物类型,它们都不是通用的
【解决方案2】:

我不会采用这种方法,但它不起作用的原因是因为您将 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) 添加更通用的多方法。

如果真的只有两个可能的 Pets,那么你真的应该考虑枚举而不是动态调度,但我再次认为这不是问题。

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

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: { $0.matches(a, b) })?.verb
        ?? "passes by"
    print("\(a.name) meets \(b.name) and \(verb)")
}

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

不只是一个动词,给 Meeting 添加一个动作函数:

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

现在它开始看起来更像您的多方法,将所有专门的逻辑放在一个地方:

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

encounters 称它为:

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

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

它并不完全像多方法那样强大,但它更容易在编译时推断类型安全。

【讨论】:

  • 谢谢罗伯!我同意,在 switch 语句中输入所有内容并使用 fatalError 肯定让人不舒服。在 Julia 中,不需要 switch 语句或类似的东西,它能够确定哪些宠物在参数中(我假设在运行时)并为适当的动物使用正确的重载方法。在 Julia 中添加新动物非常容易,因为您只需要创建另一种动物类型,然后为该动物创建另一个方法重载。但我喜欢您在数据方法中查找的动态类型。可惜 Swift 不支持多分派!
  • 在 Julia 中,至少在我所有的实验中,如果你添加另一个 Pet 并且未能添加调度函数,它会在运行时崩溃。它基本上有一个隐含的fatalError。你只是看不到它。这是主要的区别,Swift 积极试图避免的事情。在 Julia 中,您可以添加一个默认的 Pet 多方法,但如果您不这样做,它就不会捕获它。 (请注意,Julia 非常适合交互式开发,崩溃并不是什么大问题。你看到它,你修复它,继续前进。但是 iOS 应用程序中的崩溃更难发现,更不用说修复了。 )
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-12-18
  • 2018-05-21
  • 2017-10-22
  • 1970-01-01
  • 1970-01-01
  • 2020-08-22
  • 1970-01-01
相关资源
最近更新 更多