Swift泛型强制误解 [英] Swift generic coercion misunderstanding

查看:78
本文介绍了Swift泛型强制误解的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我使用



但是我可以在没有任何编译器错误的情况下编写下一行:

  var arrays = Array< Array< BaseProtocol>>()
let arrayOfChild = Array< ChildClass>()
arrays.append(arrayOfChild)



那么,通用Swift数组和泛型Signal有什么区别呢?

Array (和 Set )和 Dictionary )从t中获得特殊待遇他编译器,允许协变(我在这个Q& A的 中稍微详细地介绍了这一点)。



然而,任意的泛型类型是不变的 - 因此在类型系统中, Signal< ChildClass> 和<$ c即使 ChildClass 可转换为<$ c> $ c> Signal< BaseProtocol> 完全不相关的类型$ c> BaseProtocol (另请参阅本Q& A )。



这样做的一个原因是它会完全破坏泛型引用类型,它们定义了关于 T 。



例如,如果您已将 Signal 执行为:

  class Signal< T> {

var t:T

init(t:T){
self.t = t
}
}

如果您可以说:

  let signalInt = Signal(t:5)

let signalAny:Signal< Any> = signalInt

您可以这样说:

  signalAny.t =wassup

完全错误,因为您不能将字符串分配给 Int 属性。

Array 这种东西对于安全的原因在于它是一种值类型 - 因此当您执行:

  let intArray = [2,3,4] 

var anyArray:[Any] = intArray
anyArray.append(wassup)

没有问题,因为 anyArray intArray copy - 因此 append( _:)不是问题。



然而,这不能应用于任意的通用值类型,因为值类型可以包含任意数量的通用引用类型,这导致我们退回到允许非法操作定义反变化事物的通用引用类型的危险之路。




正如Rob在他的回答中所说,引用类型的解决方案,如果您需要维护对相同基础的引用例如,是使用类型橡皮擦。



如果我们考虑这个例子:

  protocol BaseProtocol {} 
class ChildClass:BaseProtocol {}
类AnotherChild:BaseProtocol {}

class Signal< T> {
var t:T

init(t:T){
self.t = t
}
}

让childSignal = Signal(t:ChildClass())
让anotherSignal = Signal(t:AnotherChild())

包装任何 Signal< T> 实例的类型橡皮擦,其中 T 符合 BaseProtocol 可能如下所示:

  struct AnyBaseProtocolSignal {
private let _t:() - > BaseProtocol

var t:BaseProtocol {return _t()}

init< T:BaseProtocol>(_ base:Signal< T>){
_t = { base.t}
}
}

// ...

let signals = [AnyBaseProtocolSignal(childSignal),AnyBaseProtocolSignal(anotherSignal)]

现在我们可以通过异信号来讨论Signal 其中 T 是符合 BaseProtocol 的某种类型。



然而,这个包装的一个问题是,我们仅限于使用 BaseProtocol 进行交谈。如果我们有 AnotherProtocol 并且想为 Signal 其中 T 符合 AnotherProtocol



解决方法是传递转换函数到type-eraser,允许我们执行一个任意的upcast。

  struct AnySignal< ; T> {
private let _t:() - > T(b):信号< U> ;,变换:@escaping(U) - > T){
_t = {transform(base.t)}
}
}

现在我们可以讨论 Signal的异构类型Signal 其中 T 是某种类型的可转换到一些 U ,这是在创建类型橡皮擦时指定的。

 让信号:[AnySignal< BaseProtocol>] = [
AnySignal(childSignal,transform:{$ 0}),
AnySignal(anotherSignal,transform:{$ 0})
//或AnySignal(childSignal,transform:{$ 0 as BaseProtocol})
//是显式的。
]

然而,传递相同的 transform

在Swift 3.1中(可用于Xcode 8.3测试版),您可以通过定义来自调用者的负担来减轻这种负担你自己的初始化程序专门用于 BaseProtocol 的扩展:

 扩展AnySignal where T == BaseProtocol {

init< U:BaseProtocol>(_ base:Signal< U>){
self.init(base,transform:{$ 0})
}

$ / code>

(并且对于要转换的任何其他协议类型重复) p>

现在您可以说:

 让信号:[AnySignal< BaseProtocol> ;] = [
AnySignal(childSignal),
AnySignal(anotherSignal)
]

(你实际上可以在这里删除数组的显式类型注释,编译器会推断它是 [AnySignal< BaseProtocol>] - 但是如果你打算允许更多的便利初始化者,我会保持它的明确性)






值类型或引用类型的解决方案,您希望专门创建新实例,执行转换 Signal< T> (其中 T 符合 BaseProtocol Signal 类型的扩展中的初始化,其中 T == BaseProtocol

 扩展名Signal其中T == BaseProtocol {
convenience init< T:BaseProtocol>(other:Signal< T>){
self.init t:other.t)
}
}

// ...

让信号:[Signal< BaseProtocol>] = [
Signal(other:childSignal),
Signal(other:anotherSignal)
]



 Swift 3.1,可以通过实例方法实现: 

 扩展Signal其中T:BaseProtocol {
func asBaseProtocol() - >信号u BaseProtocol> {
return Signal< BaseProtocol>(t:t)
}
}

// ...

让信号:[ Signal< BaseProtocol>] = [
childSignal.asBaseProtocol(),
anotherSignal.asBaseProtocol()
]

这两种情况下的过程对于 struct 都是相似的。


I'm using Signals library.

Let's say I defined BaseProtocol protocol and ChildClass which conforms BaseProtocol.

protocol BaseProtocol {}
class ChildClass: BaseProtocol {}

Now I want to store signals like:

var signals: Array<Signal<BaseProtocol>> = []
let signalOfChild = Signal<ChildClass>()
signals.append(signalOfChild)

I get error:

But I can write next lines without any compiler error:

var arrays = Array<Array<BaseProtocol>>()
let arrayOfChild = Array<ChildClass>()
arrays.append(arrayOfChild)

So, what the difference between generic Swift Array and generic Signal?

解决方案

The difference is that Array (and Set and Dictionary) get special treatment from the compiler, allowing for covariance (I go into this in slightly more detail in this Q&A).

However, arbitrary generic types are invariant – therefore in the eyes of the type-system, Signal<ChildClass> and Signal<BaseProtocol> are seen as completely unrelated types, even though ChildClass is convertible to BaseProtocol (see also this Q&A).

One reason for this is it would completely break generic reference types that define contravariant things (such as method parameters and property setters) with respect to T.

For example, if you had implemented Signal as:

class Signal<T> {

    var t: T

    init(t: T) {
        self.t = t
    }
}

If you were able to say:

let signalInt = Signal(t: 5)

let signalAny: Signal<Any> = signalInt

you could then say:

signalAny.t = "wassup"

which is completely wrong, as you cannot assign a String to an Int property.

The reason why this kind of thing is safe for Array is that it's a value type – thus when you do:

let intArray = [2, 3, 4]

var anyArray : [Any] = intArray
anyArray.append("wassup")

there are no problems, as anyArray is a copy of intArray – thus the contravariance of append(_:) is not a problem.

However, this cannot be applied to arbitrary generic value types, as value types can contain any number of generic reference types, which leads us back down the dangerous road of allowing an illegal operation for generic reference types that define contravariant things.


As Rob says in his answer, the solution for reference types, if you need to maintain a reference to the same underlying instance, is to use a type-eraser.

If we consider the example:

protocol BaseProtocol {}
class ChildClass: BaseProtocol {}
class AnotherChild : BaseProtocol {}

class Signal<T> {
    var t: T

    init(t: T) {
        self.t = t
    }
}

let childSignal = Signal(t: ChildClass())
let anotherSignal = Signal(t: AnotherChild())

A type-eraser that wraps any Signal<T> instance where T conforms to BaseProtocol could look like this:

struct AnyBaseProtocolSignal {
    private let _t: () -> BaseProtocol

    var t: BaseProtocol { return _t() }

    init<T : BaseProtocol>(_ base: Signal<T>) {
        _t = { base.t }
    }
}

// ...

let signals = [AnyBaseProtocolSignal(childSignal), AnyBaseProtocolSignal(anotherSignal)]

This now lets us talk in terms of heterogenous types of Signal where the T is some type that conforms to BaseProtocol.

However one problem with this wrapper is that we're restricted to talking in terms of BaseProtocol. What if we had AnotherProtocol and wanted a type-eraser for Signal instances where T conforms to AnotherProtocol?

One solution to this is to pass a transform function to the type-eraser, allowing us to perform an arbitrary upcast.

struct AnySignal<T> {
    private let _t: () -> T

    var t: T { return _t() }

    init<U>(_ base: Signal<U>, transform: @escaping (U) -> T) {
        _t = { transform(base.t) }
    }
}

Now we can talk in terms of heterogenous types of Signal where T is some type that's convertible to some U, which is specified at the creation of the type-eraser.

let signals: [AnySignal<BaseProtocol>] = [
    AnySignal(childSignal, transform: { $0 }),
    AnySignal(anotherSignal, transform: { $0 })
    // or AnySignal(childSignal, transform: { $0 as BaseProtocol })
    // to be explicit.
]

However, the passing of the same transform function to each initialiser is a little unwieldy.

In Swift 3.1 (available with Xcode 8.3 beta), you can lift this burden from the caller by defining your own initialiser specifically for BaseProtocol in an extension:

extension AnySignal where T == BaseProtocol {

    init<U : BaseProtocol>(_ base: Signal<U>) {
        self.init(base, transform: { $0 })
    }
}

(and repeat for any other protocol types you want to convert to)

Now you can just say:

let signals: [AnySignal<BaseProtocol>] = [
    AnySignal(childSignal),
    AnySignal(anotherSignal)
]

(You can actually remove the explicit type annotation for the array here, and the compiler will infer it to be [AnySignal<BaseProtocol>] – but if you're going to allow for more convenience initialisers, I would keep it explicit)


The solution for value types, or reference types where you want to specifically create a new instance, to is perform a conversion from Signal<T> (where T conforms to BaseProtocol) to Signal<BaseProtocol>.

In Swift 3.1, you can do this by defining a (convenience) initialiser in an extension for Signal types where T == BaseProtocol:

extension Signal where T == BaseProtocol {
    convenience init<T : BaseProtocol>(other: Signal<T>) {
        self.init(t: other.t)
    }
}

// ...    

let signals: [Signal<BaseProtocol>] = [
    Signal(other: childSignal),
    Signal(other: anotherSignal)
]

Pre Swift 3.1, this can be achieved with an instance method:

extension Signal where T : BaseProtocol {
    func asBaseProtocol() -> Signal<BaseProtocol> {
        return Signal<BaseProtocol>(t: t)
    }
}

// ...

let signals: [Signal<BaseProtocol>] = [
    childSignal.asBaseProtocol(),
    anotherSignal.asBaseProtocol()
]

The procedure in both cases would be similar for a struct.

这篇关于Swift泛型强制误解的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆