Por que os protocolos não se conformam?
Permitir que os protocolos se ajustem a si mesmos no caso geral não é válido. O problema está nos requisitos de protocolo estático.
Esses incluem:
static
métodos e propriedades
- Inicializadores
- Tipos associados (embora atualmente evitem o uso de um protocolo como um tipo real)
Podemos acessar esses requisitos em um espaço reservado genérico T
onde T : P
- no entanto, não podemos acessá-los no próprio tipo de protocolo, pois não há um tipo de conformidade concreto para o qual encaminhar. Portanto, não podemos permitir T
que seja P
.
Considere o que aconteceria no exemplo a seguir se permitirmos que a Array
extensão seja aplicável a [P]
:
protocol P {
init()
}
struct S : P {}
struct S1 : P {}
extension Array where Element : P {
mutating func appendNew() {
// If Element is P, we cannot possibly construct a new instance of it, as you cannot
// construct an instance of a protocol.
append(Element())
}
}
var arr: [P] = [S(), S1()]
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
arr.appendNew()
Não é possível chamar appendNew()
a [P]
, porque P
(the Element
) não é um tipo concreto e, portanto, não pode ser instanciado. Ele deve ser chamado em uma matriz com elementos do tipo concreto, onde esse tipo está em conformidade P
.
É uma história semelhante com requisitos de método e propriedade estáticos:
protocol P {
static func foo()
static var bar: Int { get }
}
struct SomeGeneric<T : P> {
func baz() {
// If T is P, what's the value of bar? There isn't one – because there's no
// implementation of bar's getter defined on P itself.
print(T.bar)
T.foo() // If T is P, what method are we calling here?
}
}
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
SomeGeneric<P>().baz()
Não podemos falar em termos de SomeGeneric<P>
. Precisamos de implementações concretas dos requisitos do protocolo estático (observe como não há implementações foo()
ou bar
definidas no exemplo acima). Embora possamos definir implementações desses requisitos em uma P
extensão, eles são definidos apenas para os tipos concretos em conformidade P
- você ainda não pode chamá-los por P
si.
Por causa disso, o Swift nos impede completamente de usar um protocolo como um tipo que se adapta a si mesmo - porque quando esse protocolo tem requisitos estáticos, não.
Os requisitos do protocolo da instância não são problemáticos, pois você deve chamá-los em uma instância real que esteja em conformidade com o protocolo (e, portanto, deve ter implementado os requisitos). Portanto, ao chamar um requisito em uma instância digitada como P
, podemos apenas encaminhar essa chamada para a implementação desse tipo do tipo concreto subjacente.
No entanto, abrir exceções especiais para a regra nesse caso pode levar a inconsistências surpreendentes no modo como os protocolos são tratados pelo código genérico. Embora isso tenha sido dito, a situação não é muito diferente dos associatedtype
requisitos - o que (atualmente) impede que você use um protocolo como um tipo. Ter uma restrição que o impeça de usar um protocolo como um tipo que se adapta a si mesmo quando possui requisitos estáticos pode ser uma opção para uma versão futura do idioma
Edit: E, como explorado abaixo, isso parece com o que a equipe Swift está buscando.
@objc
protocolos
Na verdade, é exatamente assim que a linguagem trata os @objc
protocolos. Quando eles não têm requisitos estáticos, eles se adaptam a si mesmos.
O seguinte compila perfeitamente:
import Foundation
@objc protocol P {
func foo()
}
class C : P {
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c)
baz
requer que T
esteja em conformidade com P
; mas podemos substituir em P
para T
porque P
não tem requisitos estáticos. Se adicionarmos um requisito estático P
, o exemplo não será mais compilado:
import Foundation
@objc protocol P {
static func bar()
func foo()
}
class C : P {
static func bar() {
print("C's bar called")
}
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)'
Portanto, uma solução alternativa para esse problema é criar seu protocolo @objc
. Concedido, isso não é uma solução ideal em muitos casos, pois força os tipos conformes a serem classes, além de exigir o tempo de execução do Obj-C, portanto, não o torna viável em plataformas que não sejam da Apple, como o Linux.
Mas suspeito que essa limitação é (uma das) principais razões pelas quais a linguagem já implementa 'protocolo sem requisitos estáticos em conformidade com ela mesma' para @objc
protocolos. O código genérico escrito em torno deles pode ser significativamente simplificado pelo compilador.
Por quê? Como @objc
os valores digitados no protocolo são efetivamente apenas referências de classe cujos requisitos são despachados usando objc_msgSend
. Por outro lado, os @objc
valores que não são do tipo protocolo são mais complicados, pois carregam tabelas de valores e de testemunhas para gerenciar a memória de seu valor empacotado (armazenado de maneira indireta) e determinar quais implementações exigem os diferentes requisitos, respectivamente.
Devido a essa representação simplificada para @objc
protocolos, um valor desse tipo de protocolo P
pode compartilhar a mesma representação de memória que um 'valor genérico' do tipo de algum espaço reservado genérico T : P
, provavelmente facilitando para a equipe Swift permitir a auto-conformidade. O mesmo não se aplica a não @objc
protocolos, no entanto, como esses valores genéricos atualmente não possuem tabelas de testemunhas de valor ou protocolo.
No entanto, esse recurso é intencional e espera-se que seja implementado em não @objc
protocolos, conforme confirmado pelo membro da equipe Swift Slava Pestov nos comentários do SR-55 em resposta à sua pergunta sobre o assunto (solicitado por esta pergunta ):
Matt Neuburg adicionou um comentário - 7 set 2017 13:33
Isso compila:
@objc protocol P {}
class C: P {}
func process<T: P>(item: T) -> T { return item }
func f(image: P) { let processed: P = process(item:image) }
Adicionar @objc
faz com que seja compilado; removê-lo não compila novamente. Alguns de nós, no Stack Overflow, acham isso surpreendente e gostariam de saber se isso é deliberado ou se é um caso de buggy.
Slava Pestov adicionou um comentário - 7 set 2017 13:53
É deliberado - é o levantamento dessa restrição que trata esse bug. Como eu disse, é complicado e ainda não temos planos concretos.
Então, espero que seja algo que a linguagem um dia também suporte para não @objc
protocolos.
Mas que soluções atuais existem para não @objc
protocolos?
Implementando extensões com restrições de protocolo
No Swift 3.1, se você deseja uma extensão com uma restrição de que um determinado espaço reservado genérico ou tipo associado deve ser um determinado tipo de protocolo (não apenas um tipo concreto que esteja em conformidade com esse protocolo) - você pode simplesmente defini-lo com uma ==
restrição.
Por exemplo, poderíamos escrever sua extensão de matriz como:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
Obviamente, isso agora nos impede de chamá-lo em uma matriz com elementos do tipo concreto em conformidade P
. Para resolver isso, basta definir uma extensão adicional para quando Element : P
e avançar para a == P
extensão:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
No entanto, vale a pena notar que isso executará uma conversão O (n) da matriz em a [P]
, pois cada elemento precisará ser encaixotado em um contêiner existencial. Se o desempenho for um problema, você pode simplesmente resolver isso reimplementando o método de extensão. Essa não é uma solução totalmente satisfatória - espero que uma versão futura da linguagem inclua uma maneira de expressar uma restrição de 'tipo de protocolo ou conforme o tipo de protocolo'.
Antes do Swift 3.1, a maneira mais geral de conseguir isso, como Rob mostra em sua resposta , é simplesmente criar um tipo de wrapper para a [P]
, no qual você pode definir o (s) método (s) de extensão.
Passando uma instância do tipo de protocolo para um espaço reservado genérico restrito
Considere a seguinte situação (artificial, mas não incomum):
protocol P {
var bar: Int { get set }
func foo(str: String)
}
struct S : P {
var bar: Int
func foo(str: String) {/* ... */}
}
func takesConcreteP<T : P>(_ t: T) {/* ... */}
let p: P = S(bar: 5)
// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)
Não podemos passar p
para takesConcreteP(_:)
, pois atualmente não podemos substituir P
um espaço reservado genérico T : P
. Vamos dar uma olhada em algumas maneiras pelas quais podemos resolver esse problema.
1. Abrir existenciais
Ao invés de tentar substituir P
para T : P
, o que se poderia cavar o tipo concreto subjacente de que o P
valor digitado foi acondicionamento e substituto que, em vez? Infelizmente, isso requer um recurso de idioma chamado abertura existencial , que atualmente não está diretamente disponível para os usuários.
No entanto, o Swift abre implicitamente existenciais (valores do tipo protocolo) ao acessar membros neles (ou seja, ele extrai o tipo de tempo de execução e o torna acessível na forma de um espaço reservado genérico). Podemos explorar esse fato em uma extensão de protocolo em P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Observe o Self
espaço reservado genérico implícito usado pelo método de extensão, que é usado para digitar o self
parâmetro implícito - isso acontece nos bastidores com todos os membros da extensão de protocolo. Ao chamar esse método em um valor digitado em protocolo P
, Swift desenterra o tipo concreto subjacente e o usa para satisfazer o Self
espaço reservado genérico. É por isso que somos capazes de chamar takesConcreteP(_:)
com self
- estamos satisfazendo T
com Self
.
Isso significa que agora podemos dizer:
p.callTakesConcreteP()
E takesConcreteP(_:)
é chamado com seu espaço reservado genérico T
sendo satisfeito pelo tipo concreto subjacente (neste caso S
). Observe que isso não é "protocolos em conformidade com eles mesmos", pois estamos substituindo um tipo concreto em vez de P
- tente adicionar um requisito estático ao protocolo e ver o que acontece quando você o chama de dentro takesConcreteP(_:)
.
Se o Swift continuar a proibir a conformidade dos protocolos, a próxima melhor alternativa seria abrir implicitamente os existenciais ao tentar passá-los como argumentos para parâmetros do tipo genérico - efetivamente fazendo exatamente o que nosso trampolim de extensão de protocolo fez, apenas sem o clichê.
No entanto, observe que abrir existenciais não é uma solução geral para o problema de protocolos que não estão em conformidade. Ele não lida com coleções heterogêneas de valores do tipo protocolo, que podem todos ter tipos concretos subjacentes diferentes. Por exemplo, considere:
struct Q : P {
var bar: Int
func foo(str: String) {}
}
// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}
// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]
// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array)
Pelas mesmas razões, uma função com vários T
parâmetros também seria problemática, pois os parâmetros devem receber argumentos do mesmo tipo - no entanto, se tivermos dois P
valores, não há como garantir em tempo de compilação que ambos tenham o mesmo concreto subjacente tipo.
Para resolver esse problema, podemos usar um apagador de tipo.
2. Crie uma borracha de tipo
Como diz Rob , uma borracha de tipo , é a solução mais geral para o problema de protocolos que não estão em conformidade. Eles nos permitem agrupar uma instância do tipo de protocolo em um tipo concreto que esteja em conformidade com esse protocolo, encaminhando os requisitos da instância para a instância subjacente.
Portanto, vamos criar uma caixa de exclusão de tipo que encaminhe P
os requisitos da instância para uma instância arbitrária subjacente que esteja em conformidade com P
:
struct AnyP : P {
private var base: P
init(_ base: P) {
self.base = base
}
var bar: Int {
get { return base.bar }
set { base.bar = newValue }
}
func foo(str: String) { base.foo(str: str) }
}
Agora podemos apenas falar em termos de, em AnyP
vez de P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
Agora, considere por um momento exatamente por que tivemos que construir essa caixa. Como discutimos anteriormente, o Swift precisa de um tipo concreto para os casos em que o protocolo possui requisitos estáticos. Considere se P
tivesse um requisito estático - precisaríamos implementá-lo AnyP
. Mas como deveria ter sido implementado? Estamos lidando com instâncias arbitrárias que estão em conformidade com P
aqui - não sabemos como seus tipos concretos subjacentes implementam os requisitos estáticos; portanto, não podemos expressar isso de maneira significativa AnyP
.
Portanto, a solução nesse caso é realmente realmente útil no caso de requisitos de protocolo de instância . No caso geral, ainda não podemos tratar P
como um tipo concreto em conformidade P
.
let arr
linha, o compilador deduz o tipo[S]
e o código é compilado. Parece que um tipo de protocolo não pode ser usado da mesma maneira que um relacionamento de classe - superclasse.