Engano rápido de coerção genérica DoCow
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)
Mas eu posso escrever as próximas linhas sem qualquer erro de compilador:
var arrays = Array<Array>() let arrayOfChild = Array() arrays.append(arrayOfChild)
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() ]