Por que covariância e contravariância não suportam tipo de valor

149

IEnumerable<T>é co-variante, mas não suporta o tipo de valor, apenas o tipo de referência. O código simples abaixo é compilado com sucesso:

IEnumerable<string> strList = new List<string>();
IEnumerable<object> objList = strList;

Mas mudar de stringpara intreceberá um erro compilado:

IEnumerable<int> intList = new List<int>();
IEnumerable<object> objList = intList;

O motivo é explicado no MSDN :

A variação se aplica apenas a tipos de referência; se você especificar um tipo de valor para um parâmetro de tipo variante, esse parâmetro de tipo será invariável para o tipo construído resultante.

Pesquisei e descobri que algumas perguntas mencionavam o motivo do boxe entre o tipo de valor e o tipo de referência . Mas ainda não me lembro muito bem por que o boxe é a razão?

Alguém poderia dar uma explicação simples e detalhada por que covariância e contravariância não suportam o tipo de valor e como o boxe afeta isso?

cuongle
fonte
3
ver também a resposta de Eric à minha pergunta semelhante: stackoverflow.com/questions/4096299/...
Thorn

Respostas:

126

Basicamente, a variação se aplica quando o CLR pode garantir que não precisa fazer nenhuma alteração representacional nos valores. Todas as referências têm a mesma aparência - para que você possa usar um IEnumerable<string>como um IEnumerable<object>sem nenhuma alteração na representação; o próprio código nativo não precisa saber o que você está fazendo com os valores, desde que a infraestrutura tenha garantido que será definitivamente válido.

Para tipos de valor, isso não funciona - para tratar um IEnumerable<int>como IEnumerable<object>, o código que usa a sequência precisaria saber se deveria ser realizada uma conversão de boxe ou não.

Você pode ler a publicação no blog de Eric Lippert sobre representação e identidade para obter mais informações sobre esse tópico em geral.

EDIT: Depois de reler a postagem do blog de Eric, é pelo menos tanto identidade quanto representação, embora os dois estejam vinculados. Em particular:

É por isso que as conversões covariantes e contravariantes dos tipos de interface e delegado exigem que todos os argumentos de tipo variados sejam de tipos de referência. Para garantir que uma conversão de referência de variante sempre preserve a identidade, todas as conversões que envolvem argumentos de tipo também devem preservar a identidade. A maneira mais fácil de garantir que todas as conversões não triviais nos argumentos de tipo preservem a identidade é restringi-las a serem conversões de referência.

Jon Skeet
fonte
5
@CuongLe: Bem, é um detalhe de implementação em alguns sentidos, mas acredito que seja a razão subjacente à restrição.
precisa saber é o seguinte
2
@ AndréCaron: O post de Eric é importante aqui - não é apenas representação, mas também preservação de identidade. Mas preservação de representação significa que o código gerado não precisa se preocupar com isso.
precisa saber é o seguinte
1
Precisamente, a identidade não pode ser preservada porque intnão é um subtipo de object. O fato de ser necessária uma mudança representacional é apenas uma consequência disso.
André Caron
3
Como int não é um subtipo de objeto? Int32 herda de System.ValueType, que herda de System.Object.
David Klempfner 6/11
1
@DavidKlempfner Acho que o comentário do AndréCaron é pouco formulado. Qualquer tipo de valor como Int32tem duas formas representacionais, "in a box" e "in a box". O compilador precisa inserir código para converter de um formulário para outro, mesmo que isso normalmente seja invisível no nível do código-fonte. De fato, somente o formulário "in a box" é considerado pelo sistema subjacente como um subtipo object, mas o compilador lida automaticamente com ele sempre que um tipo de valor é atribuído a uma interface compatível ou a algo do tipo object.
Steve
10

Talvez seja mais fácil entender se você pensar na representação subjacente (mesmo que esse seja realmente um detalhe de implementação). Aqui está uma coleção de strings:

IEnumerable<string> strings = new[] { "A", "B", "C" };

Você pode pensar stringsem ter a seguinte representação:

[0]: referência de string -> "A"
[1]: referência de string -> "B"
[2]: referência de string -> "C"

É uma coleção de três elementos, cada um sendo uma referência a uma string. Você pode converter isso em uma coleção de objetos:

IEnumerable<object> objects = (IEnumerable<object>) strings;

Basicamente, é a mesma representação, exceto que agora as referências são referências a objetos:

[0]: referência a objeto -> "A"
[1]: referência a objeto -> "B"
[2]: referência a objeto -> "C"

A representação é a mesma. As referências são tratadas apenas de maneira diferente; você não pode mais acessar a string.Lengthpropriedade, mas ainda pode ligar object.GetHashCode(). Compare isso com uma coleção de ints:

IEnumerable<int> ints = new[] { 1, 2, 3 };
[0]: int = 1
[1]: int = 2
[2]: int = 3

Para converter isso em um, IEnumerable<object>os dados devem ser convertidos com o encaixe das entradas:

[0]: referência a objeto -> 1
[1]: referência a objeto -> 2
[2]: referência a objeto -> 3

Essa conversão requer mais do que um elenco.

Martin Liversage
fonte
2
O boxe não é apenas um "detalhe de implementação". Os tipos de valor em caixa são armazenados da mesma maneira que os objetos de classe e se comportam, tanto quanto o mundo externo pode perceber, como objetos de classe. A única diferença é que, na definição de um tipo de valor em caixa, thisrefere-se a uma estrutura cujos campos sobrepõem os do objeto de heap que o armazena, em vez de se referir ao objeto que os mantém. Não há uma maneira limpa de uma instância do tipo de valor em caixa obter uma referência ao objeto de heap em anexo.
Supercat #
7

Penso que tudo começa com a definição de LSP(Princípio da Substituição de Liskov), que clime:

se q (x) for uma propriedade comprovável sobre objetos x do tipo T, então q (y) deve ser verdadeiro para objetos y do tipo S, em que S é um subtipo de T.

Mas tipos de valor, por exemplo, intnão podem ser substituídos por objectin C#. Prove é muito simples:

int myInt = new int();
object obj1 = myInt ;
object obj2 = myInt ;
return ReferenceEquals(obj1, obj2);

Isso retorna falsemesmo se atribuirmos a mesma "referência" ao objeto.

Tigran
fonte
1
Acho que você está usando o princípio certo, mas não há provas a serem feitas: intnão é um subtipo de, objectportanto, o princípio não se aplica. Sua "prova" se baseia em uma representação intermediária Integer, que é um subtipo objecte para o qual o idioma possui uma conversão implícita ( object obj1=myInt;atualmente é expandida para object obj1=new Integer(myInt);).
André Caron
A linguagem cuida da conversão correta entre os tipos, mas o comportamento do ints não corresponde ao que seria esperado do subtipo do objeto.
Tigran
Todo o meu argumento é precisamente que intnão é um subtipo de object. Além disso, o LSP não se aplica porque myInt, obj1e se obj2refere a três objetos diferentes: um inte dois (ocultos) Integers.
André Caron
22
@ André: C # não é Java. A intpalavra-chave do C # é um alias para os BCLs System.Int32, que na verdade é um subtipo de object(um alias de System.Object). De fato, inta classe base é System.ValueTypequem é a classe base System.Object. Tente avaliar a seguinte expressão e veja: typeof(int).BaseType.BaseType. O motivo de ReferenceEqualsretornar falso aqui é que ele inté encaixotado em duas caixas separadas e a identidade de cada caixa é diferente para qualquer outra caixa. Portanto, duas operações de boxe sempre produzem dois objetos que nunca são idênticos, independentemente do valor do box.
Allon Guralnek
@AllonGuralnek: Cada tipo de valor (por exemplo, System.Int32ou List<String>.Enumerator) realmente representa dois tipos de coisas: um tipo de local de armazenamento e um tipo de objeto de pilha (às vezes chamado de "tipo de valor em caixa"). Os locais de armazenamento cujos tipos derivam System.ValueTypemanterão o primeiro; objetos heap cujos tipos fazem o mesmo manterão o último. Na maioria das línguas, existe um elenco alargado do primeiro para o segundo, e um elenco mais estreito do último para o primeiro. Note-se que, enquanto os tipos de valor em caixa têm o mesmo descritor de tipo como locais-tipo de valor de armazenamento, ...
supercat
3

Ele se resume a um detalhe de implementação: Tipos de valor são implementados de maneira diferente para tipos de referência.

Se você forçar os tipos de valor a serem tratados como tipos de referência (por exemplo, encaixotá-los, por exemplo, referindo-se a eles por meio de uma interface), poderá obter variação.

A maneira mais fácil de ver a diferença é simplesmente considerar uma Array: uma matriz de tipos de Valor é reunida na memória de forma contígua (diretamente), onde, como uma matriz de tipos de Referência, apenas a referência (um ponteiro) possui contígua na memória; os objetos que estão sendo apontados são alocados separadamente.

A outra questão (relacionada) (*) é que (quase) todos os tipos de referência têm a mesma representação para fins de variação e muito código não precisa saber da diferença entre os tipos, portanto, a covariância e a contravariância são possíveis (e facilmente implementado - geralmente apenas por omissão de verificação de tipo extra).

(*) Pode ser o mesmo problema ...

Mark Hurd
fonte