Archiv für die Kategorie ‘Tipps und Tricks’

Was wir von einer Klotür lernen können…

Mittwoch, 01. Juli 2009

In der Informationstherorie ist die kleinste mögliche Informationseinheit ein Bit! 0 oder 1, An oder Aus, Offen oder Zu, Wahr oder Falsch. Alles mögliche Darstellungsformen, genauergesagt Interpretationsformen dieser minimalen Informationseinheit. Eine funktionierende Klotür kann nur Offen oder Verschlossen sein. Aber es existieren mehrere Interpretationsmöglichkeiten, die sich auf diesen Zustand beziehen. Wenn ich aufs Klo möchte, interessiert mich natürlich ob eine Kabine Frei oder Besetzt ist, da ich natürlich eine dieser Kabinen verwenden möchte. Befinde ich mich bereits in einer Kabine, so interessiert mich nicht mehr ob diese Kabine frei ist, denn ich weiss ja das sie es nicht mehr ist, ich bin ja schliesslich drin. Aber mich interessiert nun, ob die Tür Verschlossen oder Offen ist. Ich möchte ja schliesslich meinen Geschäftigkeiten ungestört nachgehen. Somit haben wir mehrere Interpretationsmöglichkeiten des Türzustandes. Dieser ist von dem Zustand abhängig, ob ich mich vor oder hinter der Tür befinde.

Wenn wir die Brücke jetzt zur Softwareentwicklung schlagen, könnte man das Vor bzw. Hinter der Tür als “Ich verwende eine Komponente” bzw. als “Ich entwickle eine Komponente” verstehen. Der Komponentenentwickler wird sich sicherlich für den Zustand interessieren, das eine Kabine besetzt ist, denn dann muss er mit den  ”Daten” irgendwas anstellen. Was kümmern ihn also die leeren Kabinen?

Jemand der diese Komponente jedoch verwendet, den interessiert nicht ob eine Kabine bereits besetzt ist. Ihn interessieren besetzte Kabinen nicht wirklich. Jemand der schon einmal enorm dringend aufs Klo musste, wird das nachvollziehen können. Uns interessieren in diesem Moment nicht die 15 Kabinen die Besetzt sind, uns interessiert genau die Kabine die in dem Moment frei ist.

Dies ist der Grund, warum wir in diesem Fall an der Komponenten-Schnittstelle kein Flag zur Verfügung stellen sollten, welches Kabine.IstBesetzt heisst. Dieses Flag sollte stattdessen Kabine.IstFrei heissen. Denn das interessiert den Anwender an dieser Komponente.

Der Komponentenentwickler sollte aber Intern mit dem Zustand arbeiten, den er am Häufigsten braucht (was hier der Zustand Kabine.IstBesetzt sein dürfte). Wie lösen wir dieses Dilemma? Ganz einfach, das Zauberwort heisst Kapselung. Nehmen wir als Beispiel einmal C# als Zielsprache. Dort können wir mit dem IstBesetzt intern arbeiten, bieten aber ein Property IstFrei an der Schnittstelle an:

Kabine.cs

Somit arbeitet jeder mit der Interpretation, die für ihn am Besten geeignet ist!

Diese Betrachtungsweise ist natürlich nicht nur auf Flags beschränkt! Dies gilt für alle Daten, Methoden und Properties die eine Schnittstelle nach außen offenlegt.

Zahlenvergleich

Freitag, 26. Juni 2009

Heutzutage wird es einem sehr leicht gemacht große Datenbestände zu sortieren. Es gibt vorgefertigte, hoch performante Methoden die lediglich die zu sortierenden Daten und einen Zeiger/Referenz auf eine Vergleichsmethode erwarten. Diese Methode ist im Normalfall ganz einfach gestrickt. Sie bekommt einfach zwei Parameter vom Typ der zu sortierenden Daten. Die Methode hat nichts weiter zu tun als diese Daten auszuwerten. Wenn der erste Parameter als “größer” zu bewerten ist, gibt die Methode einen positiven Wert, bei “kleiner” einen Negativen und bei Gleichheit eine 0 zurück.

Betrachten wir hier einmal den Spezialfall eines Integervergleichs. Eine Methode dafür sieht wie folgt aus:

Image

Nun gibt es aber eine Reihe von Menschen, die an einem aktuten Nanosekundenverschwendungssyndrom leiden. Bedingt durch meine technische Ausbildung, muss ich mich selbst hier dazu zählen.
Um Laufzeit zu sparen, könnte man nun auf die Folgende, doch sehr naheliegende Version dieser Methode kommen:

Image

Wenn a größer als b ist, so gibt diese Methode einen Wert größer 0, bei a < b einen negativen Wert und bei a = b eine 0 zurück.

Aber stimmt das Wirklich? Immer?

Wahrscheinlich ist die Frage nur mit Nein zu beantworten, denn sonst hätte ich mir sicherlich nicht die Mühe gemacht diesen Artikel zu schreiben. Aber woran krankt es denn nun bei dieser Lösung?

Das Problem welches wir in diesem Fall haben, ist die Beschränkung des Wertebereiches für einen Ganzahlwert. Rein mathematisch betrachtet ist die Formulierung korrekt. Geben wir den Kindern einmal Namen: Sei min der kleinste Wert und max der größte Wert den unsere Ganzzahl annehmen kann. Was passiert nun intern, wenn wir zu max noch eine 1 hinzuaddieren? Es kommt zu einem Überlauf und die Variable kippt auf den Wert min. Dies wird uns bei dieser Lösung zum Verhängnis. Betrachten wir einmal folgenden Aufruf:

Compare( max, min );

Wir erwarten hier, das die Methode einen Wert größer 0 zurück gibt. Wir erhalten jedoch -1. Dies lässt sich auch leicht nachvollziehen. Gehen wir mal von einer 32 Bit breiten Variablen aus. Diese hat einen Wertebereich von -2147483648 bis 2147483647. Wenn wir jetzt min von max abziehen, erhalten wir 2147483647 – (-2147483648) was nach Adam Riese nun 4294967295 ergibt. Das ist in Hex: 0xFFFFFFFF. Was bedeutet alle Bits sind gesetzt. Dies entspricht aber gerade (in der 2er Komplementdarstellung) die -1.

Jetzt könnte uns ja interessieren in wievielen Fällen dies passiert, um eine Abschätzung des Fehler zu geben. Dazu müssen wir uns der (sowas braucht man nur in der Schule-)Mathematik zuwenden. Zuerst müssen wir die Bedingung für gültige Vergleiche aufstellen. Das ist ziemlich einfach. Es dürfen am unteren und oberen Rand keine Überläufe stattfinden, da sonst das Vorzeichen wechselt. Daraus ergibt sich folgende Bedingung:

Image

Wir haben auch noch ein paar Randbedingungen die wir (trivialerweise) einhalten müssen:

Image

Ich möchte hier den allgemeinen Fall für eine n-Bit Breite im 2er Komplement dargestellte Zahl betrachten. Somit ergeben sich die Grenzen für min und max wie folgt:

Image

Nun können wir loslegen. Da a und b zwei unabhängige Variablen sind, kann für jeden Wert b, a Werte zwischen min und max annehmen. Wir haben also eine Matrix über a und b. Betrachten wir erst einmal die gültigen a Werte für ein gegebenes b. Dafür formen wir einfach die ursprüngliche Ungleichung nach a um und erhalten:

Image

Da aber a wegen der oben genannten Randbedingungen auch im Bereich [ min, max ] liegen muss, müssen wir die Grenzen ebenfalls auf diese Grenzen beschränken. Somit erhalten wir folgende Ungleichung:

Image

Da es sich um diskrete Werte handelt können wir nun alle möglichen gültigen Werte für a für ein gegebenes b berechnen:

Image

Uns interessieren allerdings die ungültigen Werte für a. Somit müssen wir von der Gesamtanzahl einer Zeile unsere gültigen Subtrahieren:

Image

Nun die Klammern auflösen und zusammenfassen:

Image

Somit erhalten wir die Anzahl von ungültigen Werten in einer Matrixzeile. Nun müssen wir das ganze nur noch über alle Matrixzeilen aufzummieren:

Image

Diese Formel gibt uns somit an, für wieviele Werte die Vergleichsmethode fehlschlägt. Aber wie kommen wir denn hier nun weiter? Wir haben Maxwertbildungen in einer Summe. Uffz… Nun hallen mir zum ersten mal wieder – nach sehr langer Zeit – die Worte meines Mathematikprofessors in den Ohren…

Sie dürfen keine Skrupel haben Beträge und Maximalwertbildungen in Fallunterscheidungen zu betrachten!

Ok Herr Engels. Dann mal los: Da es sich hier um einfache Maximalwertbildungen handelt, können wir das soger mit ein wenig Überlegung auch ganz einfach durchführen. Dazu ziehen wir die Summe einfach erstmal in drei Teile auseinander:

Image

Betrachten wir mal exemplarisch die 2. Summe. Wir können die Maximalwertbildung ganz einfach auflösen indem wir uns überlegen, für welche b einmal min bzw. b + min verwendet wird. Wann ist also min > b + min? Ganz einfach dann, wenn b < 0 ist. Somit müssen wir die Summe nur in zwei Teile aufteilen. Einen Teil für die negativen b und einen für b >= 0. Das sieht in diesem Fall dann wie folgt aus:

Image

Hier können wir wieder die 2. Summe auseinanderziehen. Auf diese Weise zerlegen wir auch die Zweite Maximalwertsumme. Wenn dann alles in der Ursprungsgleichung eingesetzt wird, kürzt sich eine Menge raus. Folgend also die einzelnen Schritte:

Image

Mit der folgenden Formel, die man in einem durchschnittlichen Formelwerk der Mathematik findet, können die Summen aufgelöst werden:

Image

Somit ergibt sich dann für die Anzahl der ungültigen Wertkombinationen die Formel:

Image

Uns interessiert aber wieviele Kombinationen bezogen auf alle möglichen Kombinationen fehlschlagen. Dazu müssen wir diesen Wert lediglich durch die Anzahl aller möglichen Kombinationen teilen:

Image

Nun können wir für min und max unsere Definitionen von oben einsetzen. Somit erhalten wir nach ein wenig Rechnerei folgendes Ergebnis:

Image

Hier können wir bereits den Wert n für die Bitbreite einsetzen. Aber wir sollten doch lieber einmal versuchen alles zu Vereinfachen. Macht man sich die Mühe und multipliziert das alles schön aus und fast zusammen, so kommt ein doch unerwartetes Ergebnis heraus:

Image

Das bedeutet, das diese Methode unabhängig vom darstellbaren Wertebereich stets in 25% aller Fälle falsch ist… Ups…

Dies ist – wie ich finde – ein sehr schönes Beispiel dafür, wie man sich schnell selbst ganz einfach eine Grube graben kann, nur weil man denkt es ganz geschickt und im “vorbeigehen” besser zu machen. Ausserdem zeigt es, wieviel Spass man mit einer Methode haben kann, die kaum kürzer sein könnte.

C driven C#

Mittwoch, 24. September 2008

Alte Gewohnheiten wird man oft nicht los, auch wenn es längst keinen Grund mehr gibt daran festzuhalten. So habe ich z.B. folgendes Konstrukt in unserem C#-Code gefunden, welches offensichtlich von einem eingefleischten C++ Programmierer kommt:

ClassA a = new ClassA();
if( a != null )
{
   …
}

Was in C++ noch Richtig war, ist in C# in dieser Art und Weise einfach Unsinn. In C++ konnte es natürlich passieren, wenn es keinen Speicher mehr gab, das der new-Operator als Ergebnis “NULL” lieferte. In C# jedoch wird in diesem Fall eine Exception “geschmissen”. Genauer gesagt die OutOfMemoryException. Was leicht in der MSDN nachgelesen werden kann. Sollte einem C/C++ Programmierer immer noch ein ungütes Gefühl beschleichen, steht einem ja ein Blick in die C#-Language-Specification offen, die ja bei Microsoft runtergeladen werden kann. Für die Sprachversion 1.2 ist im Kapitel 7.5.10.1 folgendes zu lesen:

AIf there is not enough memory available to allocate the new instance, a System.OutOfMemoryExceptionis thrown and no further steps are executed

Dort steht es also schwarz auf weiss. Sogar noch mit dem Hinweis, das keine Anweisung nach dem new Ausgeführt werden (was ja ein wesentlicher Teil des Exception-Handlings ist). Somit kann getrost auf die if-Abfrage verzichtet werden. Möchte man wirklich überprüfen ob nicht genug Speicher vorhanden war, kann man wie gewohnt mit try/catch die Exception abfangen. Über die Sinnhaftigkeit lässt sich vortrefflich streiten. Dies würde aber diesen Eintrag sprengen und muss nochmal an anderer Stelle etwas ausführlicher Diskutiert werden.

Bin ich noch null oder was?

Mittwoch, 24. September 2008

Da ich ja wieder mit einem Code-Review “beauftragt” wurde, mussten ja wieder Dinge für Coding-Horror abfallen. So wie auch folgendes kleines C# Konstrukt. Habe wieder alles Unnötige weggelassen. Man betrachte folgende kleine Klasse:

Image

Die Klassenvariable obj wird zuerst mit null initalisiert. Im Konstruktor wird dann überprüft, ob obj schon auf ein Objekt referenziert und wenn nicht, wird eine Instanz angelegt. Wenn wir mal in die C#-Language-Spezifikation sehen, steht dort was beim Instanziieren einer Klasse (Referenced-Type) passiert.

- A new instance of class T is allocated. If there is not enough memory available to allocate the new instance, a System.OutOfMemoryException is thrown and no further steps are executed.

- All fields of the new instance are initialized to their default values (§?5.2).

- The instance constructor is invoked according to the rules of function member invocation (§?7.4.3). A reference to the newly allocated instance is automatically passed to the instance constructor and the instance can be accessed from within that constructor as this.

Gehen wir also davon aus, das wir genug Speicher hatten und das Objekt tatsächlich angelegt werden konnte. Dann kann man dort nachlesen, das zuerst die Membervariablen (Fields) initialisiert werden. Das heisst in unserem Beispiel, das die Variable obj mit null Initialisiert wird (was auch defaultmässig passieren würde, wenn man das “= null“ weglässt) bevor der Konstruktor aufgerufen wird. Danach folgt gleich der Konstruktoraufruf. Dort wird dann auf null überprüft, was obj wie wir ja jetzt wissen “zwingend” sein muss. Somit ist diese Überprüfung im Konstruktor völlig seinfrei.

Die Initalisierung von obj kann also direkt in der Klasse erfolgen:

Dieses vorgehen hat aber einen Haken. Wenn die anzulegende Klasse (hier object) eine Exception schmeisst und diese in der eigenen Klasse (ObjectMania) abgefangen werden soll. Dann muss die Instanziierung im Konstruktor erfolgen:

Abschliessend nochmal zur Frage, ob es sinvoll ist obj mit null vorzubelegen. Im Prinzip wäre es egal, da alle neue Referenzen den Wert null haben. Von Microsoft wird jedoch empfolen, Variablen stets mit einem Defaultwert zu initalisieren (MCTS Kurs: 70-536).

ANSI-C – Darf ich alles?

Donnerstag, 15. März 2007

Dieser kleine Beitrag soll verdeutlichen, auch wenn ein ANSI-C Compiler Quellcode fehlerfrei übersetzt, es noch lange nicht heisst, dass die Ergebnisse immer vorhersagbar sind. Dazu betrachten wir die folgende, vermutlich aus ästhetischen Gründen entstandene Anweisung (x sei mal 1):

y = x++ + ++x;

Was könnten wir nach dieser Anweisung in y erwarten? Wir könnten zum Einen von links nach rechts durchgehen und sagen: x ist erst 1, also steht schonmal 1 + ++x dort. Das ++x wird aber vor der Addition erhöht, also ist es schon 2. Somit erwarten wir 1 + 2 = 3. x wird danach nochmal hochgezählt und hätte nach dieser Anweisung 4.

Zum Anderen könnten wir aber auch sagen, dass das x vor dem gesamten Ausdruck inkrementiert wird. Also hätten wir 2 + 2 = 4 für y. Und x wäre nachher sogar 5.

Dies verdeutlicht, obwohl diese Anweisung von jedem ANSI-C Compiler übersetzt werden kann, kann Ergebnis nicht vorhergesagt werden. Solche Sonderfälle werden in diesen Standards nicht aufgeführt, da es viel zu viele wären. Der Programmierer hat dafür zu sorgen, das dem Compiler klipp und klar ist, was von ihm erwartet wird. Solche Fehler können auch noch Unscheinbarer sein. Oben fällt die ungewöhnliche Struktur ja gleich ins Auge. Aber es kann auch einfacher und optisch “verschleiert” vorkommen. Ein Beispiel, wo es nicht sofort ins Auge gesprungen wäre:

z = ( x + y + z ) / ++x;

Also immer ein wachsames Auge für solche Unstimmigkeiten haben. Und selbst wenn der Compiler solche Anweisungen eindeutig auflösen könnte, sollte man auf die Komplexität und Lesbarkeit achten. Denn ich möchte persönlich keine Anweisung ala

++x += –x *= ++x -= -x;

verstehen müssen. Auch wenn es ein ANSI-Compiler ohne murren übersetzt.

continue in Schleifen

Mittwoch, 07. März 2007

Da es meist die kleinen Dinge sind, die ein Program lesbarer und wartbarer machen, möchte ich in dieser Rubrik Tipps und Tricks vorstellen mit denen wirklich jeder bei sich selbst anfangen kann (und sollte). Diese Tipps sollen als Anregung für eigene Überlegungen dienen. Vielleicht gibt es ja noch bessere oder ähnlich elegante Lösungen. Dann würde ich mich über Vorschläge und anregende Diskussionen freuen.

Als leichten und bekömmlichen Einstieg, wird ein (in der Praxis häufig vorkommendes) while-Schleifenkonstrukt untersucht.

while( A )
{
    if( B ) continue;
    C;
}

Warum ist dies eine nicht so elegante Lösung? Weil wir zwei Stufen “denken” müssen um rauszubekommen, wann C durchlaufen wird. Zuerst müssen wir feststellen, dass C nicht erreicht wird, wenn B wahr ist. Als zweiten Denkschritt folgt die Verneinung und das C dann erreicht wird, wenn B nicht wahr ist.

Wie wäre es eleganter?

while( A )
{
    if( !B )
    {
        C;
    }
}

Hier kann man auf einem Blick sehen, das C nur ausgeführt wenn B nicht zutrifft. Natürlich könnte man hier schon herrlich streiten, ob es hierbei sinvoll ist. Meine Meinung dazu ist, wenn es einen Denkschritt erspart, ist es eleganter und lesbarer. Außerdem schreibe ich meist irgendwelchen Programmiercode nur einmal, aber gelesen wird er hunderte von malen. Wie kann man also enorme Zeit in der Softwareentwicklung sparen? Genau: Vordenken!

Als zweiter Grund ist anzuführen, das auch break und continue Anweisungen den Programmcode schwerer lesbar machen können. Deshalb sollten sie sparsam eingesetzt und kommentiert werden. Warum das erste Beispiel daher nicht so elegant ist, ist ersichtlich wenn man sich folgendes Äquivalent dazu ansieht:

WhileLabel: while( A )
{
    if( B ) goto WhileLabel;
    C;
}

Hier würde jeder Programmierlehrling schon Einwände bringen. Natürlich soll von break und continue gebrauch gemacht werden. Jedoch sparsam und sacht. Unnötiges sollte hingegen, auch wenn es ein wenig Vordenken erfordert, ausgemerzt werden.