DevTrain

Autor: Klaas Wedemeyer

Datentypen selbst erstellen

Das .net Framework bietet eine Menge Datentypen an, in die man seine Daten speichern kann. Doch hin und wieder wünscht man sich einen Datentyp, den das Framework nicht zur Verfügung stellt. Um die Daten sauber zu verwalten, bietet es sich an, dafür einen eigenen Datentypen zu erstellen. Hierbei sollte man ein paar Regeln einhalten, damit man Problemen vorbeugt und anderen das Arbeiten mit dem Datentyp erleichtert. Diese möchte ich an Hand eines Money Datentyps darstellen. Der Money Datentyp verwaltet zu einem Betrag auch die Währung und erlaubt so einer Anwendung mit verschiedenen Währungen zu arbeiten.
Bei einer Zuweisung kopiert .net die Referenzen zu und erstellt keine Kopien der Objekte. Das spart viel Platz birgt aber auch die Gefahr, dass andere Programmteile unbemerkt und ungewollt den Inhalt ändern. Deshalb sollen die Werte beim Erstellen gesetzt und danach nie wieder geändert werden. Für das Ändern der Werte muss immer ein neues Objekt angelegt werden.

Nicht ? myMoney.Currency = ?EUR?;
Sondern ? myMoney = new Money(myMoney.Amount, ?EUR?);

So muss eine Änderung explizit zugewiesen werden und kann nicht implizit von einem anderen Programmteil geschehen, welches zufällig die gleiche Referenz hat.

Dafür hat .net einen speziellen Datentypen: die readonly membervariable. Dieser kann nur im Konstruktor gesetzt werden.

public readonly decimal Amount;
public readonly string Currency;

public Money(decimal Amount, string Currency)
{
 this.Amount = Amount;
 this.Currency = Currency;
}

Um das Objekt zu kopieren, sollte ein copieconstructor eingefügt werden

public Money(Money Amount)
{
 this.Amount = Amount.Amount;
 this.Currency = Amount.Currency;
}

Da es auch ein leeres Objekt geben soll (noch keine Zahlung eingegangen), gibt es einen leeren Konstruktor. Es ist nicht nötig, immer wieder neue Objekte davon anzulegen. Deshalb wird nur ein Objekt angelegt und in der Member Empty bereit gestellt.

private Money()
{
 this.Amount = 0;
 this.Currency = "";
}
public static readonly Money Empty = new Money();

Nun soll mit dem Typen auch gerechnet werden. Dazu überläd man die nötigen Operatoren +, -, *, /.

public static Money operator +(Money Amount1, Money Amount2)
{
 return new Money(Amount1.Amount + Amount2.Amount, Amount1.Currency);
}
public static Money operator -(Money Amount1, Money Amount2)
?
public static Money operator *(Money Amount, decimal Number)
?
public static Money operator *(decimal Number, Money Amount)
?
public static Money operator /(Money Amount, decimal Number)
?

Der * Operator hat zwei verschiedene Typen und soll komutativ sein. Deshalb muss er zwei mal definiert werden. Beim / Operator muß man naturlich auf DivByZero achten.
Um zwei Objekte zu vergleichen, müssen die Vergleichsoperatoren überschrieben werden

public static bool operator ==(Money Amount1, Money Amount2)
{
 if (Amount1.Currency != Amount2.Currency)
  return false;
 return Amount1.Amount == Amount2.Amount;
}
public static bool operator !=(Money Amount1, Money Amount2)
?
public static bool operator >(Money Amount1, Money Amount2)
?
public static bool operator <(Money Amount1, Money Amount2)
?
public static bool operator <=(Money Amount1, Money Amount2)
?
public static bool operator >=(Money Amount1, Money Amount2)
?

Dazu müssen aber drei weitere Funktionen implementiert werden

public int Compare(object x, object y)
{
 Money val1 = (Money)x;
 Money val2 = (Money)y;
 if (val1.Amount == val2.Amount) return 0;
 if (val1.Amount < val2.Amount) return -1;
 return 1;
}
public override bool Equals(Object value)
{
 return Amount == ((Money)value).Amount && Currency == ((Money)value).Currency;
}
public override int GetHashCode()
{
 string s = Amount.ToString() + Currency.ToString();
 return s.GetHashCode();
}

Diese werden für die Vergleichsoperatoren benötigt. Da die Comparefunktion schon implementiert ist, kann man in der Klasse auch gleich das system.Collections.IComparer Interface implementieren. Das Aufwendigste ist die Ein-und die Ausgabe. Es soll das Currency Format aus den Regions- und Sprachoptionen der Systemsteuerung verwendet werden.
Die drei üblichen Befehle sind ToString, Parse und TryParse. Alle drei können mit und ohne einem FormatProvider aufgerufen werden. Ohne FormatProvider wird der aktuelle Provider verwendet.

public override string ToString()
{
 return ToString(System.Threading.Thread.CurrentThread.CurrentCulture);
}
public static Money Parse(string s)
?
public static bool TryParse(string s, out Money result)
?

Für die Formatierung  kann man die ToString Methode des Decimal Typen verwenden. Ruft man diese mit ?C? auf, wird nicht das NumberFormat sondern das CurrencyFormat verwendet. Man muß nur im Provider die Währung einstellen

public string ToString(IFormatProvider provider)
{
if (this.Equals(Empty))  return "";

 // Systemeinstellungen beachten
 CultureInfo CInfo = (CultureInfo) provider;
 NumberFormatInfo NFInfo = CInfo.NumberFormat;

 // Einen eigenen Provider mit den geänderten Daten erstellen
 NumberFormatInfo InfoCurrency = (NumberFormatInfo) NFInfo.Clone();
 InfoCurrency.CurrencySymbol = Currency;

 // mit dem kann man dann formatieren
 return Amount.ToString("C", InfoCurrency);
}

Beim TryParse muß zuerst die Währung vom Betrag getrennt werden. Dann kann der Betrag mit Decimal.TryParse gelesen werden. Leider gibt es hier Probleme mit dem CurrencyGroupSeparator. Da er nur optische Zwecke hat, kann man ihn mit Replace einfach aus dem Text löschen.

public static Money Parse(string s, IFormatProvider provider)
{
 Money result = Money.Empty;
 if (TryParse(s, provider, out result))
  return result;
 throw new FormatException("Der Text '" + s + "' konnte nich konvertiert werden");
}

public static bool TryParse(string s, IFormatProvider provider, out Money result)
{
 result = Money.Empty;

 // die Zahl von der Währung trennen
 string sAmount = "";
 string sCurrency = "";

 foreach (char c in s)
 {
  if (("0123456789,.-+").IndexOf(c) != -1)
   sAmount += c;
  else
   sCurrency += c;
 }

 // den Betrag lesen
 // Systemeinstellungen beachten
 decimal Amount = 0;

 // Systemeinstellungen beachten
 CultureInfo CInfo = (CultureInfo)provider;
 NumberFormatInfo NFInfo = CInfo.NumberFormat;

 if (!decimal.TryParse(sAmount.Replace(NFInfo.CurrencyGroupSeparator, ""), NumberStyles.Currency, provider, out Amount))
  return false;

 // und fertig
 result = new Money(Amount, sCurrency);
 return true;
}

Auf meiner Homepage www.KlaasWedemeyer.de findet Ihr den ganzen Quelltext und ein Beispiel. Dort werden für die Währung der dreistellige Code (EUR, USD, GBP, ...) und für die Ausgabe die üblichen Bezeichner (?, $, £, ...) verwendet. Dazu gibt es noch ein Control, das die Ein- und Ausgabe erleichtert.

Klaas Wedemeyer


Erfasst am: 08.07.2006 - Artikel-URL: http://www.devtrain.de/news.aspx?artnr=980
© Copyright 2003 ppedv AG - http://www.ppedv.de