Ocorrência de boxe em C #

86

Estou tentando coletar todas as situações em que o boxe ocorre em C #:

  • Convertendo tipo de valor em System.Objecttipo:

    struct S { }
    object box = new S();
    
  • Convertendo tipo de valor em System.ValueTypetipo:

    struct S { }
    System.ValueType box = new S();
    
  • Convertendo o valor do tipo de enumeração em System.Enumtipo:

    enum E { A }
    System.Enum box = E.A;
    
  • Convertendo o tipo de valor em referência de interface:

    interface I { }
    struct S : I { }
    I box = new S();
    
  • Usando tipos de valor na concatenação de string C #:

    char c = F();
    string s1 = "char value will box" + c;
    

    nota: constantes de chartipo são concatenadas em tempo de compilação

    nota: desde a versão 6.0 C # compilador otimiza concatenação envolvendo bool, char, IntPtr, UIntPtrtipos

  • Criando delegado a partir do método de instância do tipo de valor:

    struct S { public void M() {} }
    Action box = new S().M;
    
  • Chamando métodos virtuais não substituídos em tipos de valor:

    enum E { A }
    E.A.GetHashCode();
    
  • Usando padrões de constantes C # 7.0 sob isexpressão:

    int x = …;
    if (x is 42) { … } // boxes both 'x' and '42'!
    
  • Encaixe em conversões de tipos de tupla C #:

    (int, byte) _tuple;
    
    public (object, object) M() {
      return _tuple; // 2x boxing
    }
    
  • Parâmetros opcionais de objecttipo com valores padrão de tipo de valor:

    void M([Optional, DefaultParameterValue(42)] object o);
    M(); // boxing at call-site
    
  • Verificando o valor do tipo genérico irrestrito para null:

    bool M<T>(T t) => t != null;
    string M<T>(T t) => t?.ToString(); // ?. checks for null
    M(42);
    

    observação: isso pode ser otimizado por JIT em alguns tempos de execução .NET

  • Valor de teste de structtipo de tipo irrestrito ou genérico com is/ asoperadores:

    bool M<T>(T t) => t is int;
    int? M<T>(T t) => t as int?;
    IEquatable<T> M<T>(T t) => t as IEquatable<T>;
    M(42);
    

    observação: isso pode ser otimizado por JIT em alguns tempos de execução .NET

Existem mais situações de boxe, talvez escondidas, que você conheça?

controle de fluxo
fonte
2
Lidei com isso há algum tempo, e achei bastante interessante: Detectando (des) boxe usando FxCop
George Duckett
Muito boas formas de conversão que você mostrou. De fato, eu não conhecia o segundo ou talvez nunca tenha experimentado :) Obrigado
Zenwalker
12
deveria ser questão wiki da comunidade
Sly
2
E quanto aos tipos anuláveis? private int? nullableInteger
allansson
1
@allansson, tipos anuláveis ​​são apenas tipos de valores
controlflow

Respostas:

42

Essa é uma ótima pergunta!

O boxing ocorre exatamente por um motivo: quando precisamos de uma referência a um tipo de valor . Tudo o que você listou se enquadra nesta regra.

Por exemplo, uma vez que objeto é um tipo de referência, converter um tipo de valor para objeto requer uma referência a um tipo de valor, o que causa boxe.

Se você deseja listar todos os cenários possíveis, você também deve incluir derivados, como retornar um tipo de valor de um método que retorna um objeto ou um tipo de interface, porque isso converte automaticamente o tipo de valor para o objeto / interface.

A propósito, o caso de concatenação de string que você identificou astutamente também deriva da conversão para o objeto. O operador + é traduzido pelo compilador para uma chamada ao método Concat de string, que aceita um objeto para o tipo de valor que você passa, portanto, a conversão para o objeto e, portanto, ocorre o boxing.

Com o passar dos anos, sempre aconselhei os desenvolvedores a lembrar o único motivo do boxe (eu especifiquei acima) em vez de memorizar cada caso, porque a lista é longa e difícil de lembrar. Isso também promove a compreensão de qual código IL o compilador gera para nosso código C # (por exemplo, + em string produz uma chamada para String.Concat). Quando você estiver em dúvida sobre o que o compilador gera e se ocorrer um boxing, você pode usar o IL Disassembler (ILDASM.exe). Normalmente, você deve procurar o opcode da caixa (há apenas um caso em que o boxing pode ocorrer mesmo que o IL não inclua o opcode da caixa, mais detalhes abaixo).

Mas concordo que algumas ocorrências de boxe são menos óbvias. Você listou um deles: chamar um método não substituído de um tipo de valor. Na verdade, isso é menos óbvio por outro motivo: quando você verifica o código IL, você não vê o opcode da caixa, mas o opcode da restrição, então mesmo no IL não é óbvio que o boxing aconteça! Não vou entrar em detalhes exatos por que evitar que esta resposta se torne ainda mais longa ...

Outro caso de boxe menos óbvio é ao chamar um método de classe base de uma estrutura. Exemplo:

struct MyValType
{
    public override string ToString()
    {
        return base.ToString();
    }
}

Aqui, ToString é substituído, portanto, chamar ToString em MyValType não gerará boxing. No entanto, a implementação chama o ToString base e isso causa boxe (verifique o IL!).

A propósito, esses dois cenários de boxe não óbvios também derivam da única regra acima. Quando um método é chamado na classe base de um tipo de valor, deve haver algo para a palavra-chave this se referir. Visto que a classe base de um tipo de valor é (sempre) um tipo de referência, a palavra - chave this deve se referir a um tipo de referência e, portanto, precisamos de uma referência a um tipo de valor e, portanto, o boxing ocorre devido à regra única.

Aqui está um link direto para a seção do meu curso online .NET que discute boxe em detalhes: http://motti.me/mq

Se você está interessado apenas em cenários de boxe mais avançados, aqui está um link direto (embora o link acima o leve até lá também, uma vez que discute o material mais básico): http://motti.me/mu

Eu espero que isso ajude!

Motti

Motti Shaked
fonte
1
Se a ToString()for chamado em um tipo de valor específico que não o substitui, o tipo de valor será encaixotado no local da chamada ou o método será despachado (sem encaixotamento) para uma substituição gerada automaticamente que não faz nada além de encadear (com boxe) para o método básico?
supercat
@supercat Chamar qualquer método que chame baseum tipo de valor causará boxing. Isso inclui métodos virtuais que não são substituídos pela estrutura e os Objectmétodos que não são virtuais (como GetType()). Veja esta pergunta .
Şafak Gür
@ ŞafakGür: Eu sei que o resultado final será o boxe. Eu queria saber o mecanismo exato pelo qual isso acontece. Visto que o compilador que gera IL pode não saber se o tipo é um tipo de valor ou referência (pode ser genérico), ele irá gerar um callvirt independentemente. O JITter saberia se o tipo é um tipo de valor e se ele substitui ToString, então ele poderia gerar um código de site de chamada para fazer o boxing; alternativamente, ele pode gerar automaticamente para cada estrutura que não substitui ToStringum mehtod public override void ToString() { return base.ToString(); }e ...
supercat
... fazer o boxe ocorrer dentro desse método. Como o método seria muito curto, ele poderia ser alinhado. Fazer coisas com a última abordagem permitiria que o ToString()método de uma estrutura fosse acessado via Reflection como qualquer outro e usado para criar um delegado estático que toma o tipo de estrutura como refparâmetro [tal coisa funciona com métodos de estrutura não herdados], mas eu apenas tentei criar um delegado e não funcionou. É possível criar um delegado estático para o ToString()método de uma estrutura e, em caso afirmativo, qual deve ser o tipo de parâmetro?
supercat
Links estão quebrados.
OfirD
5

Chamando o método GetType () não virtual no tipo de valor:

struct S { };
S s = new S();
s.GetType();
Viacheslav Ivanov
fonte
2
GetTyperequer boxing não apenas porque não é virtual, mas porque os locais de armazenamento de tipo de valor, ao contrário dos objetos de heap, não têm um campo "oculto" que GetType()pode ser usado para identificar seu tipo.
supercat
@supercat Hmmm. 1. Boxe adicionado pelo compilador e campo oculto usado pelo tempo de execução. Pode ser o compilador adicionar boxing porque sabe sobre o tempo de execução ... 2. Quando chamamos ToString (string) não virtual no valor enum, também requer boxing e não acredito que o compilador adicione isso porque sabe sobre os detalhes de implementação Enum.ToString (string) . Assim é, penso eu, posso dizer que o boxe sempre ocorreu quando o método não virtual no "tipo de valor base" chamado.
Viacheslav Ivanov
Eu não tinha considerado Enumter nenhum método não virtual próprio, embora um ToString()método para um Enumprecise ter acesso para digitar informações. Eu me pergunto se Object, ValueTypeou Enumtem algum método não virtual que possa executar seus trabalhos sem informações de tipo.
supercat de
3

Mencionado na resposta de Motti, apenas ilustrando com exemplos de código:

Parâmetros envolvidos

public void Bla(object obj)
{

}

Bla(valueType)

public void Bla(IBla i) //where IBla is interface
{

}

Bla(valueType)

Mas isso é seguro:

public void Bla<T>(T obj) where T : IBla
{

}

Bla(valueType)

Tipo de retorno

public object Bla()
{
    return 1;
}

public IBla Bla() //IBla is an interface that 1 inherits
{
    return 1;
}

Verificando T irrestrito contra nulo

public void Bla<T>(T obj)
{
    if (obj == null) //boxes.
}

Uso de dinâmica

dynamic x = 42; (boxes)

Outro

enumValue.HasFlag

Nawfal
fonte
0
  • Usando as coleções não genéricas em System.Collectionscomo ArrayListou HashTable.

Concedido, essas são instâncias específicas do seu primeiro caso, mas podem ser pegadinhas ocultas. É incrível a quantidade de código que ainda encontro hoje que usa esses em vez de List<T>e Dictionary<TKey,TValue>.

Jesse C. Slicer
fonte
0

Adicionar qualquer valor de tipo de valor ao ArrayList causa boxe:

ArrayList items = ...
numbers.Add(1); // boxing to object
sll
fonte