Pourquoi avons-nous besoin de boxe et de déballage en C #?


325

Pourquoi avons-nous besoin de boxe et de déballage en C #?

Je sais ce qu'est la boxe et le déballage, mais je ne peux pas en comprendre l'utilisation réelle. Pourquoi et où dois-je l'utiliser?

short s = 25;

object objshort = s;  //Boxing

short anothershort = (short)objshort;  //Unboxing

Réponses:


482

Pourquoi

Pour avoir un système de types unifié et permettre aux types de valeurs d'avoir une représentation complètement différente de leurs données sous-jacentes par rapport à la façon dont les types de référence représentent leurs données sous-jacentes (par exemple, an intest juste un ensemble de trente-deux bits qui est complètement différent d'une référence type).

Pensez-y comme ça. Vous avez une variable ode type object. Et maintenant vous en avez un intet vous voulez le mettre o. oest une référence à quelque chose quelque part, et ce intn'est absolument pas une référence à quelque chose quelque part (après tout, c'est juste un nombre). Donc, ce que vous faites est le suivant: vous créez un nouveau objectqui peut stocker le int, puis vous affectez une référence à cet objet o. Nous appelons ce processus «boxe».

Donc, si vous ne vous souciez pas d'avoir un système de types unifié (c'est-à-dire que les types de référence et les types de valeurs ont des représentations très différentes et que vous ne voulez pas une façon commune de "représenter" les deux), alors vous n'avez pas besoin de boxe. Si vous ne vous souciez pas d'avoir intreprésenté leur valeur sous-jacente (c.-à-d., Avoir à la place des inttypes de référence aussi et simplement stocker une référence à leur valeur sous-jacente), vous n'avez pas besoin de boxe.

où dois-je l'utiliser.

Par exemple, l'ancien type de collection ArrayListne mange que objects. Autrement dit, il ne stocke que des références à quelque chose qui vit quelque part. Sans boxe, vous ne pouvez pas mettre un intdans une telle collection. Mais avec la boxe, c'est possible.

Maintenant, à l'époque des génériques, vous n'avez pas vraiment besoin de cela et pouvez généralement aller gaiement sans réfléchir au problème. Mais il y a quelques mises en garde à prendre en compte:

C'est correct:

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

Ce n'est pas:

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

Au lieu de cela, vous devez faire ceci:

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

Nous devons d'abord décompresser explicitement le double( (double)o), puis le convertir en un int.

Quel est le résultat de ce qui suit:

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

Pensez-y une seconde avant de passer à la phrase suivante.

Si vous avez dit Trueet Falsesuper! Attends quoi? C'est parce que ==sur les types de référence utilise l'égalité de référence qui vérifie si les références sont égales, pas si les valeurs sous-jacentes sont égales. C'est une erreur dangereusement facile à faire. Peut-être encore plus subtil

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

imprimera également False!

Mieux vaut dire:

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

qui sera ensuite, heureusement, imprimé True.

Une dernière subtilité:

[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);

Quelle est la sortie? Ça dépend! Si Pointest un structalors la sortie est 1mais si Pointest un classalors la sortie est 2! Une conversion de boxe crée une copie de la valeur encadrée expliquant la différence de comportement.


@Jason Voulez-vous dire que si nous avons des listes primitives, il n'y a aucune raison d'utiliser de la boxe / unboxing?
Pacerier

Je ne sais pas ce que vous entendez par "liste primitive".
jason

3
Pourriez-vous s'il vous plaît parler de l'impact des performances de boxinget unboxing?
Kevin Meredith

@KevinMeredith il y a une explication de base sur les performances des opérations de boxe et de déballage
InfZero

2
Excellente réponse - meilleure que la plupart des explications que j'ai lues dans des livres réputés.
FredM

59

Dans le framework .NET, il existe deux types de types: les types de valeur et les types de référence. Ceci est relativement courant dans les langues OO.

L'une des caractéristiques importantes des langages orientés objet est la capacité de gérer les instances de manière agnostique. C'est ce qu'on appelle le polymorphisme . Puisque nous voulons profiter du polymorphisme, mais que nous avons deux espèces différentes de types, il doit y avoir un moyen de les réunir afin que nous puissions gérer l'un ou l'autre de la même manière.

Maintenant, dans les temps anciens (1.0 de Microsoft.NET), il n'y avait pas ce hullabaloo générique nouveau. Vous ne pouviez pas écrire une méthode ayant un seul argument pouvant servir un type de valeur et un type de référence. C'est une violation du polymorphisme. La boxe a donc été adoptée comme moyen de contraindre un type de valeur en un objet.

Si cela n'était pas possible, le cadre serait jonché de méthodes et de classes dont le seul but était d'accepter les autres espèces de type. Non seulement cela, mais comme les types de valeurs ne partagent pas vraiment un ancêtre de type commun, vous devez avoir une surcharge de méthode différente pour chaque type de valeur (bit, octet, int16, int32, etc etc etc).

La boxe a empêché cela de se produire. Et c'est pourquoi les Britanniques célèbrent le lendemain de Noël.


1
Avant les génériques, la boxe automatique était nécessaire pour faire beaucoup de choses; étant donné l'existence de génériques, cependant, sans la nécessité de maintenir la compatibilité avec l'ancien code, je pense que .net serait mieux sans conversion de boxe implicite. La conversion d'un type de valeur comme List<string>.Enumeratorpour IEnumerator<string>produire un objet qui se comporte principalement comme un type de classe, mais avec une Equalsméthode cassée . Une meilleure façon de convertir List<string>.Enumeratoren IEnumerator<string>serait d'appeler un opérateur de conversion personnalisé, mais l'existence d'une conversion implicite empêche cela.
supercat

42

La meilleure façon de comprendre cela est d'examiner les langages de programmation de niveau inférieur sur lesquels C # s'appuie.

Dans les langages de niveau le plus bas comme C, toutes les variables vont au même endroit: la pile. Chaque fois que vous déclarez une variable, elle va sur la pile. Il ne peut s'agir que de valeurs primitives, comme un booléen, un octet, un entier 32 bits, un uint 32 bits, etc. La pile est à la fois simple et rapide. Au fur et à mesure que des variables sont ajoutées, elles vont juste l'une au-dessus de l'autre, donc la première que vous déclarez se trouve à dire, 0x00, la suivante à 0x01, la suivante à 0x02 en RAM, etc. En outre, les variables sont souvent pré-adressées lors de la compilation- temps, donc leur adresse est connue avant même d'exécuter le programme.

Au niveau suivant, comme C ++, une deuxième structure de mémoire appelée Heap est introduite. Vous vivez toujours principalement dans la pile, mais des entrées spéciales appelées pointeurs peuvent être ajoutées à la pile, qui stockent l'adresse mémoire pour le premier octet d'un objet, et cet objet vit dans le tas. Le tas est une sorte de gâchis et quelque peu coûteux à entretenir, car contrairement aux variables de pile, elles ne s'empilent pas de façon linéaire vers le haut et vers le bas pendant l'exécution d'un programme. Ils peuvent aller et venir sans séquence particulière, et ils peuvent grandir et rétrécir.

Il est difficile de gérer les pointeurs. Ils sont à l'origine de fuites de mémoire, de dépassements de mémoire tampon et de frustration. C # à la rescousse.

À un niveau supérieur, C #, vous n'avez pas besoin de penser aux pointeurs - le framework .Net (écrit en C ++) y pense pour vous et vous les présente comme des références aux objets, et pour les performances, vous permet de stocker des valeurs plus simples comme bools, octets et ints comme types de valeur. Sous le capot, les objets et les éléments qui instancient une classe se trouvent sur le tas coûteux géré par la mémoire, tandis que les types de valeur vont dans la même pile que vous aviez en C de bas niveau - super rapide.

Par souci de simplifier l'interaction entre ces 2 concepts fondamentalement différents de la mémoire (et stratégies de stockage) du point de vue du codeur, les types de valeur peuvent être encadrés à tout moment. Avec la boxe, la valeur est copiée de la pile, placée dans un objet et placée sur le tas - interaction plus coûteuse mais fluide avec le monde de référence. Comme d'autres réponses le soulignent, cela se produit lorsque vous dites par exemple:

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

Une illustration forte de l'avantage de la boxe est une vérification de null:

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

Notre objet o est techniquement une adresse dans la pile qui pointe vers une copie de notre bool b, qui a été copiée dans le tas. Nous pouvons vérifier o pour null car le bool a été mis en boîte et mis là.

En général, vous devez éviter la boxe sauf si vous en avez besoin, par exemple pour passer un int / bool / que ce soit en tant qu'objet à un argument. Il existe certaines structures de base dans .Net qui exigent toujours le passage de types de valeur en tant qu'objet (et nécessitent donc la boxe), mais pour la plupart, vous ne devriez jamais avoir besoin de Boxer.

Une liste non exhaustive des structures C # historiques qui nécessitent la boxe, que vous devez éviter:

  • Le système d'événement se révèle avoir une condition de race dans son utilisation naïve, et il ne prend pas en charge l'async. Ajoutez le problème de boxe et cela devrait probablement être évité. (Vous pouvez le remplacer par exemple par un système d'événement asynchrone qui utilise des génériques.)

  • Les anciens modèles Threading et Timer ont forcé une boîte sur leurs paramètres mais ont été remplacés par async / wait qui sont beaucoup plus propres et plus efficaces.

  • Les collections .Net 1.1 reposaient entièrement sur la boxe, car elles étaient antérieures aux génériques. Ceux-ci sont toujours en marche dans System.Collections. Dans tout nouveau code, vous devez utiliser les collections de System.Collections.Generic, qui en plus d'éviter la boxe, vous offrent également une sécurité de type plus forte .

Vous devez éviter de déclarer ou de passer vos types de valeurs en tant qu'objets, à moins que vous n'ayez à faire face aux problèmes historiques ci-dessus qui forcent la boxe, et que vous souhaitiez éviter les performances de boxe plus tard lorsque vous saurez que cela va être boxé de toute façon.

Selon la suggestion de Mikael ci-dessous:

Faites ceci

using System.Collections.Generic;

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

Pas ça

using System.Collections;

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

Mettre à jour

Cette réponse a initialement suggéré que Int32, Bool, etc. provoquent la boxe, alors qu'en fait ce sont de simples alias pour les types de valeur. C'est-à-dire que .Net a des types comme Bool, Int32, String et C # qui les alias bool, int, string, sans aucune différence fonctionnelle.


4
Vous m'a appris ce que une centaine de programmeurs et IT professionnels expliquent les années couldnt, mais le changement de dire ce que vous devez faire au lieu de ce qu'il faut éviter, car il a un peu difficile à suivre .. règles de base ne marche pas le plus souvent aller 1
.vous

2
Cette réponse aurait dû être marquée comme RÉPONSE des centaines de fois!
Pouyan

3
il n'y a pas "Int" en c #, il y a int et Int32. je crois que vous avez tort de déclarer que l'un est un type de valeur et que l'autre est un type de référence enveloppant le type de valeur. à moins que je ne me trompe, c'est vrai en Java, mais pas en C #. En C #, ceux qui apparaissent en bleu dans l'EDI sont des alias pour leur définition de structure. Donc: int = Int32, bool = Boolean, string = String. La raison d'utiliser bool sur booléen est parce qu'il est suggéré comme tel dans les directives et conventions de conception MSDN. Sinon, j'adore cette réponse. Mais je vais voter jusqu'à ce que vous me prouviez le contraire ou que vous corrigiez cela dans votre réponse.
Heriberto Lugo

2
Si vous déclarez une variable comme int et une autre comme Int32, ou bool et booléen - clic droit et définition de la vue, vous vous retrouverez dans la même définition pour une structure.
Heriberto Lugo

2
@HeribertoLugo est correct, la ligne "Vous devez éviter de déclarer vos types de valeur comme booléen au lieu de booléen" est erronée. Comme le souligne OP, vous devez éviter de déclarer votre booléen (ou booléen, ou tout autre type de valeur) en tant qu'objet. bool / Boolean, int / Int32, ne sont que des alias entre C # et .NET: docs.microsoft.com/en-us/dotnet/csharp/language-reference/…
STW

21

La boxe n'est pas vraiment quelque chose que vous utilisez - c'est quelque chose que le runtime utilise pour que vous puissiez gérer les types de référence et de valeur de la même manière si nécessaire. Par exemple, si vous avez utilisé une ArrayList pour contenir une liste d'entiers, les entiers ont été encadrés pour tenir dans les emplacements de type objet de l'ArrayList.

En utilisant des collections génériques maintenant, cela disparaît à peu près. Si vous créez un List<int>, aucune boxe n'est effectuée - le List<int>peut contenir les entiers directement.


Vous avez toujours besoin de boxe pour des choses comme le formatage de chaînes composites. Vous ne le verrez peut-être pas aussi souvent lorsque vous utilisez des génériques, mais il est certainement toujours là.
Jeremy S

1
true - il apparaît tout le temps dans ADO.NET aussi - les valeurs des paramètres sql sont toutes des 'objets quel que soit le type de données réel
Ray

11

Boxing et Unboxing sont spécifiquement utilisés pour traiter les objets de type valeur comme type de référence; déplacer leur valeur réelle vers le tas géré et accéder à leur valeur par référence.

Sans boxing et unboxing, vous ne pourriez jamais transmettre des types de valeur par référence; et cela signifie que vous ne pouvez pas passer des types de valeurs comme instances d'Object.


toujours une belle réponse après presque 10 ans monsieur +1
snr

1
Le passage par référence de types numériques existe dans les langues sans boxing, et d'autres langages implémentent le traitement des types de valeur comme des instances d'Object sans boxing et le déplacement de la valeur vers le tas (par exemple, les implémentations de langages dynamiques où les pointeurs sont alignés sur des limites de 4 octets utilisent les quatre inférieurs) des bits de références pour indiquer que la valeur est un entier ou un symbole plutôt qu'un objet complet; ces types de valeurs sont immuables et de la même taille qu'un pointeur).
Pete Kirkham

8

Le dernier endroit où j'ai dû déballer quelque chose était lors de l'écriture de code qui récupérait certaines données d'une base de données (je n'utilisais pas LINQ to SQL , tout simplement l'ancien ADO.NET ):

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

Fondamentalement, si vous travaillez avec des API plus anciennes avant les génériques, vous rencontrerez de la boxe. A part ça, ce n'est pas si courant.


4

La boxe est requise, lorsque nous avons une fonction qui a besoin d'un objet en tant que paramètre, mais que nous avons différents types de valeurs qui doivent être transmis, dans ce cas, nous devons d'abord convertir les types de valeurs en types de données d'objet avant de les transmettre à la fonction.

Je ne pense pas que ce soit vrai, essayez plutôt ceci:

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

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

Cela fonctionne très bien, je n'ai pas utilisé de boxe / unboxing. (À moins que le compilateur ne fasse cela en coulisses?)


C'est parce que tout hérite de System.Object, et vous donnez à la méthode un objet avec des informations supplémentaires, donc en gros vous appelez la méthode de test avec ce qu'elle attend et tout ce à quoi elle peut s'attendre puisqu'elle n'attend rien de particulier. Beaucoup de choses en .NET se font dans les coulisses, et la raison pour laquelle c'est un langage très simple à utiliser
Mikael Puusaari

1

Dans .net, chaque instance d'Object, ou tout type dérivé de celui-ci, comprend une structure de données qui contient des informations sur son type. Les types de valeur "réels" dans .net ne contiennent pas de telles informations. Pour permettre aux données des types de valeur d'être manipulées par des routines qui s'attendent à recevoir des types dérivés de l'objet, le système définit automatiquement pour chaque type de valeur un type de classe correspondant avec les mêmes membres et champs. Boxing crée une nouvelle instance de ce type de classe, copiant les champs d'une instance de type valeur. Unboxing copie les champs d'une instance du type classe vers une instance du type valeur. Tous les types de classe qui sont créés à partir des types de valeur sont dérivés de la classe ironiquement nommée ValueType (qui, malgré son nom, est en fait un type de référence).


0

Lorsqu'une méthode ne prend qu'un type de référence comme paramètre (disons une méthode générique contrainte d'être une classe via la newcontrainte), vous ne pourrez pas lui passer un type de référence et vous devrez le mettre en boîte.

Cela est également vrai pour toutes les méthodes qui prennent objectcomme paramètre - cela a être un type de référence.


0

En général, vous voudrez généralement éviter de mettre en boîte vos types de valeur.

Cependant, il existe de rares cas où cela est utile. Si vous devez cibler le framework 1.1, par exemple, vous n'aurez pas accès aux collections génériques. Toute utilisation des collections dans .NET 1.1 nécessiterait de traiter votre type de valeur comme un System.Object, ce qui provoque la boxe / unboxing.

Il existe encore des cas pour que cela soit utile dans .NET 2.0+. Chaque fois que vous souhaitez tirer parti du fait que tous les types, y compris les types de valeur, peuvent être traités directement comme un objet, vous devrez peut-être utiliser la boxe / unboxing. Cela peut être pratique parfois, car il vous permet d'enregistrer n'importe quel type dans une collection (en utilisant un objet au lieu de T dans une collection générique), mais en général, il vaut mieux éviter cela, car vous perdez la sécurité du type. Le seul cas où la boxe se produit fréquemment, cependant, est lorsque vous utilisez la réflexion - de nombreux appels en réflexion nécessiteront une boxe / unboxing lorsque vous travaillez avec des types de valeur, car le type n'est pas connu à l'avance.


0

La boxe est la conversion d'une valeur en un type de référence avec les données à un certain décalage dans un objet sur le tas.

Quant à ce que fait réellement la boxe. Voici quelques exemples

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;

Unboxing en Mono est un processus de conversion d'un pointeur à un décalage de 2 gpointers dans l'objet (par exemple 16 octets). A gpointerest un void*. Cela a du sens lorsque l'on regarde la définition de MonoObjectcar il ne s'agit clairement que d'un en-tête pour les données.

C ++

Pour encadrer une valeur en C ++, vous pouvez faire quelque chose comme:

#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;
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.