将符合多个协议的类型转换为单个协议

Posted

技术标签:

【中文标题】将符合多个协议的类型转换为单个协议【英文标题】:Casting type conforming to multiple protocols as a single protocol 【发布时间】:2016-04-27 21:56:21 【问题描述】:

我很难让 Swift 理解符合两个协议的对象数组与符合其中一个协议的数组是一样的。

假设我有两个协议,可识别和可命名:

protocol Identifiable 
    var identifier: Int  get 


protocol Namable 
    var name: String  get 

还有两个函数将打印符合这些协议的对象数组的信息:

func printIdentifiers(itemsToPrint: [Identifiable]) 
    for (itemNumber, item) in itemsToPrint.enumerate() 
        print("\(itemNumber): \(item.identifier)")
    


func printNames(itemsToPrint: [Namable]) 
    for (itemNumber, item) in itemsToPrint.enumerate() 
        print("\(itemNumber): \(item.name)")
    

然后是两个符合这些协议的结构:

struct Friend: Identifiable, Namable 
    var identifier: Int
    var name: String


struct Dog: Identifiable, Namable 
    var identifier: Int
    var name: String

然后说我有一个符合这两个协议的项目数组:

let jeff = Friend(identifier: 232314, name: "Jeff")
let fido = Dog(identifier: 45678, name: "Fido")
let identifiableAndNamableItems: [protocol<Identifiable, Namable>] = [jeff, fido]

当我将jeff 分配给Namable 的变量时,Swift 没有问题:

let namableJeff: Namable = jeff //This is fine!

但是当我尝试这样做时,它吓坏了:

printNames(identifiableAndNamableItems)

无法将 [protocol] 类型的值转换为 预期的参数类型 [可命名]

知道为什么吗? Swift 直观地知道protocol&lt;Identifiable, Namable&gt; 类型的变量可以分配给Namable 类型的变量,因为任何符合两种协议的对象都必须只符合其中一种协议。但它不理解一个符合两个协议的项目数组可以分配给一个符合其中一个协议的项目数组。

【问题讨论】:

"但是它不理解符合两个协议的项目数组可以分配给符合其中一个协议的项目数组"正确,因为"array-of-items- that-c​​onform-to-a-protocol”实际上并不是一个成熟的类型。在这里查看我的问题的讨论:***.com/questions/33112559/… 【参考方案1】:

Swift 不能执行完整的 collection 类型转换(仅适用于某些幕后自动 Objective-C 可桥接对象,或在超类和子类元素的集合之间),其中 元素在一个可以分配给另一个的意义上是相关的。您需要明确帮助编译器显示逐元素转换是有效的,例如在调用 printNames 之前使用 .map 操作

printNames(identifiableAndNamableItems.map $0 )
    /* 0: Jeff
       1: Fido */

还请注意,您不必全力以赴地使用多种协议来查看此行为;这同样是显而易见的,例如以下更简单的示例

protocol Foo  
struct Bar: Foo 

let bar = Bar()
let foo: Foo = bar // ok

let barArr: [Bar] = [Bar(), Bar()]
let fooArr: [Foo] = barArr // cannot convert value of type '[Bar]' to specified type '[Foo]'
// let fooArr: [Foo] = barArr.map $0  // OK

【讨论】:

有趣。现在这是一个很好的解决方法,也许未来版本的编译器会对此更智能。我已经提交了雷达,以防万一:)【参考方案2】:

@dfri 和 @matt 都很好地说明了为什么这不起作用。这是因为隐式集合类型转换非常有限,Swift 讨厌在大多数情况下使用非具体类型。

我唯一要补充的是,与必须使用 map 手动转换类型相比,该问题的更具体的解决方案(双关语)是使用类型擦除,如 Rob demonstrates in his answer here。

这将允许您将protocol&lt;Identifiable, Namable&gt; 的非具体类型包装在一个新的具体类型AnyIdentifiableAndNamable 中(随意想出一个更吸引人的名称)。然后,您可以将此具体类型用于您的数组。

你会希望它看起来像这样:

struct AnyIdentifiableAndNamable:Identifiable, Namable 

    // your non-concrete typed base
    private let _base:protocol<Identifiable, Namable>

    // implement protocol properties to simply return the base's property
    var identifier: Int return _base.identifier
    var name: String return _base.name

    init<T:Identifiable where T:Namable>(_ base:T) 
        _base = base
    


let jeff = Friend(identifier: 232314, name: "Jeff")
let fido = Dog(identifier: 45678, name: "Fido")
let identifiableAndNamableItems = [AnyIdentifiableAndNamable(jeff), AnyIdentifiableAndNamable(fido)]

然后您只需修改您的打印函数以使用泛型。例如:

func printIdentifiers<T:Identifiable>(itemsToPrint: [T]) 
    for (itemNumber, item) in itemsToPrint.enumerate() 
        print("\(itemNumber): \(item.identifier)")
    


func printNames<T:Namable>(itemsToPrint: [T]) 
    for (itemNumber, item) in itemsToPrint.enumerate() 
        print("\(itemNumber): \(item.name)")
    

现在您无需执行任何转换即可将您的 [AnyIdentifiableAndNamable] 传递给 [T],因为 Swift 会为您推断类型。

【讨论】:

以上是关于将符合多个协议的类型转换为单个协议的主要内容,如果未能解决你的问题,请参考以下文章

Swift 同时将对象转换为类型和协议

无法将协议的通用关联类型的值转换为预期的参数类型

如何将 Class 对象转换为符合协议

http协议详解1

转换为协议默认方法的泛型类型

如何将符合协议的类声明为参数类型?