Arbeiten mit .NET: Das Framework/ Basistechnologien/ Datentypen/ Dezimalzahlen

Dieses Kapitel enthält Abschnitte für mehrere Varianten, die teilweise geprüft oder ergänzt werden müssen.
Problemstellen: Code für C++
Wenn du einen Teil geprüft oder geändert hast, kannst du den Hinweis darauf aus der Liste der Teile entfernen. Wenn der letzte Teil geprüft worden ist, kann die gesamte {{Vorlage:Fehlender Teil}} entfernt werden.

In diesem Kapitel erhalten Sie genauere Erläuterungen zu denjenigen einfachen Datentypen, mit denen Dezimalzahlen verarbeitet werden.

Wikipedia hat einen Artikel zum Thema:

Überblick

Bearbeiten

Die folgende Tabelle nennt zu jedem Datentyp, der für eine Dezimalzahl steht, den Standardwert sowie das mögliche Minimum und Maximum. Als Postfix oder Suffix wird ein Zeichen genannt, das an den Wert angehängt wird, um den Typ zu kennzeichnen. Alle diese Typen gehören zu den vorgeschriebenen Standardtypen (den "CLS-kompatiblen") und sind deshalb fett gedruckt.

Klassenname CLS Standard Postfix Minimum Maximum Länge Genauigkeit unendlich
Single ja 0.0 F in C#
F in VB
–3,402823E38
= – 3,402823 * 1038
+3,402823E38
= 3,402823 * 1038
39 Stellen 7 Stellen definiert
Double ja 0.0 (kein) –1,79769313486232E308
≈ – 1,797693 * 10308
+1,79769313486232E308
≈ 1,797693 * 10308
309 Stellen 15–16 Stellen definiert
Decimal ja 0.0 M in C#
D in VB
–7,922816E28
= –7,922816 * 1028
+7,922816E28
= 7,922816 * 1028
29 Stellen 28 Stellen nicht def.

Wie zu sehen ist, unterscheiden sich diese Typen sowohl im Wertebereich als auch in der Genauigkeit. Dies wird unter Hinweise genauer erläutert, ebenso wie die Spalte "unendlich".

Mit "E38" wird die wissenschaftliche Schreibweise benutzt. Das Single-Maximum bedeutet also eine Zahl mit maximal 39 Stellen, von denen aber nur die ersten 7 Stellen berücksichtigt werden:
3,402823E38
= 3,402823 * 1038
= 340.282.300.000.000.000.000.000.000.000.000.000.000.000.000.000

Der folgende Code als Teil einer Main-Methode benutzt ein paar dieser Eigenschaften.

  • Der kleinstmögliche Single-Wert wird mit sich selbst multipliziert – einmal als Single, einmal als Double.
  • Zu einem Betrag wird die Mehrwertsteuer berechnet und addiert.
  • In einer Schleife wird ein kleiner Dezimalwert 10.000-Mal addiert: einmal als Double, einmal als Decimal.

Bitte beachten Sie, dass innerhalb von Code immer der Punkt als Dezimaltrenner verwendet werden muss:

C#-Quelltext
Single d1 = Single.MinValue;
Single d2 = d1 * d1;
Double d3 = d1 * d1;
Console.WriteLine(String.Format("Single = {0}"
	+ Environment.NewLine + "Double = {1}", d2, d3));
Decimal m1 = 13.1m;
Decimal m2 = m1 * 0.19m;
Decimal m3 = m1 + m2;
Console.WriteLine(String.Format("[Decimal]  {0} + {1} = {2}", m1, m2, m3));
Double f4 = 0;
Decimal m4 = 0;
for(int x1 = 0; x1 < 10000; x1++) {
	f4 += 0.0001;
	m4 += 0.0001m;
}
Console.WriteLine(String.Format("decimal = {0}"
	+ Environment.NewLine + "Double = {1}", m4, f4));
Console.ReadKey();
VB.NET-Quelltext
Dim d1 As Single = Single.MinValue
Dim d2 As Single = d1 * d1
Dim d3 As Double = d1 * d1
Console.WriteLine(String.Format("Single = {0}" _
	+ Environment.NewLine + "Double = {1}", d2, d3))
Dim m1 As Decimal = 13.1d
Dim m2 As Decimal = m1 * 0.19d
Dim m3 As Decimal = m1 + m2
Console.WriteLine(String.Format("[Decimal]  {0} + {1} = {2}", m1, m2, m3))
Dim f4 As Double = 0.0
Dim m4 As Decimal = 0.0d
For x1 As Integer = 1 To 10000 Step 1
	f4 += 0.0001
	m4 += 0.0001d
Next x1
Console.WriteLine(String.Format("decimal = {0}" _
	+ Environment.NewLine + "Double = {1}", m4, f4))
Console.ReadKey()
  Ausgabe
Single = +unendlich
Double = 1,15792075433824E+77
[Decimal]  13,1 + 2,489 = 15,589
decimal = 1,0000
Double = 0,999999999999906

Bei direkter Ein- und Ausgabe von Werten hängen Dezimalzeichen und Tausendertrenner auch von der Installation des Rechners und der konkreten Formatierung ab.

Hinweise

Bearbeiten

Bei diesen Datentypen handelt es sich um Gleitkommazahlen. Dies ist immer eine näherungsweise Darstellung einer reellen Zahl (in Exponentialdarstellung wie in der Tabelle). Bei Single und Double muss man mit dieser Ungenauigkeit leben; bei Decimal sorgt die interne Verarbeitung der Daten für größere Genauigkeit.

Unendlich u.a.

Bearbeiten

Für Single und Double gibt es die folgenden Eigenschaften, was durch das Wort "definiert" in der obigen Spalte "unendlich" deutlich gemacht wird:

  • PositiveInfinity steht für "plus unendlich". Es wird angezeigt als Ergebnis, wenn eine positive Zahl durch 0 dividiert wird oder wenn es größer ist als der Maximalwert (wie im obigen ersten Beispiel).
  • NegativeInfinity steht entsprechend für "minus unendlich", wenn eine negative Zahl durch 0 dividiert wird oder wenn ein Ergebnis kleiner ist als der Minimalwert.

Dazu gibt es Prüfungen, ob ein Wert unendlich ist; dafür ändern wir das obige Beispiel leicht:

C#-Quelltext
Single d1 = Single.MinValue;
Single d2 = d1 * d1;
Double d3 = d1 * d1;
Console.WriteLine(String.Format("d1 unendlich: {0:f} {1}"
				+ Environment.NewLine + "d2 +unendlich: {2:f} {3}"
				+ Environment.NewLine + "d3 -unendlich: {4:f} {5}", 
				d1, Single.IsInfinity(d1),
				d2, Single.IsPositiveInfinity(d2), 
				d3, Double.IsNegativeInfinity(d3)));
Console.ReadKey();
VB.NET-Quelltext
Dim d1 As Single = Single.MinValue
Dim d2 As Single = d1 * d1
Dim d3 As Double = d1 * d1
Console.WriteLine(String.Format("d1 unendlich: {0:f} {1}" _
				+ Environment.NewLine + "d2 +unendlich: {2:f} {3}" _
				+ Environment.NewLine + "d3 -unendlich: {4:f} {5}", _
				d1, Single.IsInfinity(d1), _
				d2, Single.IsPositiveInfinity(d2), _
				d3, Double.IsNegativeInfinity(d3)))
Console.ReadKey()
  Ausgabe
d1 unendlich: -340282300000000000000000000000000000000,00 False
d2 +unendlich: +unendlich True
d3 -unendlich: 115792075433824000000000000000000000000000000000000000000000000000000000000000,00 False
  • Beim Wert 0 (der Zahl Null) wird zwischen dem genauen Wert 0, einer positiven 0 und einer negativen 0 unterschieden.
  • Als NaN (Not a Number) wird das Ergebnis einer ungültigen Gleitkommaoperation bezeichnet, nämlich die Division von 0 durch 0.

In der Praxis sind diese Situationen eigentlich selten von Bedeutung. Sie müssen aber den folgenden Unterschied bei unzulässigen Berechnungen beachten:

  • Bei allen anderen numerischen Datentypen gibt es einen Laufzeitfehler (Exception), wenn der Wertebereich überschritten wird oder eine Division durch 0 erfolgt.
  • Bei Single- und Double-Operationen gibt es niemals Laufzeitfehler, sondern stattdessen ein "passendes" Ergebnis wie "unendlich" oder "NaN".

Sie müssen deshalb in allen Fällen selbst eine entsprechende Fehlerbehandlung vorbereiten.

Genauigkeit

Bearbeiten

Schon der letzte Teil des einleitenden Beispiels machte deutlich, dass Single und Double ungenau rechnen. Dies ist eine direkte Auswirkung davon, wie Dezimalzahlen von Computern verarbeitet werden. Schauen wir uns zwei einfache Beispiele an:

C#-Quelltext
Single d1 = 0.0003f / 3 - 0.0001f;
Double d2 = 0.0003 / 3 - 0.0001;
Console.WriteLine(String.Format("d1 = {0} / gleich null: {1}",
	 			 d1, d1 == 0));
Console.WriteLine(String.Format("d2 = {0} / gleich null: {1}",
				 d2, d2 == 0));
Single d3 = 8.436713f;
Single d4 = 8.4367131f;
Console.WriteLine(String.Format("d3 = {0} / d4 = {1} / gleich: {2}",
				 d3, d4, d3 == d4));
VB.NET-Quelltext
Dim d1 As Single = 0.0003f / 3 - 0.0001f
Dim d2 As Double = 0.0003  / 3 - 0.0001
Console.WriteLine(String.Format("d1 = {0} / gleich null: {1}", _
				 d1, d1 = 0))
Console.WriteLine(String.Format("d2 = {0} / gleich null: {1}", _
				 d2, d2 = 0))
Dim d3 As Single = 8.436713
Dim d4 As Single = 8.4367131
Console.WriteLine(String.Format("d3 = {0} / d4 = {1} / gleich: {2}", _
				 d3, d4, d3 = d4))
  Ausgabe
d1 = 7,275958E-12 / gleich null: False
d2 = -1,35525271560688E-20 / gleich null: False
d3 = 8,436713 / d4 = 8,436713 / gleich: True

Die erste Rechnung sollte, wie man im Kopf ausrechnen kann oder direkt sieht, als Ergebnis 0 liefern. Tatsächlich gibt es bei Single und Double unterschiedliche Ergebnisse, die aber immer ungleich 0 sind. Bei Double ist der Wert um 8 Zehnerpotenzen näher an 0, was an der höheren Genauigkeit liegt; aber es bleibt ungleich 0.

Der Vergleich zwischen d3 und d4 zeigt: Wegen der beschränkten Genauigkeit wird die letzte Dezimalstelle bei d4 von vornherein ignoriert. Beide Werte werden also als gleich angesehen.

 

Merke
Single und Double rechnen mit Dezimalwerten ungenau. Direkte Vergleiche auf Gleichheit liefern oft falsche Ergebnisse.


Verfahren bei Ungenauigkeiten

Bearbeiten

Offensichtlich muss man sich Gedanken darüber machen, wie diese Ungenauigkeiten behandelt werden können:

  • Das Ergebnis einer Rechnung wird durch Math.Round gerundet; es gibt auch Varianten, die die Genauigkeit dieser Rundung festlegen.
  • Das Ausgabeformat bei Console.WriteLine, String.Format, ToString o.ä. erhält eine Vorschrift über die gewünschte Anzahl von Dezimalstellen; das führt automatisch zu einer Rundung.
  • Anstelle einer Prüfung auf Gleichheit wird auf näherungsweise Gleichheit geprüft.

Zum letzten Punkt gehören zwei Schritte, die im folgenden Code zusammengefasst werden.

C#-Quelltext
namespace Wikibooks.CSharp.ConsoleApp
{
	class Program
	{
		static void Main(string[] args)
		{
    			double epsilon = Epsilon();
    			Console.WriteLine(epsilon);
	    		
    			Double d2 = 0.0003 / 3 - 0.0001;
    			if( Math.Abs(d2 - 0.0) < epsilon )
    				Console.WriteLine("d2 ist nahezu gleich Null");
	    		else
				Console.WriteLine("d2 ist wirklich ungleich Null");
			Console.ReadKey();
		}
		
        	public static double Epsilon()
	        {
        	    double tau = 1.0;
	            double alt = 1.0;
        	    double neu = 0.0;

	            while (neu != alt)
        	    {
                	tau *= 0.5;
	                neu = alt + tau;
        	    }
	            return 2.0 * tau;
        	}		
	}
}
VB.NET-Quelltext
Namespace Wikibooks.VBNet.ConsoleApp
	Module Program
		Sub Main()
	    		Dim epsilon As Double = CalcEpsilon()
    			Console.WriteLine(epsilon)
    		
    			Dim d2 as Double = 0.0003 / 3 - 0.0001
	    		if( Math.Abs(d2 - 0.0) < epsilon )
    				Console.WriteLine("d2 ist nahezu gleich Null")
    			else
				Console.WriteLine("d2 ist wirklich ungleich Null")
			end if
			Console.ReadKey()
		End Sub
		
	        Function CalcEpsilon() As Double
        		Dim tau as Double = 1.0
        		Dim alt as Double = 1.0
        		Dim neu as Double = 0.0

        		While (neu <> alt)
	        		tau *= 0.5
        			neu = alt + tau
        		End While
        		Return 2.0 * tau
		End Function
	End Module
End Namespace
  Ausgabe
2,22044604925031E-16
d2 ist nahezu gleich Null
Vergleich durch einen Näherungswert

Die beiden Werte, die verglichen werden sollen – im Beispiel das Ergebnis der Rechnung und der konstante Wert 0.0 (der eigentlich überflüssig ist und nur zur Verdeutlichung hingeschrieben ist) – werden voneinander abgezogen; dann wird geprüft, ob der Absolutbetrag der Differenz kleiner ist als die zulässige Abweichung.

Festlegung des Näherungswertes

Dieser Wert wird üblicherweise Epsilon genannt; er kann wie im Beispiel vorgegeben werden.[1] Diese Berechnung erfolgt am besten am Anfang des Programms einmalig; der Wert wird "irgendwo" zur wiederholten Verwendung gespeichert.

Hinweis: Single.Epsilon und Double.Epsilon haben nichts damit zu tun; diese sind jeweils der kleinste mögliche Wert, der größer als 0 ist.

Nebenbei gesagt: In der Praxis spielen diese Ungenauigkeiten oft keine Rolle. Man muss sich aber dieser Möglichkeit bewusst sein und darf nicht irgendwann erstaunt aufschreien: ".NET rechnet falsch!"

Der Datentyp Decimal als Besonderheit

Bearbeiten

Zur Vermeidung der Ungenauigkeiten wurde der Datentyp Decimal eingeführt, dessen Werte intern anders gespeichert und verarbeitet wird als bei Single oder Double.

Der Decimal-Wertetyp ist für finanzmathematische Berechnungen geeignet, bei denen zahlreiche signifikante Vor- und Nachkommastellen erforderlich sind und keine Rundungsfehler auftreten dürfen.

Wie aus den obigen Beispielen deutlich wird, gibt es die für Single und Double beschriebenen Probleme beim Rechnen und Vergleichen bei Decimal nicht.

Operationen

Bearbeiten

Für alle diese Datentypen stehen die folgenden Möglichkeiten zur Verfügung:

  • In den Abschnitten zu Datentypen verarbeiten werden behandelt:
    • Vergleiche zwischen Werten desselben Typs
    • Rechenoperationen zwischen Werten desselben Typs
  • In den Abschnitten zu Typumwandlungen werden behandelt:
    • Umwandlung einer Zahl in einen String
    • Umwandlung eines Strings in eine Zahl
    • Umwandlung zwischen Zahlen verschiedener Typen

Mit dem letzten Schritt sind auch Vergleiche und Rechenoperationen zwischen Werte verschiedener Typen möglich.

Grenzüberschreitung

Bearbeiten

Die bisherigen Beispiele zeigen schon, was passiert, wenn durch eine Rechnung oder Typumwandlung der maximale Wertebereich überschritten wird:

  • Eine Rechnung bei Single oder Double, die den Wertebereich überschreitet, führt zum Ergebnis plus-unendlich bzw. minus-unendlich.
  • Ein Single-Wert kann ohne weiteres als Double-Wert verarbeitet werden.
  • Eine Rechnung bei Decimal, die den Wertebereich überschreitet, führt wie bei ganzen Zahlen zu einer OverflowException.

Ohne genauere Prüfungen können wir außerdem für Double-Werte feststellen:

  • Wenn er den Single-Wertebereich überschreitet, wird er als plus-unendlich bzw. minus-unendlich verstanden.
  • Wenn er im Single-Wertebereich bleibt, kann er direkt verarbeitet werden, verliert dabei aber an Genauigkeit.
  • Wie bei ganzen Zahlen ist es auch möglich, mit der Modulo-Operation aus dem Double-Wert einen passenden Single-Wert zu errechnen.

Die beiden letzten Situationen werden im folgenden Beispiel gezeigt:

C#-Quelltext
// ein Double-Wert, der in den Single-Bereich passt
Double d1 = 0.2468097531 * Single.MaxValue;
Single s1 = (Single)d1;
Console.WriteLine(d1);
Console.WriteLine(s1);
    		
// ein zu großer Double-Wert
Double d2 = 3.57902468 * Single.MaxValue;
// der kleinste Double-Wert, der über Single hinausreicht
Double dd = Single.MaxValue + Double.Epsilon;
// die modulo-Operation anwenden
Double d3 = d2 % dd;
// das Ergebnis als Single vergleichen
Single s2 = (Single)d3;
Console.WriteLine(d2);
Console.WriteLine(dd);
Console.WriteLine(d3);
Console.WriteLine(s2);
VB.NET-Quelltext
' ein Double-Wert, der in den Single-Bereich passt
Dim d1 As Double = 0.2468097531 * Single.MaxValue
Dim s1 As Single = CSng(d1)
Console.WriteLine(d1)
Console.WriteLine(s1)
    		
' ein zu großer Double-Wert
Dim d2 As Double = 3.57902468 * Single.MaxValue
' der kleinste Double-Wert, der über Single hinausreicht
Dim dd As Double = Single.MaxValue + Double.Epsilon
' die modulo-Operation anwenden
Dim d3 As Double = d2 Mod dd
' das Ergebnis als Single vergleichen
Dim s2 As Single = CSng(d3)
Console.WriteLine(d2)
Console.WriteLine(dd)
Console.WriteLine(d3)
Console.WriteLine(s2)
  Ausgabe
// ein Double-Wert, der als Single-Wert verarbeitet werden kann
8,39850019581439E+37
8,3985E+37
// ein zu großer Double-Wert, der zu einem Single-Wert gemacht werden kann
1,21787891678761E+39
3,40282346638529E+38
1,97031876872023E+38
1,970319E+38

Sie sehen im ersten Fall und beim letzten Schritt, dass nur die Genauigkeit verloren geht. Ob diese Modulo-Funktion in der Praxis sinnvoll ist, lassen wir einmal offen; wahrscheinlich wird man eher eine glatte Zehnerpotenz dafür verwenden.

Welchen Typ soll ich nehmen?

Bearbeiten

Wegen der Unterschiede zwischen Single bzw. Double sowie Decimal ist diese Entscheidung ganz einfach:

  • Bei wissenschaftlich-technischen Problemen oder Berechnungen des Alltags ist eine Genauigkeit von mehr als sieben Ziffern selten erforderlich; deshalb genügt Single.
  • Wenn es auf mehr Ziffern oder größere Werte als 1038 ankommt, ist Double zu verwenden. Es spricht auch nichts dagegen, dies als Standard zu nutzen.
  • Bei finanzmathematischen Berechnungen darf es keine Ungenauigkeiten geben; dafür muss Decimal gewählt werden.

Zusammenfassung

Bearbeiten

Es gibt mehrere Datentypen für Dezimalzahlen, die sich im Wertebereich und in der Genauigkeit unterscheiden.

  • Am häufigsten wird System.Double verwendet, der Datentyp für doppelte Genauigkeit.
  • Rechenoperationen mit Single und Double bringen häufig Ungenauigkeiten; direkte Vergleiche können in die Irre führen.
  • Rechenoperationen mit Decimal weisen eine erheblich größere Genauigkeit ohne Rundungsfehler auf.

Wir können uns also merken:

 

Merke
Für Dezimalzahlen gibt es zwei verschiedene Arten von Datentypen:
  • Single genügt für einfache Situationen, Double für doppelte Genauigkeit.
  • Decimal ist zu verwenden, wenn es keine Ungenauigkeiten geben darf – wie bei Finanz- und Währungsberechnungen.


Übungen

Bearbeiten

Hinweis: Wenn hier von Datentypen gesprochen wird, sind immer die drei hier behandelten Typen für Dezimalzahlen gemeint.

Übung 1 Definitionen Zur Lösung

Welche der folgenden Aussagen sind richtig, welche sind falsch?

  1. Alle Datentypen haben nur eine begrenzte Genauigkeit.
  2. Decimal hat eine größere Genauigkeit als Double.
  3. Die Genauigkeit von Single und Double unterscheidet sich nicht.
  4. Der Wertebereich von Decimal ist größer als derjenige von Single.
  5. Für alle Datentypen steht die Größenangabe "minus-unendlich" zur Verfügung.

Übung 2 Wertebereich Zur Lösung

Welche Datentypen können die folgende Werte bearbeiten? Bitte geben Sie jeweils an, ob die Bearbeitung genau oder mit Rundung erfolgt.

Mehrere Antworten sind möglich. Der Punkt ist als Dezimaltrenner zu verstehen.

  1. 123.456
  2. –789.01234
  3. 1234567890
  4. 12345678901234567890
  5. 123456789012345678901234567890
  6. 1234567890123456789012345678901234567890

Übung 3 Wertebereich Zur Lösung
  1. Wie lautet das Ergebnis als Single-Datentyp, wenn Single.MaxValue mit 1.5 multipliziert wird?
  2. Wie lautet das Ergebnis als Double-Datentyp, wenn Single.MaxValue mit 10 multipliziert wird? Es genügt ein ungefährer Wert.
  3. Wie lautet das Ergebnis als Decimal-Datentyp, wenn Decimal.MaxValue mit 2 multipliziert wird?

Übung 4 Genauigkeit Zur Lösung

Welche der folgenden Aufgaben kann ein genaues Ergebnis liefern, welche nicht?

  1. Single: 123 * 456
  2. Single: 12.3 * 4.56
  3. Single: 9876 * 6789
  4. Double: 9876 * 6789
  5. Double: 987.6 * 6.789
  6. Double: 1.23 / 0.41
  7. Decimal: 1.23 / 0.41

Übung 5 Rundungsfehler behandeln Zur Lösung

Ein Programm soll zwei verschiedene komplizierte Berechnungen mit Double-Werten ausführen. Wie ist die Prüfung vorzunehmen, ob die beiden Berechnungen dasselbe Ergebnis liefern?

Lösungen

Lösung zu Übung 1 Definitionen Zur Übung

Die Aussagen 1, 2 sind richtig, die Aussagen 3, 4, 5 sind falsch.

Lösung zu Übung 2 Wertebereich Zur Übung
  1. Single (fast genau), Double (fast genau), Decimal (genau); im Rahmen der Darstellung alles genau
  2. Single (ungenau), Double (fast genau), Decimal (genau)
  3. Single (ungenau), Double (genau), Decimal (genau)
  4. Single, Double (jeweils ungenau), Decimal (genau)
  5. Single, Double (jeweils ungenau)
  6. Double (ungenau)

Lösung zu Übung 3 Wertebereich Zur Übung
  1. Single.PositiveInfinity
  2. + 3,402823E39 = 3,402823 * 1039
  3. OverflowException

Lösung zu Übung 4 Genauigkeit Zur Übung

Aufgaben 1, 4, 7 werden immer genau berechnet. Die Lösung von Aufgabe 3 ist wegen der vorgegebenen Genauigkeit des Datentyps immer ungenau. Die Ergebnisse zu den Aufgaben 2, 5, 6 können wegen Rundungsfehlern ungenau sein.

Lösung zu Übung 5 Rundungsfehler behandeln Zur Übung

Es kann auf jeden Fall zu Rundungsfehlern kommen. Deshalb kann keine exakte Gleichheit, sondern nur eine näherungsweise Gleichheit geprüft werden. Dies geht mit einem Verfahren wie folgt:

if( Math.Abs(ergebnis1 - ergebnis2) < epsilon )

  1. Quelle: Maschinengenauigkeit