O que é reificação?

163

Eu sei que Java implementa polimorfismo paramétrico (genéricos) com apagamento. Eu entendo o que é apagamento.

Eu sei que o C # implementa polimorfismo paramétrico com reificação. Eu sei que isso pode fazer você escrever

public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}

ou que você pode saber em tempo de execução qual é o parâmetro type de algum tipo parametrizado, mas não entendo qual é .

  • O que é um tipo reificado?
  • O que é um valor reificado?
  • O que acontece quando um tipo / valor é reificado?
Martijn
fonte
Não é uma resposta, mas pode ajudar de alguma forma: beust.com/weblog/2011/07/29/erasure-vs-reification
Heringer
@heringer que parece responder bastante bem à pergunta "o que é apagamento" e parece responder basicamente "o que é reificação" com "não apagamento" - um tema comum que encontrei ao procurar inicialmente uma resposta antes de postar aqui.
Martijn
5
... e não havia me pensar re ificação é o processo de conversão de uma switchvolta construção de um if/ else, quando anteriormente tinha sido convertido a partir de um if/ elsea um switch...
Digital Trauma
8
Res , reis é latim por coisa , então reificação é literalmente coisificação . Não tenho nada útil para contribuir no que diz respeito ao uso do termo pelo C #, mas o fato de eles o usarem me faz sorrir.
KRyan #

Respostas:

209

Reificação é o processo de pegar uma coisa abstrata e criar uma coisa concreta.

O termo reificação em C # genéricos refere-se ao processo pelo qual uma definição de tipo genérico e um ou mais argumentos de tipo genérico (a coisa abstrata) são combinados para criar um novo tipo genérico (a coisa concreta).

Para expressá-lo de forma diferente, é o processo de tirar a definição de List<T>e inte produzir um concreto List<int>tipo.

Para entender melhor, compare as seguintes abordagens:

  • Nos genéricos Java, uma definição de tipo genérico é transformada em essencialmente um tipo genérico concreto compartilhado em todas as combinações de argumentos de tipo permitido. Portanto, vários tipos (nível de código-fonte) são mapeados para um tipo (nível binário) - mas, como resultado, as informações sobre os argumentos de tipo de uma instância são descartadas nessa instância (apagamento de tipo) .

    1. Como efeito colateral dessa técnica de implementação, os únicos argumentos de tipo genérico permitidos nativamente são aqueles que podem compartilhar o código binário de seu tipo concreto; o que significa aqueles tipos cujos locais de armazenamento possuem representações intercambiáveis; o que significa tipos de referência. O uso de tipos de valor como argumentos de tipo genérico requer encaixotá-los (colocando-os em um wrapper de tipo de referência simples).
    2. Nenhum código é duplicado para implementar genéricos dessa maneira.
    3. As informações de tipo que poderiam estar disponíveis no tempo de execução (usando reflexão) são perdidas. Isso, por sua vez, significa que a especialização de um tipo genérico (a capacidade de usar código fonte especializado para qualquer combinação de argumentos genéricos em particular) é muito restrita.
    4. Esse mecanismo não requer suporte do ambiente de tempo de execução.
    5. Existem algumas soluções alternativas para reter informações de tipo que um programa Java ou uma linguagem baseada em JVM pode usar.
  • Em genéricos C #, a definição de tipo genérico é mantida na memória em tempo de execução. Sempre que um novo tipo concreto é necessário, o ambiente de tempo de execução combina a definição de tipo genérico e os argumentos de tipo e cria o novo tipo (reificação). Portanto, obtemos um novo tipo para cada combinação dos argumentos de tipo, em tempo de execução .

    1. Essa técnica de implementação permite que qualquer tipo de combinação de argumentos de tipo seja instanciada. Usar tipos de valor como argumentos de tipo genérico não causa boxe, pois esses tipos obtêm sua própria implementação. (O boxe ainda existe em C # , é claro - mas acontece em outros cenários, não neste.)
    2. A duplicação de código pode ser um problema - mas, na prática, não é, porque implementações suficientemente inteligentes ( incluindo o Microsoft .NET e Mono ) podem compartilhar código por algumas instanciações.
    3. As informações de tipo são mantidas, o que permite a especialização até certo ponto, examinando argumentos de tipo usando reflexão. No entanto, o grau de especialização é limitado, como resultado de uma definição de tipo genérica ser compilada antes de qualquer reificação (isso é feito compilando a definição com base nas restrições nos parâmetros de tipo - portanto, o compilador deve ser capaz de "entender" a definição mesmo na ausência de argumentos de tipo específico ).
    4. Essa técnica de implementação depende muito do suporte ao tempo de execução e da compilação JIT (é por isso que você costuma ouvir que os genéricos de C # têm algumas limitações em plataformas como iOS , onde a geração dinâmica de código é restrita).
    5. No contexto de C # genéricos, a reificação é feita para você pelo ambiente de tempo de execução. No entanto, se você quiser entender de maneira mais intuitiva a diferença entre uma definição de tipo genérico e um tipo genérico concreto, sempre poderá executar uma reificação por conta própria, usando a System.Typeclasse (mesmo que a combinação de argumentos de tipo genérico específica que você está instanciando não tenha feito ' (apareça diretamente no seu código-fonte).
  • Nos modelos C ++, a definição do modelo é mantida na memória em tempo de compilação. Sempre que uma nova instanciação de um tipo de modelo é necessária no código-fonte, o compilador combina a definição do modelo e os argumentos do modelo e cria o novo tipo. Portanto, obtemos um tipo exclusivo para cada combinação dos argumentos do modelo, em tempo de compilação .

    1. Essa técnica de implementação permite que qualquer tipo de combinação de argumentos de tipo seja instanciada.
    2. Sabe-se que ele duplicou o código binário, mas uma cadeia de ferramentas suficientemente inteligente ainda pode detectar isso e compartilhar código por algumas instanciações.
    3. A definição do modelo em si não é "compilada" - apenas suas instanciações concretas são realmente compiladas . Isso coloca menos restrições no compilador e permite um maior grau de especialização de modelos .
    4. Como as instanciações de modelo são executadas em tempo de compilação, também não é necessário suporte a tempo de execução aqui.
    5. Ultimamente, esse processo é chamado de monomorfização , especialmente na comunidade Rust. A palavra é usada em contraste com o polimorfismo paramétrico , que é o nome do conceito de origem genérica .
Theodoros Chatzigiannakis
fonte
7
Ótima comparação com modelos C ++ ... eles parecem estar entre os C # e os genéricos de Java. Você tem código e estrutura diferentes para lidar com diferentes tipos genéricos específicos, como em C #, mas tudo é feito em tempo de compilação, como em Java.
Luaan 7/08/2015
3
Além disso, em C ++, isso permite introduzir a especialização de modelos, onde cada (ou apenas alguns) tipos concretos podem ter implementações diferentes. Obviamente não é possível em Java, mas também em C #.
Quetzalcoatl
@quetzalcoatl, embora uma das razões para usá-lo seja reduzir a quantidade de código produzido com tipos de ponteiro, e o C # faz algo comparável aos tipos de referência nos bastidores. Ainda assim, esse é apenas um motivo para usá-lo, e definitivamente há momentos em que a especialização de modelos seria boa.
Jon Hanna
Para Java, você pode adicionar que, enquanto as informações de tipo são apagadas, as conversões são adicionadas pelo compilador, tornando o bytecode indistinguível do bytecode pré-genérico.
Rusty Core
27

Reificação significa geralmente (fora da ciência da computação) "tornar algo real".

Na programação, algo é reificado se pudermos acessar informações sobre ele na própria linguagem.

Para dois exemplos completamente não genéricos de algo que o C # faz e não tem reificado, vamos usar métodos e acesso à memória.

As linguagens OO geralmente têm métodos (e muitas que não têm funções semelhantes, embora não estejam ligadas a uma classe). Como tal, você pode definir um método em um idioma assim, chamá-lo, talvez substituí-lo e assim por diante. Nem todas essas linguagens permitem que você lide com o próprio método como dados para um programa. C # (e, na verdade, .NET em vez de C #) permite fazer uso de MethodInfoobjetos que representam os métodos; portanto, em métodos de C # são reificados. Métodos em C # são "objetos de primeira classe".

Todas as linguagens práticas têm alguns meios para acessar a memória de um computador. Em uma linguagem de baixo nível como C, podemos lidar diretamente com o mapeamento entre os endereços numéricos usados ​​pelo computador, de modo que gostos int* ptr = (int*) 0xA000000; *ptr = 42;sejam razoáveis ​​(desde que tenhamos um bom motivo para suspeitar que o acesso ao endereço de memória 0xA000000dessa maneira tenha vencido ' explodir algo). No C #, isso não é razoável (podemos forçá-lo no .NET, mas com o gerenciamento de memória do .NET movendo as coisas, é pouco provável que seja útil). C # não possui endereços de memória reificados.

Portanto, como refied significa "tornado real" um "tipo reificado" é um tipo sobre o qual podemos "falar" no idioma em questão.

Em genéricos, isso significa duas coisas.

Uma delas é que List<string>é um tipo tão stringou intsão. Podemos comparar esse tipo, obter seu nome e perguntar sobre ele:

Console.WriteLine(typeof(List<string>).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Console.WriteLine(typeof(List<string>) == (42).GetType()); // False
Console.WriteLine(typeof(List<string>) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True
Console.WriteLine(typeof(List<string>).GenericTypeArguments[0] == typeof(string)); // True

Uma conseqüência disso é que podemos "falar sobre" os tipos de parâmetros de um método genérico (ou método de uma classe genérica) dentro do próprio método:

public static void DescribeType<T>(T element)
{
  Console.WriteLine(typeof(T).FullName);
}
public static void Main()
{
  DescribeType(42);               // System.Int32
  DescribeType(42L);              // System.Int64
  DescribeType(DateTime.UtcNow);  // System.DateTime
}

Como regra, fazer isso demais é "fedido", mas tem muitos casos úteis. Por exemplo, veja:

public static TSource Min<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = default(TSource);
  if (value == null)
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      do
      {
        if (!e.MoveNext()) return value;
        value = e.Current;
      } while (value == null);
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (x != null && comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  else
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      if (!e.MoveNext()) throw Error.NoElements();
      value = e.Current;
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  return value;
}

Isso não faz muitas comparações entre o tipo de TSourcee vários tipos para comportamentos diferentes (geralmente um sinal de que você não deveria ter usado genéricos), mas se divide entre um caminho de código para tipos que podem ser null(deve retornar nullse nenhum elemento encontrado e não deve fazer comparações para encontrar o mínimo se um dos elementos comparados for null) e o caminho do código para tipos que não podem ser null(devem ser lançados se nenhum elemento for encontrado e não precisam se preocupar com a possibilidade de nullelementos )

Como TSourceé "real" dentro do método, essa comparação pode ser feita em tempo de execução ou em tempo de execução (geralmente tempo de execução, certamente o caso acima faria isso no momento da execução e não produziria código de máquina para o caminho não percorrido) e temos versão "real" separada do método para cada caso. (Embora como uma otimização, o código da máquina seja compartilhado para diferentes métodos para diferentes parâmetros do tipo de referência, porque pode ser sem afetar isso e, portanto, podemos reduzir a quantidade de código da máquina emitida).

(Não é comum falar sobre reificação de tipos genéricos em C #, a menos que você também lide com Java, porque em C # nós consideramos essa reificação como garantida; todos os tipos são reificados. Em Java, tipos não genéricos são referidos como reificados, porque é uma distinção entre eles e tipos genéricos).

Jon Hanna
fonte
Você não acha capaz de fazer o que Minfaz acima útil? Caso contrário, é muito difícil cumprir seu comportamento documentado.
Jon Hanna
Considero que o bug é o comportamento (des) documentado e a implicação de que esse comportamento é útil (como um aparte, o comportamento de Enumerable.Min<TSource>é diferente, pois ele não lança para tipos não-referência em uma coleção vazia, mas retorna o padrão (TSource) e está documentado apenas como "Retorna o valor mínimo em uma sequência genérica". Eu diria que ambos devem lançar uma coleção vazia ou que um elemento "zero" deve ser passado como linha de base e o comparador / função de comparação sempre deve ser passada))
Martijn
1
Isso seria muito menos útil que o Min atual, que corresponde ao comportamento comum do banco de dados em tipos anuláveis ​​sem tentar o impossível em tipos não anuláveis. (A ideia da linha de base não é impossível, mas não é muito útil, a menos que haja um valor que você saiba que nunca estaria na fonte).
Jon Hanna
1
Thingification teria sido um nome melhor para isso. :)
tchrist
@ Chris uma coisa pode ser irreal.
Jon Hanna
15

Como o duffymo já observou , "reificação" não é a principal diferença.

Em Java, os genéricos estão basicamente lá para melhorar o suporte ao tempo de compilação - ele permite que você use coleções fortemente tipadas, por exemplo, em seu código, e tenha a segurança de tipo manipulada para você. No entanto, isso só existe no momento da compilação - o código de código compilado não tem mais noção de genéricos; todos os tipos genéricos são transformados em tipos "concretos" (usando objectse o tipo genérico é ilimitado), adicionando conversões de tipo e verificações de tipo, conforme necessário.

No .NET, os genéricos são um recurso integrante do CLR. Quando você compila um tipo genérico, ele permanece genérico na IL gerada. Não é apenas transformado em código não genérico como em Java.

Isso tem vários impactos sobre como os genéricos funcionam na prática. Por exemplo:

  • O Java precisa SomeType<?>permitir que você passe qualquer implementação concreta de um determinado tipo genérico. O C # não pode fazer isso - todo tipo genérico ( reificado ) específico é de seu próprio tipo.
  • Tipos genéricos não ligados em Java significam que seu valor é armazenado como um object. Isso pode ter um impacto no desempenho ao usar tipos de valor em tais genéricos. Em C #, quando você usa um tipo de valor em um tipo genérico, ele permanece um tipo de valor.

Para dar uma amostra, vamos supor que você tenha um Listtipo genérico com um argumento genérico. Em Java, List<String>e List<Int>acabará sendo exatamente o mesmo tipo em tempo de execução - os tipos genéricos realmente existem apenas para código em tempo de compilação. Todas as chamadas para, por exemplo GetValue, serão transformadas em (String)GetValuee (Int)GetValuerespectivamente.

Em C #, List<string>e List<int>existem dois tipos diferentes. Eles não são intercambiáveis ​​e sua segurança de tipo também é aplicada em tempo de execução. Não importa o que você faz, new List<int>().Add("SomeString")vai não trabalho - o armazenamento subjacente em List<int>é realmente alguns array de inteiros, enquanto em Java, é necessariamente uma objectmatriz. Em C #, não há elencos envolvidos, nem boxe etc.

Isso também deve deixar óbvio por que o C # não pode fazer a mesma coisa que o Java SomeType<?>. Em Java, todos os tipos genéricos "derivados de" SomeType<?>acabam sendo exatamente o mesmo tipo. No C #, todos os vários SomeType<T>s específicos são de seu próprio tipo separado. Removendo verificações em tempo de compilação, é possível passar em SomeType<Int>vez de SomeType<String>(e realmente, tudo o que isso SomeType<?>significa é "ignorar verificações em tempo de compilação para o tipo genérico especificado"). Em C #, não é possível, nem mesmo para tipos derivados (ou seja, você não pode fazer List<object> list = (List<object>)new List<string>();mesmo que stringseja derivado object).

Ambas as implementações têm seus prós e contras. Houve algumas vezes em que eu adoraria ser capaz de permitir SomeType<?>como argumento em C # - mas simplesmente não faz sentido o modo como os genéricos de C # funcionam.

Luaan
fonte
2
Bem, você pode usar os tipos List<>, Dictionary<,>e assim por diante, em C #, mas a diferença entre isso e uma determinada lista ou dicionário concreto requer um pouco de reflexão para colmatar. A variação nas interfaces ajuda em alguns dos casos em que poderíamos querer preencher essa lacuna facilmente, mas não todos.
Jon Hanna
2
@ JonHanna Você pode usar List<>para instanciar um novo tipo genérico específico - mas ainda significa criar o tipo específico que você deseja. Mas você não pode usar List<>como argumento, por exemplo. Mas sim, pelo menos isso permite que você preencha a lacuna usando a reflexão.
Luaan 7/08/15
O .NET Framework possui três restrições genéricas codificadas que não são tipos de local de armazenamento; todas as outras restrições devem ser do tipo de local de armazenamento. Além disso, as únicas vezes em que um tipo genérico Tpode satisfazer uma restrição do tipo local de armazenamento Usão quando Te Usão do mesmo tipo, ou Ué um tipo que pode conter uma referência a uma instância de T. Não seria possível ter significativamente um local de armazenamento do tipo, SomeType<?>mas em teoria seria possível ter uma restrição genérica desse tipo.
Supercat
1
Não é verdade que o bytecode Java compilado não tenha noção de genéricos. É que as instâncias de classe não têm noção de genéricos. Isso é uma diferença importante; Escrevi anteriormente sobre isso em programmers.stackexchange.com/questions/280169/… , se você estiver interessado.
Ruakh 8/08
2

Reificação é um conceito de modelagem orientada a objetos.

Reify é um verbo que significa "tornar algo abstrato real" .

Quando você faz programação orientada a objetos, é comum modelar objetos do mundo real como componentes de software (por exemplo, janela, botão, pessoa, banco, veículo etc.)

Também é comum reificar conceitos abstratos em componentes também (por exemplo, WindowListener, Broker, etc.)

duffymo
fonte
2
Reificação é um conceito geral de "tornar algo real" que, embora se aplique à modelagem orientada a objetos, como você diz, também tem um significado no contexto da implementação de genéricos.
9137 Jon Hanna
2
Então, eu fui educado lendo estas respostas. Vou alterar minha resposta.
Duffymo
2
Essa resposta não faz nada para abordar o interesse do OP em genéricos e polimorfismos paramétricos.
Erick G. Hagstrom
Este comentário não faz nada para atender ao interesse de alguém ou aumentar seu representante. Vejo que você não ofereceu nada. A minha foi a primeira resposta e definiu a reificação como algo mais amplo.
Duffymo
1
Sua resposta pode ter sido a primeira, mas você respondeu a uma pergunta diferente, não a solicitada pelo OP, que seria clara a partir do conteúdo da pergunta e de suas tags. Talvez você não tenha lido a pergunta completamente antes de escrever sua resposta, ou talvez não soubesse que o termo "reificação" tem um significado estabelecido no contexto dos genéricos. De qualquer forma, sua resposta não é útil. Voto negativo.
jcsahnwaldt Reinstate Monica