Sellest, mida System.Decimal kõhus peidab..

Ilmselt iga asjalikum koodikirjutaja teab, mille poolest erinevad System.Single (või float, kui keegi seda rohkem eelistab) ja System.Decimal andmetüübid. Yada-yada, kahendsüsteem vs kümnendsüsteem. Sellegipoolest jäin mõneks hetkeks mõttesse kui avastasin, et ka ToString() käitub erinevalt:

Console.WriteLine((1.0F).ToString());   // returns '1'
Console.WriteLine((1.000F).ToString()); // returns '1'

Console.WriteLine((1M).ToString());     // returns '1'
Console.WriteLine((1.000M).ToString()); // returns '1,000'
Console.WriteLine(1M == 1.000M);        // returns true

Järeldus tuleviku tarbeks on see, et Decimal korral ei tohi eeldada, et ToString() sama väärtusega sisendi korral alati sama vastuse annaks. Kui formaat on oluline, siis tuleb alati see ka täpsustada ning mitte lootma jääda parameetriteta meetodile.

Console.WriteLine((1.000M).ToString("0"));   
// returns '1'

Loomulikult, kui formaat on märgi täpsusega oluline, siis tuleks ka kultuur täpsustada. Antud juhul ei ole see oluline.

 

Miks Decimal nii käitub ?

Teatavasti on  Decimal 128-bitine struktuur. Sisemiselt:

  • 3 * 32bit "täpsusosa" - low, med and  hi-bits
  • märgibitt
  • scale-väärtusele, mis määrab kui suur osa täpsusosa kümnendkohtadest on murdosa.

Siit ka tuleneb lubatud väärtuste hulk:

{ s * c * 10^(-e)|
     s kuulub hulka {-1,1}, 
     0 <= c <= 2^96 ,
     ja 0 <= e <= 28
}

Muuhulgas nähtub, et seetõttu leiduvad erinevad (s,c,e)-komplektid (ehk decimal sisemised väärtustused), mis omavad muutuja kasutaja poolt vaadatuna sama väärtust (näiteks arvu 1). Eelpool kirjeldatud näide demonstreeris just kahte erinevat sisemist väärtustust.

Illustreerimiseks võib selle näite veel ilmekamalt lahti kirjutada, kasutades konstruktorit, mis võimaldab kõiki eeltoodud komponente ise sisestada:

Decimal d10 = new decimal(10, 0, 0, false, 1);
Console.WriteLine(d10.ToString());   // returns '1,0'
Decimal d1000 = new decimal(1000, 0, 0, false, 3);
Console.WriteLine(d1000.ToString()); // returns '1,000'
Console.WriteLine(d10 == d1000);     //returns True;


Aga mille jaoks on ülejäänud bitid ?

Komponendid e ja märgibitt saavad kasutada 128 - 96 = 32 biti jagu ruumi. Kui märgibitt võtab neist tubli 1, siis miks e lubatud väärtused on piiratud 29 erineva väärtusega, kui kasutada on 2^31 ? Vägisi jääb mulje, et 31 - up(log_2 29) = 26 bitti iga Decimali kohta on kasutu ballast.. Raske uskuda, aga mõistlikku selgitust ei näe.


Aga miks System.Single ToString() ikkagi teisiti käitub ?

Teoreetiliselt on ka nende jaoks võimalik konstrueerida samasugune juhtum (1 * 2^10 vs 2 * 2^9) . Pead või sõrmi panti ei paneks, aga oletan, et levinumate floating-point tüüpide korral lahendatakse "normaliseerimine" riistvaras. Kahendsüsteemis nihutamine on ka oluliselt  odavam ettevõtmine.

Kommentaare ei ole: