Sinices de pacote de assinaturas de função
O slices.Clone
A função é bem simples: faz uma cópia de uma fatia de qualquer tipo.
func Clone[S ~[]E, E any](s S) S {
return append(s[:0:0], s...)
}
Isso funciona porque anexar a uma fatia com capacidade zero alocará uma nova matriz de apoio. O corpo da função acaba sendo mais curto que a assinatura da função, que é em parte porque o corpo é curto, mas também porque a assinatura é longa. Nesta postagem do blog, explicaremos por que a assinatura está escrita da maneira que é.
Clone simples
Começaremos escrevendo um genérico simples Clone
função. Este não é o do slices
pacote. Queremos pegar uma fatia de qualquer tipo de elemento e devolver uma nova fatia.
func Clone1[E any](s []E) []E {
// body omitted
}
A função genérica Clone1
tem um único parâmetro de tipo E
. É preciso um único argumento s
que é uma fatia do tipo E
e retorna uma fatia do mesmo tipo. Essa assinatura é direta para qualquer pessoa familiarizada com os genéricos em Go.
No entanto, há um problema. Os tipos de fatia nomeados não são comuns no GO, mas as pessoas os usam.
// MySlice is a slice of strings with a special String method.
type MySlice []string
// String returns the printable version of a MySlice value.
func (s MySlice) String() string {
return strings.Join(s, "+")
}
Digamos que queremos fazer uma cópia de um MySlice
E então obtenha a versão imprimível, mas com as cordas em ordem classificada.
func PrintSorted(ms MySlice) string {
c := Clone1(ms)
slices.Sort(c)
return c.String() // FAILS TO COMPILE
}
Infelizmente, isso não funciona. O compilador relata um erro:
c.String undefined (type []string has no field or method String)
Podemos ver o problema se instanciarmos manualmente Clone1
substituindo o parâmetro de tipo pelo argumento do tipo.
func InstantiatedClone1(s []string) []string
O GO Regras de atribuição Permita -nos passar um valor do tipo MySlice
para um parâmetro do tipo []string
então ligando Clone1
está bem. Mas Clone1
retornará um valor do tipo []string
não é um valor do tipo MySlice
. O tipo []string
não tem um String
Método, portanto, o compilador relata um erro.
Clone flexível
Para corrigir esse problema, temos que escrever uma versão de Clone
Isso retorna o mesmo tipo que seu argumento. Se pudermos fazer isso, então quando ligarmos Clone
com um valor do tipo MySlice
ele retornará o resultado do tipo MySlice
.
Sabemos que tem que parecer algo assim.
func Clone2[S ?](s S) S // INVALID
Esse Clone2
A função retorna um valor que é o mesmo tipo que seu argumento.
Aqui escrevi a restrição como ?
mas isso é apenas um espaço reservado. Para fazer esse trabalho, precisamos escrever uma restrição que nos permitirá escrever o corpo da função. Para Clone1
Poderíamos apenas usar uma restrição de any
Para o tipo de elemento. Para Clone2
Isso não vai funcionar: queremos exigir isso s
ser um tipo de fatia.
Já que sabemos que queremos uma fatia, a restrição de S
tem que ser uma fatia. Não nos importamos com o que é o tipo de elemento da fatia, então vamos chamá -lo E
como fizemos com Clone1
.
func Clone3[S []E](s S) S // INVALID
Isso ainda é inválido, porque não declaramos E
. O argumento do tipo E
pode ser qualquer tipo, o que significa que também deve ser um parâmetro de tipo. Como pode ser qualquer tipo, sua restrição é any
.
func Clone4[S []E, E any](s S) S
Isso está chegando perto e, pelo menos, será compilado, mas ainda não estamos lá. Se compilarmos esta versão, recebemos um erro quando ligarmos Clone4(ms)
.
MySlice does not satisfy []string (possibly missing ~ for []string in []string)
O compilador está nos dizendo que não podemos usar o argumento do tipo MySlice
Para o parâmetro de tipo S
porque MySlice
não satisfaz a restrição []E
. Isso é porque []E
como uma restrição apenas permite um tipo de fatia literal, como []string
. Não permite um tipo nomeado como MySlice
.
Restrições de tipo subjacente
Como a mensagem de erro sugere, a resposta é adicionar um ~
.
func Clone5[S ~[]E, E any](s S) S
Para repetir, escreva parâmetros e restrições de tipo [S []E, E any]
significa que o tipo de argumento para S
pode ser qualquer tipo de fatia sem nome, mas não pode ser um tipo nomeado definido como uma fatia literal. Escrita [S ~[]E, E any]
com um ~
significa que o argumento do tipo S
pode ser qualquer tipo cujo tipo subjacente é um tipo de fatia.
Para qualquer tipo nomeado type T1 T2
o tipo subjacente de T1
é o tipo subjacente de T2
. O tipo subjacente de um tipo predeclarado como int
ou um tipo literal como []string
é apenas o tipo em si. Para os detalhes exatos, Veja a especificação do idioma. Em nosso exemplo, o tipo subjacente de MySlice
é []string
.
Desde o tipo subjacente de MySlice
é uma fatia, podemos passar um argumento do tipo MySlice
para Clone5
. Como você deve ter notado, a assinatura de Clone5
é o mesmo que a assinatura de slices.Clone
. Finalmente chegamos onde queremos estar.
Antes de seguirmos em frente, vamos discutir por que a sintaxe Go requer um ~
. Pode parecer que sempre gostaríamos de permitir a passagem MySlice
então por que não fazer disso o padrão? Ou, se precisarmos apoiar a correspondência exata, por que não virar as coisas, para que uma restrição de []E
permite um tipo nomeado enquanto uma restrição de, digamos, =[]E
apenas permite literais do tipo fatia?
Para explicar isso, vamos primeiro observar que uma lista de parâmetros de tipo como [T ~MySlice]
não faz sentido. Isso é porque MySlice
não é o tipo subjacente de nenhum outro tipo. Por exemplo, se tivermos uma definição como type MySlice2 MySlice
o tipo subjacente de MySlice2
é []string
não MySlice
.
Então também [T ~MySlice]
não permitiria nenhum tipo, ou seria o mesmo que [T MySlice]
e apenas combine MySlice
. De qualquer jeito, [T ~MySlice]
não é útil. Para evitar essa confusão, o idioma proíbe [T ~MySlice]
e o compilador produz um erro como
invalid use of ~ (underlying type of MySlice is []string)
Se GO não exigisse o tilde, de modo que [S []E]
corresponderia a qualquer tipo cujo tipo subjacente é []E
então teríamos que definir o significado de [S MySlice]
.
Poderíamos proibir [S MySlice]
ou poderíamos dizer isso [S MySlice]
apenas corresponde MySlice
mas qualquer abordagem tem problemas com os tipos pré -declarados. Um tipo predeclarado, como int
é seu próprio tipo subjacente. Queremos permitir que as pessoas possam escrever restrições que aceitam qualquer argumento de tipo cujo tipo subjacente seja int
. No idioma hoje, eles podem fazer isso escrevendo [T ~int]
. Se não exigirmos o tilde, ainda precisaríamos de uma maneira de dizer “qualquer tipo cujo tipo subjacente é int
”. A maneira natural de dizer que seria [T int]
. Isso significaria que [T MySlice]
e [T int]
se comportaria de maneira diferente, embora eles pareçam muito parecidos.
Talvez possamos dizer isso [S MySlice]
corresponde a qualquer tipo cujo tipo subjacente é o tipo subjacente de MySlice
mas isso faz [S MySlice]
desnecessário e confuso.
Achamos que é melhor exigir o ~
E seja muito claro sobre quando estamos correspondendo ao tipo subjacente e não ao tipo em si.
Digite inferência
Agora que explicamos a assinatura de slices.Clone
vamos ver como realmente usando slices.Clone
é simplificado por inferência de tipo. Lembre -se, a assinatura de Clone
é
func Clone[S ~[]E, E any](s S) S
Uma chamada de slices.Clone
passará uma fatia para o parâmetro s
. A inferência de tipo simples permitirá que o compilador inferir que o argumento do tipo para o parâmetro de tipo S
é o tipo de fatia que Clone
. A inferência de tipo é então poderosa o suficiente para ver que o tipo de tipo E
é o tipo de elemento do argumento de tipo passado para S
.
Isso significa que podemos escrever
c := Clone(ms)
sem ter que escrever
c := Clone[MySlice, string](ms)
Se nos referirmos a Clone
Sem chamá -lo, temos que especificar um argumento de tipo S
como o compilador não tem nada que possa usar para inferir. Felizmente, nesse caso, a inferência do tipo é capaz de inferir o argumento do tipo E
do argumento para S
e não precisamos especificá -lo separadamente.
Isto é, podemos escrever
myClone := Clone[MySlice]
sem ter que escrever
myClone := Clone[MySlice, string]
Desconstruindo parâmetros de tipo
A técnica geral que usamos aqui, na qual definimos um parâmetro de tipo S
Usando outro parâmetro de tipo E
é uma maneira de desconstruir os tipos de assinaturas de funções genéricas. Ao desconstruir um tipo, podemos nomear e restringir todos os aspectos do tipo.
Por exemplo, aqui está a assinatura para maps.Clone
.
func Clone[M ~map[K]V, K comparable, V any](m M) M
Assim como em slices.Clone
usamos um parâmetro de tipo para o tipo de parâmetro m
e depois desconstruir o tipo usando dois outros parâmetros de tipo K
e V
.
Em maps.Clone
Nós restringimos K
Para ser comparável, conforme necessário para um tipo de chave do mapa. Podemos restringir os tipos de componentes da maneira que gostarmos.
func WithStrings[S ~[]E, E interface { String() string }](s S) (S, []string)
Isso diz que o argumento de WithStrings
deve ser um tipo de fatia para o qual o tipo de elemento tem um String
método.
Como todos os tipos GO podem ser construídos a partir de tipos de componentes, sempre podemos usar parâmetros de tipo para desconstruir esses tipos e restringi -los como gostarmos.
Ian Lance Taylor
Foto de Robin Jonathan Deutsch no Unsplash
Este artigo está disponível em O blog Go sob uma licença de ação CC por 4,0.