C#Boxing

Quelques raccourcis :

Avec C#, la généricité s'applique aux types références (aux class), or il s'avère que plusieurs types, y compris ce qui passe pour des types primitifs dans ce langage, sont des struct et n'y ont pas la forme attendue pour être manipulés de manière générique. La généricité classique de C# passe justement par des object, ce qui demande une certaine conscientisation (préférer la généricité réifiée si possible).

Lorsque le compilateur C# doit traiter un struct comme un object ou un de ses dérivés, une opération nommée Boxing est générée silencieusement (le chemin inverse est réalisé par une autre opération, nommée Unboxing).

Le Boxing n'est pas gratuit. Mieux vaut le comprendre si nous souhaitons être efficaces.

Introduction

Certaines opérations en C# ne sont possibles que sur des types références. Par « type référence », on entend ici une instance de la classe object ou d'une de ses classes dérivées. Les cas les plus marquants sont les conteneurs non-génériques (pré-C# 2.0) qui contiennent des object.

Par exemple :

// Console.Write accepte un nombre arbitrairement grand de object
// Ici, 3 est un int, qui ne dérive pas de object. Un 'box' est requis
// dans le code CIL généré, ce qui crée un objet temporaire

using System;
Console.Write("{0} ", 3);

Le Boxing et le Unboxing sont des opérations dispendieuses, du moins en comparaison avec les opérations « normales » d'affectation sur des « primitifs », mais se glissent discrètement dans bien des programmes :

int n = 3;
object obj = n; // Boxing
int m = (int) obj; // Unboxing

Généricité classique et généricité réifiée

La généricité réifiée (p.ex.: List<T>) évite le Boxing en générant des types spécifiques à chaque cas d'utilisation, contrairement à la généricité par effacement de types « classique » (p.ex. : List).

var lst0 = new List();
lst0.Add(3); // boxing
var lst1 = new List<T>();
lst1.Add(3); // on évite le boxing ici

Pour un comparatif de coûts, examinez ceci :

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
					
const int N = 1_000_000;
var dtClassique = Tester(TestClassique, N);
var dtRéifié = Tester(TestRéifié, N);
Console.WriteLine($"Avec boxing, {N} insertions en {dtClassique} ms");
Console.WriteLine($"Sans boxing, {N} insertions en {dtRéifié} ms");

static long Tester<T,A>(Func<A,T> f, A arg)
{
   Stopwatch sw = new ();
   sw.Start();
   f(arg);
   sw.Stop();
   return sw.ElapsedMilliseconds;
}
static int TestClassique(int n)
{
   ArrayList lst = new ();
   for(int i = 0; i != n; ++i)
      lst.Add(i); // boxing
   return lst.Count;
}
static int TestRéifié(int n)
{
   List<int> lst = new ();
   for(int i = 0; i != n; ++i)
      lst.Add(i); // pas de boxing
   return lst.Count;
}

À l'exécution avec https://dotnetfiddle.net/CKUz74 j'obtiens :

Avec boxing, 1000000 insertions en 59 ms
Sans boxing, 1000000 insertions en 5 ms

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !