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
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
Respostas:
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 o
do tipo object
. E agora você tem um int
e deseja colocá-lo o
. o
é uma referência a algo em algum lugar, e int
enfaticamente 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 object
que pode armazenar o arquivo int
e, 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 int
representar o valor subjacente (ou seja, também tem int
tipos 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 ArrayList
come apenas object
s. Ou seja, ele apenas armazena referências a algumas coisas que moram em algum lugar. Sem o boxe, você não pode colocar um int
em 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 True
e 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, struct
então a saída é, 1
mas 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.
boxing
e unboxing
?
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.
List<string>.Enumerator
para IEnumerator<string>
os rendimentos um objecto que se comporta principalmente como um tipo de classe, mas com um quebrado Equals
método. A melhor maneira de elenco List<string>.Enumerator
para IEnumerator<string>
seria chamar um operador de conversão personalizado, mas a existência de um impede de conversão implícita de que.
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:
using System.Collections.Generic;
var employeeCount = 5;
var list = new List<int>(10);
using System.Collections;
Int32 employeeCount = 5;
var list = new ArrayList(10);
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.
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.
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.
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.
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?)
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).
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 new
restriçã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 object
como parâmetro - o que irá tem de ser um tipo de referência.
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.
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;
}