Boxing & UnBoxing - Back to the basics

Pokiaľ vieš čo je to Boxing a UnBoxing (nie UnBoxing nie je fancy rozbaľovanie nového iPhonu tvojím obľúbeným youtuberom), tak tento post nečítaj. S veľkou pravdepodobnosťou sa tu nič nového nedozvieš.

Patrím asi do skupiny posledných, ktorých chytila doba, že keď chceli začať s novou technológiou, tak sa museli prelúskať knihami. Zvyčajne tak dve až tri 400-600 stranové knižky. Človek si musel prejsť základmi, pochopiť jazyk, jeho syntax, základné koncepty, … ako funguje kompilácia, správa pamäti, runtime. Spoznával ekosystém, knižnice, nástroje, … Spravil si štandardný “Hello World” (robí to ešte niekto? 🤔) a až potom sa pustil do zložitejšieho projektu.

Dnes je to iné. Ľudia sa učia z blogov, na youtube, … Takýto človek vie veľmi rýchlo začať robiť na projekte (či už svojom, alebo v práci) a venovať sa tomu čo potrebuje. Toto je super.

Mala by ale prísť aj fáza, keď sa človek začne zaujímať o veci ako: Načo vlastne existuje trieda StringBuilder, čo je to ten Garbage Collector, prečo je dôležité používať using pri práci s IDisposable, kvôli čomu vznili generiká, … Jednoducho aby sa človek posunul a vedel robiť kvalitnejšie veci, potrebuje vedieť viac ako len syntax jazyka a features knižníc.

Rovnako je to aj s Boxingom a UnBoxingom.

Takže toto je taký malý návrat k základom pre tých čo už programujú v .NET, ale o tomto koncepte nevedia, prípadne len tušia.

Boxing

Boxing je proces, kedy hodnotový typ (premenná ktorej hodnota sa nachádza na stacku) je konvertovaný na referenčný typ (premenná ktorej hodnota je na heape). Tento proces je implicitný, čiže sa deje automaticky. Keď CLR boxuje hodnotový typ, tak vytvorí novú inštanciu System.Object a skopíruje hodnotu z pôvodnej premennej do tohto objektu.

int age = 39;

object age_obj = age; // 👈 boxing value

Ďalší príklad, kde to už nemusí byť také zrejmé:

Guid id = Guid.NewGuid();
int amount = 42;
decimal price = 42.42m;

var text = string.Format(
    "The id is {0}, the amount is {1}, the price is {2}",
    id, amount, price); // 👈 boxing values

UnBoxing

UnBoxing je opačný proces ako Boxing. Je to proces, kedy hodnota z referenčného typu je konvertovaná späť na hodnotový typ,
čo znamená že je extrahovaná z heapu a presunutá na stack. Tento proces je explicitný.

int age2 = (int)age_obj; // 👈 unboxing value

Výhody a nevýhody

Boxing a UnBoxing je proces ktorý nám v situáciach kedy to potrebujeme umožňuje pristupovať k hodnotovým aj referenčným typom ako k jednému typu.
Ako ale už cítiť, tak je to proces, ktorý nie je zadarmo. Má svoju cenu.

  1. Je potrebné alokovať pamäť pre nový objekt.
  2. Je potrebné skopírovať hodnotu z pôvodnej premennej do nového objektu.
  3. Keďže sa jedná o referenčný typ, tak je potrebné ho následne spravovať (GC) (každé spustenie GC znamená napríklad freeznutie webového API, ...)

Sú situácie kde sa boxingu vieme vyhnúť. Napríklad to bol jeden z dôvodov prečo do .NET prišli generiká (áno dôvodov je samozrejme viac).

[Benchmark]
public void AddToArrayList()
{
    for (int i = 0; i < MaxCount; i++)
    {
        _arrayList.Add(i);
    }
}

[Benchmark]
public void AddToList()
{
    for (int i = 0; i < MaxCount; i++)
    {
        _list.Add(i);
    }
}

| Method         | Mean      | Error     | StdDev    | Median    |
|--------------- |----------:|----------:|----------:|----------:|
| AddToArrayList | 82.743 ms | 13.359 ms | 29.880 ms | 70.806 ms |
| AddToList      |  8.414 ms |  4.583 ms | 10.250 ms |  5.068 ms |

Z výsledku je jasný rozdiel v čase spracovania.
O tejto výhode a použití generík asi každý vie a nikoho som tým neprekvapil. Vedeli ste napríklad ale aj o tomto?

[Benchmark]
public string UseStringFormat()
{
    return string.Format("Product: {0}, Price: {1} {2}, Amount: {3}. Date: {4} (Id: {5})",
        productName, price, currency, stockQuantity, date, guid);
}

[Benchmark]
public string UseStringInterpolation()
{
    return $"Product: {productName}, Price: {price} {currency}, " +
        $"Amount: {stockQuantity}. Date: {date} (Id: {guid})";
}

| Method                   | Mean     | Error   | StdDev   | Gen0   | Allocated |
|------------------------- |---------:|--------:|---------:|-------:|----------:|
| UseStringFormat          | 287.1 ns | 5.62 ns |  7.69 ns | 0.0544 |     456 B |
| UseStringInterpolation   | 204.9 ns | 4.00 ns |  6.89 ns | 0.0324 |     272 B |

String interpolation je rýchlejší (aj keď nie o veľa) ako string.Format a čo je dôležitejšie spôsobuje menej alokácií a tým pádom aj menej práce pre GC.

Prečo je tomu tak? Je tam viac dôvodov, ale jeden z nich je aj ten, že string interpolation na pozadí využíva generiká.
A vzniká tam niečo približne nasledovné:

DefaultInterpolatedStringHandler handler = new ();

handler.AppendLiteral("Product: ");
handler.AppendFormatted(productName);
handler.AppendLiteral(", Price: ");
handler.AppendFormatted<decimal>(price);
handler.AppendLiteral(" ");
handler.AppendFormatted(currency);
handler.AppendLiteral(", Amount: ");
handler.AppendFormatted<int>(stockQuantity);
handler.AppendLiteral(". Date: ");
handler.AppendFormatted<DateTime>(date);
handler.AppendLiteral(" (Id: ");
handler.AppendFormatted<Guid>(guid);
handler.AppendLiteral(")");

Záver

Boxing a UnBoxing je proces, ktorý je v .NET veľmi dôležitý a je dobré vedieť ako funguje, aby sme ho vedeli správne využiť prípadne sa mu vyhnúť 🙂.