Qual é a razão do uso de uma interface versus um tipo genericamente restrito

15

Em linguagens orientadas a objetos que suportam parâmetros de tipo genéricos (também conhecidos como modelos de classe e polimorfismo paramétrico, embora, é claro, cada nome possua conotações diferentes), muitas vezes é possível especificar uma restrição de tipo no parâmetro de tipo, de forma que ele desça de outro tipo. Por exemplo, esta é a sintaxe em C #:

//for classes:
class ExampleClass<T> where T : I1 {

}
//for methods:
S ExampleMethod<S>(S value) where S : I2 {
        ...
}

Quais são os motivos para usar tipos de interface reais sobre tipos restritos por essas interfaces? Por exemplo, quais são os motivos para fazer a assinatura do método I2 ExampleMethod(I2 value)?

GregRos
fonte
4
modelos de classe (C ++) são algo completamente diferente e muito mais poderoso do que míseros genéricos. Mesmo que os idiomas com genéricos tenham emprestado a sintaxe do modelo para eles.
Deduplicator
Os métodos de interface são chamadas indiretas, enquanto os métodos de tipo podem ser chamadas diretas. Portanto, o último pode ser mais rápido que o anterior e, no caso de refparâmetros do tipo de valor, pode realmente modificar o tipo de valor.
user541686
@ Reduplicador: Considerando que os genéricos são mais antigos que os modelos, não consigo ver como os genéricos poderiam ter emprestado algo de modelos, sintaxe ou não.
Jörg W Mittag
3
@ JörgWMittag: Suspeito que, por "linguagens orientadas a objetos que suportam genéricos", o Deduplicator possa ter entendido "Java e C #" em vez de "ML e Ada". Então a influência do C ++ no primeiro é clara, apesar de nem todas as linguagens terem polimorfismo genérico ou paramétrico emprestado do C ++.
Steve Jessop
2
@SteveJessop: ML, Ada, Eiffel, Haskell são anteriores aos modelos C ++, Scala, F #, OCaml veio depois e nenhum deles compartilha a sintaxe do C ++. (Curiosamente, mesmo o D, que empresta muito do C ++, especialmente modelos, não compartilha a sintaxe do C ++.) "Java e C #" são uma visão bastante restrita de "linguagens com genéricos", eu acho.
Jörg W Mittag

Respostas:

21

O uso da versão paramétrica fornece

  1. Mais informações para os usuários da função
  2. Restringe o número de programas que você pode escrever (verificação gratuita de erros)

Como exemplo aleatório, suponha que tenhamos um método que calcule as raízes de uma equação quadrática

int solve(int a, int b, int c) {
  // My 7th grade math teacher is laughing somewhere
}

E então você quer que ele funcione em outros tipos de números, como outras coisas int. Você pode escrever algo como

Num solve(Num a, Num b, Num c){
  ...
}

A questão é que isso não diz o que você deseja. Diz

Dê-me três coisas que são números (não necessariamente da mesma maneira) e eu devolverei algum tipo de número

Não podemos fazer algo como int sol = solve(a, b, c)se a, be csomos ints porque não sabemos que o método retornará umint no final! Isso leva a uma dança desajeitada, com abatimento e oração, se quisermos usar a solução em uma expressão maior.

Dentro da função, alguém pode nos dar um float, um bigint e graus, e teríamos que adicioná-los e multiplicá-los. Gostaríamos de rejeitar estaticamente isso porque as operações entre essas três classes serão sem sentido. Os graus são mod 360, por isso não será o caso a.plus(b) = b.plus(a)e hilaridades semelhantes surgirão.

Se usarmos o polimorfismo paramétrico com subtipagem, podemos descartar tudo isso, porque nosso tipo realmente diz o que queremos dizer

<T : Num> T solve(T a, T b, T c)

Ou em palavras "Se você me der um tipo que é um número, eu posso resolver equações com esses coeficientes".

Isso aparece em muitos outros lugares também. Outra boa fonte de exemplos são funções que abstrato sobre algum tipo de recipiente, ala reverse, sort, map, etc.

Daniel Gratzer
fonte
8
Em resumo, a versão genérica garante que todas as três entradas (e a saída) serão do mesmo tipo de número.
MathematicalOrchid
No entanto, isso fica aquém quando você não controla o tipo em questão (e, portanto, não pode adicionar uma interface a ele). Para máxima generalidade, você teria que aceitar uma interface parametrizada pelo tipo de argumento (por exemplo Num<int>) como um argumento extra. Você sempre pode implementar a interface para qualquer tipo através da delegação. Isso é essencialmente o que as classes de tipo de Haskell são, exceto muito mais entediante de usar, pois você precisa passar explicitamente pela interface.
Doval
16

Quais são os motivos para usar tipos de interface reais sobre tipos restritos por essas interfaces?

Porque é disso que você precisa ...

IFoo Fn(IFoo x);
T Fn<T>(T x) where T: IFoo;

são duas assinaturas decididamente diferentes. O primeiro leva qualquer tipo de implementação da interface e a única garantia que faz é que o valor de retorno satisfaça a interface.

O segundo pega qualquer tipo que implementa a interface e garante que ele retornará pelo menos esse tipo novamente (em vez de algo que satisfaça a interface menos restritiva).

Às vezes, você quer a garantia mais fraca. Às vezes você quer o mais forte.

Telastyn
fonte
Você pode dar um exemplo de como você usaria a versão de garantia mais fraca?
GregRos
4
@ GregRos - Por exemplo, em algum código analisador que escrevi. Eu tenho uma função Orque pega dois Parserobjetos (uma classe base abstrata, mas o princípio é válido) e retorna um novo Parser(mas com um tipo diferente). O usuário final não deve saber ou se importar com o tipo de concreto.
Telastyn
Em C #, imagino que devolver um T diferente do que foi passado seja quase impossível (sem dor de reflexão) sem a nova restrição, além de tornar sua garantia forte bastante inútil por si só.
NtscCobalt
1
@ NtscCobalt: é mais útil quando você combina programação paramétrica e genérica de interface. Por exemplo, o LINQ faz o tempo todo (aceita um IEnumerable<T>, retorna outro IEnumerable<T>que é, por exemplo, na verdade um OrderedEnumerable<T>)
Ben Voigt
2

O uso de genéricos restritos para parâmetros de método pode permitir que um método seja muito do seu tipo de retorno com base no que foi transmitido. No .NET, eles também podem ter vantagens adicionais. Entre eles:

  1. Um método que aceita um genérico restrito como parâmetro refou outpode receber uma variável que satisfaça a restrição; por outro lado, um método não genérico com um parâmetro do tipo de interface seria limitado a aceitar variáveis ​​desse tipo exato de interface.

  2. Um método com o parâmetro de tipo genérico T pode aceitar coleções genéricas de T. Um método que aceita um IList<T> where T:IAnimalpoderá aceitar a List<SiameseCat>, mas um método que desejasse um IList<Animal>não seria capaz de fazê-lo.

  3. Às vezes, uma restrição pode especificar uma interface em termos do tipo genérico, por exemplo where T:IComparable<T>.

  4. Uma estrutura que implementa uma interface pode ser mantida como um tipo de valor quando passada para um método que aceita um parâmetro genérico restrito, mas deve ser encaixotada quando passada como um tipo de interface. Isso pode ter um efeito enorme na velocidade.

  5. Um parâmetro genérico pode ter várias restrições, enquanto não há outra maneira de especificar um parâmetro de "algum tipo que implemente IFoo e IBar". Às vezes, isso pode ser uma faca de dois gumes, pois o código que recebeu um parâmetro do tipo IFoodificilmente passará para um método que espera um genérico de dupla restrição, mesmo que a instância em questão atenda a todas as restrições.

Se em uma situação específica não houver vantagem em usar um genérico, basta aceitar um parâmetro do tipo de interface. O uso de um genérico forçará o sistema de tipos e o JITter a realizar um trabalho extra; portanto, se não houver benefício, não se deve fazê-lo. Por outro lado, é muito comum que pelo menos uma das vantagens acima se aplique.

supercat
fonte