Por que precisamos de boxe e unboxing em c #?

325

Por que precisamos de boxe e unboxing em c #?

Eu sei o que é boxe e unboxing, mas não consigo compreender o uso real dele. Por que e onde devo usá-lo?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing
Vaibhav Jain
fonte

Respostas:

482

Por quê

Ter um sistema de tipos unificado e permitir que os tipos de valor tenham uma representação completamente diferente de seus dados subjacentes da maneira como os tipos de referência representam seus dados subjacentes (por exemplo, um inté apenas um intervalo de trinta e dois bits que é completamente diferente de uma referência tipo).

Pense nisso assim. Você tem uma variável odo tipo object. E agora você tem um inte deseja colocá-lo o. oé uma referência a algo em algum lugar, e intenfaticamente não é uma referência a algo em algum lugar (afinal, é apenas um número). Então, o que você faz é o seguinte: você cria um novo objectque pode armazenar o arquivo inte, em seguida, atribui uma referência a esse objeto o. Chamamos esse processo de "boxe".

Portanto, se você não se importa em ter um sistema de tipo unificado (ou seja, tipos de referência e tipos de valor têm representações muito diferentes e você não deseja uma maneira comum de "representar" os dois)), não precisará de boxe. Se você não se importa em intrepresentar o valor subjacente (ou seja, também tem inttipos de referência e apenas armazena uma referência ao valor subjacente), não precisa de boxe.

onde devo usá-lo.

Por exemplo, o tipo de coleção antigo ArrayListcome apenas objects. Ou seja, ele apenas armazena referências a algumas coisas que moram em algum lugar. Sem o boxe, você não pode colocar um intem tal coleção. Mas com o boxe, você pode.

Agora, nos dias dos genéricos, você realmente não precisa disso e geralmente pode se divertir sem pensar no problema. Mas há algumas ressalvas a serem observadas:

Isto está certo:

double e = 2.718281828459045;
int ee = (int)e;

Isso não é:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)o; // runtime exception

Em vez disso, você deve fazer isso:

double e = 2.718281828459045;
object o = e; // box
int ee = (int)(double)o;

Primeiro, temos que desmarcar explicitamente o double( (double)o) e depois convertê-lo em um int.

Qual é o resultado do seguinte:

double e = 2.718281828459045;
double d = e;
object o1 = d;
object o2 = e;
Console.WriteLine(d == e);
Console.WriteLine(o1 == o2);

Pense nisso por um segundo antes de passar para a próxima frase.

Se você disse Truee Falseótimo! Espere o que? Isso porque ==tipos de referência usam igualdade de referência que verifica se as referências são iguais, e não se os valores subjacentes são iguais. Este é um erro perigosamente fácil de cometer. Talvez ainda mais sutil

double e = 2.718281828459045;
object o1 = e;
object o2 = e;
Console.WriteLine(o1 == o2);

também imprimirá False!

Melhor dizer:

Console.WriteLine(o1.Equals(o2));

que, felizmente, será impresso True.

Uma última sutileza:

[struct|class] Point {
    public int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

Point p = new Point(1, 1);
object o = p;
p.x = 2;
Console.WriteLine(((Point)o).x);

Qual é a saída? Depende! Se Pointé a, structentão a saída é, 1mas se Pointé a class, a saída é 2! Uma conversão de boxe faz uma cópia do valor em caixa explicando a diferença de comportamento.

Jason
fonte
@ Jason Você quer dizer que, se tivermos listas primitivas, não há razão para usar qualquer boxe / unboxing?
Pacerier
Não sei ao certo o que você quer dizer com "lista primitiva".
jason
3
Você poderia falar com o impacto no desempenho de boxinge unboxing?
Kevin Meredith
@KevinMeredith, há uma explicação básica sobre o desempenho das operações de boxe e unboxing em msdn.microsoft.com/en-us/library/ms173196.aspx
InfZero
2
Excelente resposta - melhor do que a maioria das explicações que li em livros conceituados.
FredM
59

Na estrutura do .NET, existem duas espécies de tipos - tipos de valor e tipos de referência. Isso é relativamente comum em idiomas OO.

Um dos recursos importantes das linguagens orientadas a objetos é a capacidade de lidar com instâncias de maneira independente de tipo. Isso é conhecido como polimorfismo . Como queremos tirar proveito do polimorfismo, mas temos duas espécies diferentes de tipos, deve haver uma maneira de reuni-los para que possamos lidar com um ou outro da mesma maneira.

Agora, nos tempos antigos (1.0 do Microsoft.NET), não havia esse hullabaloo genérico novo. Você não poderia escrever um método que tivesse um único argumento que pudesse atender a um tipo de valor e um tipo de referência. Isso é uma violação do polimorfismo. Portanto, o boxe foi adotado como um meio de coagir um tipo de valor a um objeto.

Se isso não fosse possível, a estrutura estaria repleta de métodos e classes cujo único objetivo era aceitar as outras espécies do tipo. Não apenas isso, mas como os tipos de valor não compartilham verdadeiramente um ancestral de tipo comum, você teria que ter uma sobrecarga de método diferente para cada tipo de valor (bit, byte, int16, int32, etc etc etc).

O boxe impediu que isso acontecesse. E é por isso que os britânicos comemoram o Boxing Day.


fonte
1
Antes dos genéricos, o boxe automático era necessário para fazer muitas coisas; dada a existência de genéricos, no entanto, não fosse a necessidade de manter a compatibilidade com o código antigo, acho que o .net seria melhor sem conversões implícitas de boxe. Moldando um tipo de valor como List<string>.Enumeratorpara IEnumerator<string>os rendimentos um objecto que se comporta principalmente como um tipo de classe, mas com um quebrado Equalsmétodo. A melhor maneira de elenco List<string>.Enumeratorpara IEnumerator<string>seria chamar um operador de conversão personalizado, mas a existência de um impede de conversão implícita de que.
Supercat 26/09/12
42

A melhor maneira de entender isso é examinar as linguagens de programação de nível inferior que o C # se baseia.

Nos idiomas de nível mais baixo, como C, todas as variáveis ​​vão para um lugar: a pilha. Cada vez que você declara uma variável, ela fica na pilha. Eles podem ser apenas valores primitivos, como um bool, um byte, um int de 32 bits, um uint de 32 bits, etc. O Stack é simples e rápido. À medida que as variáveis ​​são adicionadas, elas são colocadas uma em cima da outra; assim, o primeiro que você declara fica em, digamos, 0x00, o próximo em 0x01, o próximo em 0x02 na RAM etc. Além disso, as variáveis ​​geralmente são pré-endereçadas na compilação- tempo, para que seu endereço seja conhecido antes mesmo de você executar o programa.

No próximo nível, como C ++, é introduzida uma segunda estrutura de memória chamada Heap. Você ainda vive principalmente na pilha, mas entradas especiais chamadas ponteiros podem ser adicionados à pilha, que armazenam o endereço de memória para o primeiro byte de um objeto e esse objeto vive na pilha. O Heap é uma bagunça e um tanto caro de manter, porque, diferentemente das variáveis ​​Stack, elas não se acumulam linearmente para cima e para baixo enquanto um programa é executado. Eles podem ir e vir em nenhuma sequência específica e podem crescer e encolher.

Lidar com indicadores é difícil. Eles são a causa de vazamentos de memória, saturação de buffer e frustração. C # para o resgate.

Em um nível mais alto, C #, você não precisa pensar em ponteiros - a estrutura .Net (escrita em C ++) pensa neles e apresenta-os como Referências a objetos e, para desempenho, permite armazenar valores mais simples como bools, bytes e ints como tipos de valor. Sob o capô, os Objetos e outras coisas que instanciam uma Classe ficam no caro Heap Gerenciado pela Memória, enquanto os Tipos de Valor ficam na mesma Pilha que você tinha no nível C - super-rápido.

Por uma questão de manter a interação entre esses dois conceitos fundamentalmente diferentes de memória (e estratégias de armazenamento) simples da perspectiva de um codificador, os Tipos de Valor podem ser encaixotados a qualquer momento. O boxe faz com que o valor seja copiado da pilha, colocado em um objeto e colocado na pilha - interação mais cara, porém fluida, com o mundo de referência. Como outras respostas apontam, isso ocorrerá quando você, por exemplo, disser:

bool b = false; // Cheap, on Stack
object o = b; // Legal, easy to code, but complex - Boxing!
bool b2 = (bool)o; // Unboxing!

Uma forte ilustração da vantagem do boxe é a verificação de nulo:

if (b == null) // Will not compile - bools can't be null
if (o == null) // Will compile and always return false

Nosso objeto o é tecnicamente um endereço na pilha que aponta para uma cópia do nosso bool b, que foi copiado para o heap. Podemos verificar o nulo porque o bool foi encaixotado e colocado lá.

Em geral, você deve evitar o Boxe, a menos que precise, por exemplo, para passar um int / bool / qualquer coisa como objeto para um argumento. Existem algumas estruturas básicas no .Net que ainda exigem a passagem de tipos de valor como objeto (e, portanto, exigem boxe), mas na maioria das vezes você nunca precisará usar o Box.

Uma lista não exaustiva de estruturas históricas de C # que exigem Boxe, que você deve evitar:

  • O sistema Event tem uma Condição de Corrida em uso ingênuo e não suporta assíncrono. Adicione o problema do boxe e provavelmente deve ser evitado. (Você pode substituí-lo, por exemplo, por um sistema de eventos assíncronos que usa genéricos.)

  • Os antigos modelos Threading e Timer forçaram uma caixa em seus parâmetros, mas foram substituídos por async / waitit, que são muito mais limpos e mais eficientes.

  • As coleções .Net 1.1 dependiam inteiramente do boxe, porque eram anteriores aos genéricos. Eles ainda estão funcionando no System.Collections. Em qualquer novo código, você deve usar as Coleções de System.Collections.Generic, que além de evitar o Boxing, também fornecem maior segurança de tipo .

Você deve evitar declarar ou passar seus Tipos de Valor como objetos, a menos que tenha que lidar com os problemas históricos acima que forçam o Boxing e queira evitar o impacto no desempenho do Boxing posteriormente, quando souber que ele será Boxed de qualquer maneira.

De acordo com a sugestão de Mikael abaixo:

Faça isso

using System.Collections.Generic;

var employeeCount = 5;
var list = new List<int>(10);

Isso não

using System.Collections;

Int32 employeeCount = 5;
var list = new ArrayList(10);

Atualizar

Esta resposta originalmente sugerida Int32, Bool etc causa boxe, quando na verdade eles são aliases simples para Tipos de Valor. Ou seja, .Net possui tipos como Bool, Int32, String e C # os aliases para bool, int, string, sem nenhuma diferença funcional.

Chris Moschini
fonte
4
Você me ensinou o que cem programadores e profissionais de TI não conseguem explicar em anos, mas mude para dizer o que você deve fazer em vez de o que evitar, porque ficou um pouco difícil de seguir. você não deve fazer isso, em vez disso, faça
Mikael Puusaari
2
Esta resposta deveria ter sido marcada como RESPOSTA cem vezes!
Pouyan
3
não há "Int" em c #, há int e Int32. Eu acredito que você está errado em afirmar que um é um tipo de valor e o outro é um tipo de referência que envolve o tipo de valor. a menos que eu esteja enganado, isso é verdade em Java, mas não em C #. Em C #, os que aparecem em azul no IDE são aliases para sua definição de estrutura. Então: int = Int32, bool = Boolean, string = String. O motivo para usar bool sobre booleano é porque é sugerido nas diretrizes e convenções de design do MSDN. Caso contrário, eu amo esta resposta. Mas vou votar até você me provar errado ou corrigir isso em sua resposta.
Heriberto Lugo 21/01
2
Se você declarar uma variável como int e outra como Int32, ou bool e Boolean - clique com o botão direito do mouse e visualize a definição, você terminará na mesma definição para uma estrutura.
Heriberto Lugo 21/01
2
@HeribertoLugo está correto, a linha "Você deve evitar declarar seus Tipos de Valor como Bool em vez de bool" está errada. Como aponta o OP, você deve evitar declarar seu bool (ou booleano ou qualquer outro tipo de valor) como Objeto. bool / Boolean, int / Int32, são apenas aliases entre C # e .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW
21

O boxe não é realmente algo que você usa - é algo que o tempo de execução usa para que você possa manipular os tipos de referência e valor da mesma maneira, quando necessário. Por exemplo, se você usou um ArrayList para armazenar uma lista de números inteiros, os números inteiros foram encaixotados para caber nos slots de tipo de objeto no ArrayList.

Usando coleções genéricas agora, isso praticamente desaparece. Se você criar um List<int>, não haverá boxe - o List<int>pode conter os números inteiros diretamente.

Raio
fonte
Você ainda precisa de boxe para coisas como formatação de string composta. Você pode não vê-lo com tanta frequência ao usar genéricos, mas definitivamente ainda está lá.
Jeremy S
1
verdade - ele mostra-se todo o tempo em ADO.NET também - os valores dos parâmetros SQL são todos 'objeto é não importa o que o tipo de dados real é
Ray
11

Boxe e Unboxing são usados ​​especificamente para tratar objetos do tipo valor como tipo de referência; movendo seu valor real para o heap gerenciado e acessando seu valor por referência.

Sem boxe e unboxing, você nunca poderia passar tipos de valor por referência; e isso significa que você não pode passar tipos de valor como instâncias de Object.

STW
fonte
resposta ainda agradável depois de quase 10 anos sir +1
snr 12/02
1
A passagem por referência de tipos numéricos existe em idiomas sem boxe, e outros idiomas implementam o tratamento de tipos de valor como instâncias de Object sem boxe e movendo o valor para o heap (por exemplo, implementações de idiomas dinâmicos em que os ponteiros estão alinhados aos limites de 4 bytes usam os quatro inferiores) bits de referências para indicar que o valor é um número inteiro ou símbolo, e não um objeto completo; esses tipos de valor são imutáveis ​​e têm o mesmo tamanho de um ponteiro).
Pete Kirkham
8

O último lugar em que tive que desmarcar alguma coisa foi ao escrever um código que recuperava alguns dados de um banco de dados (eu não estava usando LINQ to SQL , apenas o ADO.NET antigo ):

int myIntValue = (int)reader["MyIntValue"];

Basicamente, se você estiver trabalhando com APIs mais antigas antes dos genéricos, encontrará boxe. Fora isso, não é tão comum.

BFree
fonte
4

O boxe é obrigatório, quando temos uma função que precisa de um objeto como parâmetro, mas temos diferentes tipos de valores que precisam ser passados; nesse caso, precisamos primeiro converter os tipos de valor em tipos de dados do objeto antes de passá-lo para a função.

Eu não acho que isso é verdade, tente isso:

class Program
    {
        static void Main(string[] args)
        {
            int x = 4;
            test(x);
        }

        static void test(object o)
        {
            Console.WriteLine(o.ToString());
        }
    }

Isso funciona muito bem, eu não usei boxe / unboxing. (A menos que o compilador faça isso nos bastidores?)

Manoj
fonte
Isso porque tudo herda de System.Object e você está dando ao método um objeto com informações extras, então basicamente você está chamando o método de teste com o que ele está esperando e com o que ele pode esperar, já que não espera nada em particular. Um monte em .NET é feito nos bastidores, ea razão pela qual é uma linguagem muito simples de usar
Mikael Puusaari
1

No .net, toda instância de Object, ou qualquer tipo derivado, inclui uma estrutura de dados que contém informações sobre seu tipo. Os tipos de valores "reais" em .net não contêm essas informações. Para permitir que dados em tipos de valor sejam manipulados por rotinas que esperam receber tipos derivados de objeto, o sistema define automaticamente para cada tipo de valor um tipo de classe correspondente com os mesmos membros e campos. O boxe cria uma nova instância desse tipo de classe, copiando os campos de uma instância de tipo de valor. Desmarcar a cópia copia os campos de uma instância do tipo de classe para uma instância do tipo de valor. Todos os tipos de classe criados a partir de tipos de valor são derivados da classe ValueType chamada ironicamente (que, apesar do nome, na verdade é um tipo de referência).

supercat
fonte
0

Quando um método usa apenas um tipo de referência como parâmetro (digamos, um método genérico restrito a ser uma classe através da newrestrição), você não poderá passar um tipo de referência para ele e terá que encaixotá-lo.

Isto também é verdade para todos os métodos que usam objectcomo parâmetro - o que irá tem de ser um tipo de referência.

Oded
fonte
0

Em geral, você normalmente deseja evitar o boxe de seus tipos de valor.

No entanto, existem ocorrências raras em que isso é útil. Se você precisar direcionar a estrutura 1.1, por exemplo, não terá acesso às coleções genéricas. Qualquer uso das coleções no .NET 1.1 exigiria o tratamento do seu tipo de valor como System.Object, o que causa boxe / unboxing.

Ainda há casos para que isso seja útil no .NET 2.0+. Sempre que você quiser tirar proveito do fato de que todos os tipos, incluindo tipos de valor, podem ser tratados diretamente como um objeto, pode ser necessário usar boxe / unboxing. Às vezes, isso pode ser útil, pois permite salvar qualquer tipo em uma coleção (usando o objeto em vez de T em uma coleção genérica), mas, em geral, é melhor evitar isso, pois você está perdendo a segurança do tipo. Porém, o único caso em que o boxe ocorre com frequência é quando você está usando o Reflection - muitas das chamadas em reflexão exigem boxe / unboxing ao trabalhar com tipos de valor, pois o tipo não é conhecido antecipadamente.

Hunain
fonte
0

Boxe é a conversão de um valor em um tipo de referência com os dados em algum deslocamento em um objeto na pilha.

Quanto ao que o boxe realmente faz. Aqui estão alguns exemplos

Mono C ++

void* mono_object_unbox (MonoObject *obj)
 {    
MONO_EXTERNAL_ONLY_GC_UNSAFE (void*, mono_object_unbox_internal (obj));
 }

#define MONO_EXTERNAL_ONLY_GC_UNSAFE(t, expr) \
    t result;       \
    MONO_ENTER_GC_UNSAFE;   \
    result = expr;      \
    MONO_EXIT_GC_UNSAFE;    \
    return result;

static inline gpointer
mono_object_get_data (MonoObject *o)
{
    return (guint8*)o + MONO_ABI_SIZEOF (MonoObject);
}

#define MONO_ABI_SIZEOF(type) (MONO_STRUCT_SIZE (type))
#define MONO_STRUCT_SIZE(struct) MONO_SIZEOF_ ## struct
#define MONO_SIZEOF_MonoObject (2 * MONO_SIZEOF_gpointer)

typedef struct {
    MonoVTable *vtable;
    MonoThreadsSync *synchronisation;
} MonoObject;

Desembalar em Mono é um processo de conversão de um ponteiro em um deslocamento de 2 ponteiros no objeto (por exemplo, 16 bytes). A gpointeré a void*. Isso faz sentido quando se olha para a definição MonoObject, pois é claramente apenas um cabeçalho para os dados.

C ++

Para colocar um valor em C ++, você pode fazer algo como:

#include <iostream>
#define Object void*

template<class T> Object box(T j){
  return new T(j);
}

template<class T> T unbox(Object j){
  T temp = *(T*)j;
  delete j;
  return temp;
}

int main() {
  int j=2;
  Object o = box(j);
  int k = unbox<int>(o);
  std::cout << k;
}
Lewis Kelsey
fonte