Engano rápido de coerção genérica

Eu estou usando a biblioteca de sinais .

Vamos dizer que eu defini o protocolo BaseProtocol e BaseProtocol que está de acordo com o BaseProtocol .

 protocol BaseProtocol {} class ChildClass: BaseProtocol {} 

Agora quero armazenar sinais como:

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

Eu recebo erro:

Erro genérico rápido

Mas eu posso escrever as próximas linhas sem qualquer erro de compilador:

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

insira a descrição da imagem aqui

Então, qual a diferença entre o genérico Swift Array e o genérico Signal?

A diferença é que Array (e Set e Dictionary ) recebem tratamento especial do compilador, permitindo a covariância (eu entro nisso com um pouco mais de detalhes neste Q & A ).

No entanto tipos genéricos arbitrários são invariantes , o que significa que X é um tipo completamente não relacionado a X se T != U – qualquer outra relação de tipagem entre T e U (como subtipagem) é irrelevante. Aplicado ao seu caso, Signal e Signal são tipos não relacionados, mesmo que ChildClass seja um subtipo de BaseProtocol (veja também este Q & A ).

Uma razão para isso é que seria completamente quebrar tipos de referência genérica que definem coisas contravariantes (como parâmetros de function e setters de propriedade) em relação a T

Por exemplo, se você tivesse implementado o Signal como:

 class Signal { var t: T init(t: T) { self.t = t } } 

Se você fosse capaz de dizer:

 let signalInt = Signal(t: 5) let signalAny: Signal = signalInt 

você poderia então dizer:

 signalAny.t = "wassup" // assigning a String to a Signal's `t` property. 

o que é completamente errado, já que você não pode atribuir um String a uma propriedade Int .

A razão pela qual esse tipo de coisa é segura para o Array é que é um tipo de valor – assim, quando você faz:

 let intArray = [2, 3, 4] var anyArray : [Any] = intArray anyArray.append("wassup") 

não há problemas, pois anyArray é uma cópia do intArray – portanto, a contravariância do append(_:) não é um problema.

No entanto, isso não pode ser aplicado a tipos de valor genéricos arbitrários, pois tipos de valor podem conter qualquer número de tipos de referência genéricos, o que nos leva de volta à perigosa rota de permitir uma operação ilegal para tipos de referência genérica que definem itens contravariantes.


Como Rob diz em sua resposta, a solução para tipos de referência, se você precisar manter uma referência à mesma instância subjacente, é usar um tipo de borracha.

Se considerarmos o exemplo:

 protocol BaseProtocol {} class ChildClass: BaseProtocol {} class AnotherChild : BaseProtocol {} class Signal { var t: T init(t: T) { self.t = t } } let childSignal = Signal(t: ChildClass()) let anotherSignal = Signal(t: AnotherChild()) 

Uma borracha de tipo que encapsula qualquer instância Signal que T está em conformidade com BaseProtocol poderia ter esta aparência:

 struct AnyBaseProtocolSignal { private let _t: () -> BaseProtocol var t: BaseProtocol { return _t() } init(_ base: Signal) { _t = { base.t } } } // ... let signals = [AnyBaseProtocolSignal(childSignal), AnyBaseProtocolSignal(anotherSignal)] 

Isso agora nos permite falar em termos de tipos heterogêneos de Signal onde o T é algum tipo que está em conformidade com o BaseProtocol .

No entanto, um problema com este wrapper é que estamos restritos a falar em termos de BaseProtocol . E se tivéssemos outro AnotherProtocol e quiséssemos um tipo de borracha para instâncias de Signal que T está em conformidade com outro AnotherProtocol ?

Uma solução para isso é passar uma function de transform para o tipo de borracha, permitindo-nos realizar um upcast arbitrário.

 struct AnySignal { private let _t: () -> T var t: T { return _t() } init(_ base: Signal, transform: @escaping (U) -> T) { _t = { transform(base.t) } } } 

Agora podemos falar em termos de tipos heterogêneos de Signal onde T é algum tipo que é conversível em algum U , que é especificado na criação do tipo de borracha.

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

No entanto, a passagem da mesma function de transform para cada inicializador é um pouco pesada.

No Swift 3.1 (disponível com o Xcode 8.3 beta), você pode eliminar essa carga do BaseProtocol pela chamada definindo seu próprio inicializador especificamente para o BaseProtocol em uma extensão:

 extension AnySignal where T == BaseProtocol { init(_ base: Signal) { self.init(base, transform: { $0 }) } } 

(e repita para qualquer outro tipo de protocolo para o qual você deseja converter)

Agora você pode apenas dizer:

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

(Você pode realmente remover a anotação de tipo explícito para a matriz aqui, e o compilador irá inferir que ela seja [AnySignal] – mas se você for permitir mais inicializadores de conveniência, eu manteria isso explícito)


A solução para tipos de valor, ou tipos de referência onde você deseja criar especificamente uma nova instância, é executar uma conversão de Signal (onde T está em conformidade com BaseProtocol ) para Signal .

No Swift 3.1, você pode fazer isso definindo um inicializador (conveniência) em uma extensão para tipos de Signal onde T == BaseProtocol :

 extension Signal where T == BaseProtocol { convenience init(other: Signal) { self.init(t: other.t) } } // ... let signals: [Signal] = [ Signal(other: childSignal), Signal(other: anotherSignal) ] 

Pré Swift 3.1, isso pode ser conseguido com um método de instância:

 extension Signal where T : BaseProtocol { func asBaseProtocol() -> Signal { return Signal(t: t) } } // ... let signals: [Signal] = [ childSignal.asBaseProtocol(), anotherSignal.asBaseProtocol() ] 

O procedimento em ambos os casos seria semelhante para uma struct .