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
static void Main(string[] args)
{
   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 :

static void Main(string[] args)
{
   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;
					
public class Program
{
   static long Tester<T,A>(Func<A,T> f, A arg)
   {
      System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
      sw.Start();
      f(arg);
      sw.Stop();
      return sw.ElapsedMilliseconds;
   }
   static int TestClassique(int n)
   {
      System.Collections.ArrayList lst = new System.Collections.ArrayList();
      for(int i = 0; i != n; ++i)
         lst.Add(i); // boxing
      return lst.Count;
   }
   static int TestRéifié(int n)
   {
      System.Collections.Generic.List<int> lst = new System.Collections.Generic.List<int>();
      for(int i = 0; i != n; ++i)
         lst.Add(i); // pas de boxing
      return lst.Count;
   }
   public static void Main()
   {
      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");
   }
}

À l'exécution avec dotnetfiddle, j'obtiens :

Avec boxing, 1000000 insertions en 96 ms
Sans boxing, 1000000 insertions en 19 ms

Lectures complémentaires

Quelques liens pour enrichir le propos.


Valid XHTML 1.0 Transitional

CSS Valide !