Por que esse código Swift não é compilado?
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension Array where Element : P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()
O compilador diz: "O tipo P
não está em conformidade com o protocolo P
" (ou, em versões posteriores do Swift, "não é suportado o uso de 'P' como um tipo concreto em conformidade com o protocolo 'P'.").
Por que não? Isso parece um buraco na linguagem, de alguma forma. Sei que o problema decorre de declarar a matriz arr
como uma matriz de um tipo de protocolo , mas isso não é razoável? Eu pensei que os protocolos existiam exatamente para ajudar a fornecer estruturas com algo como uma hierarquia de tipos?
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.protocol P : Q { }
, P não está em conformidade com Q.Respostas:
Edição: Mais dezoito meses de trabalho com Swift, outra versão importante (que fornece um novo diagnóstico) e um comentário de @AyBayBay me faz querer reescrever esta resposta. O novo diagnóstico é:
Isso realmente torna tudo muito mais claro. Esta extensão:
não se aplica quando,
Element == P
desde que,P
não é considerado uma conformidade concreta deP
. (A solução "coloque em uma caixa" abaixo ainda é a solução mais geral.)Resposta antiga:
É mais um caso de metatipos. Swift realmente quer que você chegue a um tipo concreto para a maioria das coisas não triviais.(Eu não acho que isso seja verdade; você pode absolutamente criar algo de tamanho,[P]
não é um tipo concreto (não é possível alocar um bloco de memória de tamanho conhecidoP
).P
porque isso é feito por meio de indireção .) Parece muito com um dos casos "ainda não funciona". (Infelizmente, é quase impossível conseguir que a Apple confirme a diferença entre esses casos.) O fato de queArray<P>
pode ser um tipo variável (em queArray
não pode) indica que eles já fizeram algum trabalho nessa direção, mas os metatipos Swift têm muitas arestas vivas e casos não implementados. Eu não acho que você terá uma resposta melhor do "por que" do que isso. "Porque o compilador não permite." (Insatisfatório, eu sei. Toda a minha vida rápida ...)A solução é quase sempre colocar as coisas em uma caixa. Construímos uma borracha de tipo.
Quando o Swift permite que você faça isso diretamente (o que eu espero, eventualmente), provavelmente será apenas criando essa caixa para você automaticamente. As enumerações recursivas tinham exatamente essa história. Você tinha que encaixotá-los e isso era incrivelmente irritante e restritivo, e finalmente o compilador adicionou
indirect
para fazer a mesma coisa mais automaticamente.fonte
==
no meu exemplo Array, teremos um erro, a exigência do tipo Same faz parâmetro genérico 'elemento' não-genérico "Por que não uso do do Tomohiro.==
Gerar o mesmo erro?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 propriedadesPodemos acessar esses requisitos em um espaço reservado genérico
T
ondeT : 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 permitirT
que sejaP
.Considere o que aconteceria no exemplo a seguir se permitirmos que a
Array
extensão seja aplicável a[P]
:Não é possível chamar
appendNew()
a[P]
, porqueP
(theElement
) 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 conformidadeP
.É uma história semelhante com requisitos de método e propriedade estáticos:
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çõesfoo()
oubar
definidas no exemplo acima). Embora possamos definir implementações desses requisitos em umaP
extensão, eles são definidos apenas para os tipos concretos em conformidadeP
- você ainda não pode chamá-los porP
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 idiomaEdit: E, como explorado abaixo, isso parece com o que a equipe Swift está buscando.
@objc
protocolosNa 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:
baz
requer queT
esteja em conformidade comP
; mas podemos substituir emP
paraT
porqueP
não tem requisitos estáticos. Se adicionarmos um requisito estáticoP
, o exemplo não será mais compilado: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 usandoobjc_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 protocoloP
pode compartilhar a mesma representação de memória que um 'valor genérico' do tipo de algum espaço reservado genéricoT : 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 ):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:
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 quandoElement : P
e avançar para a== P
extensão: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):
Não podemos passar
p
paratakesConcreteP(_:)
, pois atualmente não podemos substituirP
um espaço reservado genéricoT : P
. Vamos dar uma olhada em algumas maneiras pelas quais podemos resolver esse problema.1. Abrir existenciais
Ao invés de tentar substituir
P
paraT : P
, o que se poderia cavar o tipo concreto subjacente de que oP
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
:Observe o
Self
espaço reservado genérico implícito usado pelo método de extensão, que é usado para digitar oself
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 protocoloP
, Swift desenterra o tipo concreto subjacente e o usa para satisfazer oSelf
espaço reservado genérico. É por isso que somos capazes de chamartakesConcreteP(_:)
comself
- estamos satisfazendoT
comSelf
.Isso significa que agora podemos dizer:
E
takesConcreteP(_:)
é chamado com seu espaço reservado genéricoT
sendo satisfeito pelo tipo concreto subjacente (neste casoS
). Observe que isso não é "protocolos em conformidade com eles mesmos", pois estamos substituindo um tipo concreto em vez deP
- tente adicionar um requisito estático ao protocolo e ver o que acontece quando você o chama de dentrotakesConcreteP(_:)
.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:
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 doisP
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 comP
:Agora podemos apenas falar em termos de, em
AnyP
vez deP
: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á-loAnyP
. Mas como deveria ter sido implementado? Estamos lidando com instâncias arbitrárias que estão em conformidade comP
aqui - não sabemos como seus tipos concretos subjacentes implementam os requisitos estáticos; portanto, não podemos expressar isso de maneira significativaAnyP
.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 conformidadeP
.fonte
P
) é boa porque podemos encaminhar chamadas para os requisitos da instância para a instância subjacente. No entanto, para um tipo de protocolo propriamente dito (ou sejaP.Protocol
, literalmente, apenas o tipo que descreve um protocolo) - não há adotante; portanto, não há nada para chamar os requisitos estáticos, e é por isso que no exemplo acima não podemos terSomeGeneric<P>
(é diferente para umP.Type
(metatype existencial), que descreve um metatype concreto de algo que está em conformidade comP
- mas isso é outra história)P
) quanto com metatipos existenciais (por exemplo,P.Type
metatipos). O problema é que, para os genéricos - não estamos realmente comparando igual para igual. QuandoT
éP
, não há nenhum tipo underyling concreto (meta) para requisitos estáticos frente para (T
é umP.Protocol
, não umP.Type
) ....Se você estender o
CollectionType
protocolo em vez deArray
e restringir por protocolo como um tipo concreto, poderá reescrever o código anterior da seguinte maneira.fonte
== P
vs: P
. Com == o exemplo original também funciona. E um potencial problema (dependendo do contexto) com == é que exclui sub-protocolos: se eu criar umprotocol SubP: P
, e, em seguida, definirarr
como[SubP]
, em seguida,arr.test()
não funciona mais (erro: SUBP e P deve ser equivalente).