Die wundersame Welt der Softwareentwicklung
(präsentiert in grauenhaftem HTML, aber es ist schliesslich kein HTML-Kurs)
Zum →Bookmark

0.01 Lizenz

Creative Commons License
Das folgende Zeug ist unter einer Creative Commons-Lizenz lizenziert.

1. Prä-Prolog

Vor das Vorwort sei noch folgender Satz gestellt:

„DON'T PANIC“ [Douglas Adams] oder übersetzt:

„Versuchen Sie, dies hier nicht ernster zu nehmen als unbedingt nötig“ [cbx]


2. Prolog

Vorab seien folgende Worte zur meditativen Kontemplation gestellt:

Mit dieser Basisausstattung an Lebensweisheit sollte es gelingen, sich der Kunst der Programmierung von Mikrocomputern (also PCs) zu nähern. Dass hierbei die Betrachtung von PCs gegenüber den Großrechnern dominiert hat mehrere Gründe:

Mit der aktiven Beherrschung einer Programmiersprache der dritten Generation (3GL) erwirbt man sich einen Erfahrungsschatz, der die Einarbeitung in weitere Sprachen der dritten und vierten Generation wesentlich vereinfacht.

Wer lesen kann...

Aus Gründen der Übersichtlichkeit stehen hier nur zwei Empfehlungen:

Ein kleiner historischer Abriß soll den Einstieg plastisch gestalten:


3. Historischer Abriss

Die Ära der persönlichen Computer, die auch gewöhnlichen Privatpersonen das "Programmieren"; ermöglichte, begann vielleicht 1975 mit dem Altair 8800, einem Gerät von bemerkenswert nichtexistentem praktischen Nutzwert.




Dennoch markiert der Altair 8800 einen Meilenstein, da er nicht nur als erster in Serie hergestellter Consumer-Computer einen industriell standardisierten Bus (S100-Bus) anbot sondern auch, weil etwas später ein unbekanntes Startup-Unternehmen mit dem Namen Microsoft einen BASIC-Interpreter dafür programmierte. Der Altair war um den damals neuen Intel 8080 Prozessor aufgebaut und brachte mit 2MHz Taktfrequenz eine damals kaum vorstellbare Rechenleistung in die Bastelzimmer (wozu auch immer). Der Altair wurde über Kippschalter an der Front direkt in 8080-Maschinensprache programmiert. Es gibt heute noch einen - nicht ganz ernst zu nehmenden - Altair 8800-Emulator, der unter Windows läuft.

Bis nach 1980 dominierten auch im Bereich des Office computing 8bit-Architekturen um die Prozessoren Intel 8080, 8085 und Zilog Z80, die mit Taktfrequenzen bis 8Mhz und Speichergrößen bis 64kBytes RAM über serielle Terminals durchaus anspruchsvolle Mehrbenutzersysteme unter dem Betriebssystem CP/M bedienten. Im Standard Lieferumfang dieser professionellen DV-Anlagen befand sich meiste ein BASIC-Interpreter zur Erstellung der erforderlichen Unternehmenssoftware. Kommerzielle Standardapplikationen waren zu dieser Zeit noch nicht üblich.

Erst 1981 brachte IBM mit dem 8088-Prozessor des IBM-PC halbherzig die modernere 16bit-Technologie in den Mainstream ein. Auch der IBM-PC verfügte über ein (wenigstens ansatzweise) "standardisiertes" Bussystem und ermöglichte es somit Drittherstellern, eigene Hardwareerweiterungen anzubieten

Mit dem Erfolg des PC tauchten langsam auch andere Programmiersprachen im Blickfeld der Programmierer auf. Insbesondere die Firma Borland machte sich damals mit dem legendärenTurbo-Pascal, einer sehr effizient und stabil arbeitenden Implementierung des Pascal-Standards von Nikolaus Wirth, einen guten Namen.

Weiterhin konnte auch die damals bereits allgegenwärtige Firma Microsoft mit ihrer Entwicklungsumgebung Programmers Workbench und dem ursprünglich von Lattice gekauften C-Compiler mittelfristig der im UNIX-Bereich populärsten Programmiersprache C am PC eine Vormachtstellung verschaffen. Schließlich wurde C, versehen mit diversen Standardisierungen und Erweiterungen, zusammen mit dem Nachfolger C++ zur derzeit meist benutzten Programmiersprache.

Genau deshalb, und wegen einiger sehr unangenehmer Eigenschaften dieser Sprache, wird zum Einstieg in die Programmierung sehr gerne mit ANSI-C begonnen und nach zahlreichen schmerzhaften Basiserfahrungen der genussvolle Aufstieg nach C++, Java oder C# gewagt.

Nun ist es aber andererseits so, dass, wie insbesondere desillusionierte Techniker immer wieder gerne behaupten, Wirtschaftler ohnehin keinen nennenswerten Bezug zur harten Relaität anstreben, und auch die Wirtschaftsinformatiker unter den Informatikern nicht die jenigen sind, die Tagelang auf dem Bauch durch das Dickicht der Treiberprogrammierung, harten Echtzeitanforderungen, Taskwechsellatenzen und Scheduling-Prioritäten robben.

Aus diesem Grunde wurde beschlossen, im Wintersemester 2005 erstmals den Einstieg in die Programmierung mit einer der komfortabelsten Sprachen der dritten Generation zu beginnen. In diesam Jahr haben die Studierenden des Jahrgangs 2005 die Cahnce, an einem spannenden Experiment teilzunehmen. Das Experiment heisst:


4. Warum uns Softwareentwicklung in C# alle zu glücklichen und besseren Menschen macht

Oder warum das vielleicht doch etwas zu viel erwartet ist. Um zu verstehen, was es mit C# auf sich hat, ist ein weiterer kleiner historischer Abriss nötig.

Generation 1: Maschinensprache.

Die Computer der ersten Generation wurden noch wirklich physisch programmiert, anfänglich durch Steckverbindungen, später durch Kippschalter (wie der Altair), noch später durch Lochkarten und -streifen. Die fortschrittlichste Methode der Programmierung in der so genannten ersten Gerneration bestand in der alphanumerischen Eingabe von hexadezimalen Zeichenfolgen, die direkt das Maschinenprogramm und seine Daten darstellten.

Ein Maschinenprogramm sieht in "lesbarer" Darstellung als so genannter Hexdump so aus:

00000000  7f 45 4c 46 01 01 01 00  00 00 00 00 00 00 00 00  |.ELF............|
00000010  01 00 03 00 01 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  10 01 00 00 00 00 00 00  34 00 00 00 00 00 28 00  |........4.....(.|
00000030  0b 00 08 00 55 89 e5 83  ec 08 83 e4 f0 b8 00 00  |....U...........|
00000040  00 00 29 c4 c7 04 24 00  00 00 00 e8 fc ff ff ff  |..)...$.........|
00000050  b8 00 00 00 00 c9 c3 00  48 65 6c 6c 6f 20 77 6f  |........Hello wo|
00000060  72 6c 64 0a 00 00 47 43  43 3a 20 28 47 4e 55 29  |rld...GCC: (GNU)|
00000070  20 33 2e 33 2e 35 2d 32  30 30 35 30 31 33 30 20  | 3.3.5-20050130 |
00000080  28 47 65 6e 74 6f 6f 20  33 2e 33 2e 35 2e 32 30  |(Gentoo 3.3.5.20|
00000090  30 35 30 31 33 30 2d 72  31 2c 20 73 73 70 2d 33  |050130-r1, ssp-3|
000000a0  2e 33 2e 35 2e 32 30 30  35 30 31 33 30 2d 31 2c  |.3.5.20050130-1,|
000000b0  20 70 69 65 2d 38 2e 37  2e 37 2e 31 29 00 00 2e  | pie-8.7.7.1)...|
000000c0  73 79 6d 74 61 62 00 2e  73 74 72 74 61 62 00 2e  |symtab..strtab..|
000000d0  73 68 73 74 72 74 61 62  00 2e 72 65 6c 2e 74 65  |shstrtab..rel.te|
000000e0  78 74 00 2e 64 61 74 61  00 2e 62 73 73 00 2e 72  |xt..data..bss..r|
000000f0  6f 64 61 74 61 00 2e 6e  6f 74 65 2e 47 4e 55 2d  |odata..note.GNU-|
00000100  73 74 61 63 6b 00 2e 63  6f 6d 6d 65 6e 74 00 00  |stack..comment..|
00000110  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000130  00 00 00 00 00 00 00 00  1f 00 00 00 01 00 00 00  |................|
00000140  06 00 00 00 00 00 00 00  34 00 00 00 23 00 00 00  |........4...#...|
00000150  00 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
00000160  1b 00 00 00 09 00 00 00  00 00 00 00 00 00 00 00  |................|
00000170  80 03 00 00 10 00 00 00  09 00 00 00 01 00 00 00  |................|
00000180  04 00 00 00 08 00 00 00  25 00 00 00 01 00 00 00  |........%.......|
00000190  03 00 00 00 00 00 00 00  58 00 00 00 00 00 00 00  |........X.......|
000001a0  00 00 00 00 00 00 00 00  04 00 00 00 00 00 00 00  |................|
000001b0  2b 00 00 00 08 00 00 00  03 00 00 00 00 00 00 00  |+...............|
000001c0  58 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |X...............|
000001d0  04 00 00 00 00 00 00 00  30 00 00 00 01 00 00 00  |........0.......|
000001e0  02 00 00 00 00 00 00 00  58 00 00 00 0d 00 00 00  |........X.......|
000001f0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00000200  38 00 00 00 01 00 00 00  00 00 00 00 00 00 00 00  |8...............|
00000210  65 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |e...............|
00000220  01 00 00 00 00 00 00 00  48 00 00 00 01 00 00 00  |........H.......|
00000230  00 00 00 00 00 00 00 00  65 00 00 00 59 00 00 00  |........e...Y...|
00000240  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
00000250  11 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
00000260  be 00 00 00 51 00 00 00  00 00 00 00 00 00 00 00  |....Q...........|
00000270  01 00 00 00 00 00 00 00  01 00 00 00 02 00 00 00  |................|
00000280  00 00 00 00 00 00 00 00  c8 02 00 00 a0 00 00 00  |................|
00000290  0a 00 00 00 08 00 00 00  04 00 00 00 10 00 00 00  |................|
000002a0  09 00 00 00 03 00 00 00  00 00 00 00 00 00 00 00  |................|
000002b0  68 03 00 00 15 00 00 00  00 00 00 00 00 00 00 00  |h...............|
000002c0  01 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
000002d0  00 00 00 00 00 00 00 00  01 00 00 00 00 00 00 00  |................|
000002e0  00 00 00 00 04 00 f1 ff  00 00 00 00 00 00 00 00  |................|
000002f0  00 00 00 00 03 00 01 00  00 00 00 00 00 00 00 00  |................|
00000300  00 00 00 00 03 00 03 00  00 00 00 00 00 00 00 00  |................|
00000310  00 00 00 00 03 00 04 00  00 00 00 00 00 00 00 00  |................|
00000320  00 00 00 00 03 00 05 00  00 00 00 00 00 00 00 00  |................|
00000330  00 00 00 00 03 00 06 00  00 00 00 00 00 00 00 00  |................|
00000340  00 00 00 00 03 00 07 00  09 00 00 00 00 00 00 00  |................|
00000350  23 00 00 00 12 00 01 00  0e 00 00 00 00 00 00 00  |#...............|
00000360  00 00 00 00 10 00 00 00  00 68 65 6c 6c 6f 2e 63  |.........hello.c|
00000370  00 6d 61 69 6e 00 70 72  69 6e 74 66 00 00 00 00  |.main.printf....|
00000380  13 00 00 00 01 05 00 00  18 00 00 00 02 09 00 00  |................|
Wenn das nicht aufschlussreich ist…

Ein Luxusmodell dieser Rechnergeneration war der Multitech MicroProfessor (Unheimlich geiler multimedialer Link!).

Generation 2: Assemblersprachen.

Dass diese Art der Programmierung nicht übermäßig anschaulich war,erschliesst sich ohne weitere Erkläungen. Deshalb wurden sehr bald Programme (so genannte Assembler) geschaffen, die eine mnemonische Darstellung der einzelnen Anweisungen des Programmes von einer Textform in Maschinensprache übersetzten. Der Vorteil lag auf der Hand: Der Mnemonics-Code war im Vergleich zu seitenlangen Hexdumps geradezu phantastisch gut lesbar, wie das folgende Beispiel zeigt (es handelt sich um das selbe Programm wie oben):

        .file   "hello.c"
        .section        .rodata
.LC0:
        .string "Hello world\n"
        .text
.globl main
        .type   main, @function
main:
        pushl   %ebp
        movl    %esp, %ebp
        subl    $8, %esp
        andl    $-16, %esp
        movl    $0, %eax
        subl    %eax, %esp
        movl    $.LC0, (%esp)
        call    printf
        movl    $0, %eax
        leave
        ret
        .size   main, .-main
        .section        .note.GNU-stack,"",@progbits
        .ident  "GCC: (GNU) 3.3.5-20050130 (Gentoo 3.3.5.20050130-r1)"

Ein nettes Beispiel aus dieser Generation ist der damals recht beliebte AIM-65.

Generation 3: Die Hochsprachen.

Die Assembler-Sprachen hatten einen Nachteil, der sich mit der Weiterentwicklung der Rechnersysteme zunehmend als störend erwies. Die Assembler-Sprachen waren, trotz Makroassembler und umfangreichen Funktionsbibliotheken, in ihrer Ausdrucksmöglichkeit starr an dien Befehlssatz der jeweiligen CPU gebunden. Damit war ein Wechsel auf eine bessere Hardware fast immer ein Totalschaden, da die leistungsfähigeren CPUs damals auch immer einen leistungsfähigeren (aber inkompatiblen) Befahlssatz mitbrachten (Die heilige Kuh der unumstößlichen Binärkompatibilität wurde erst später von IBM und Microsoft inthronisiert).

Da gleichzeitig die Rechenleistung der CPUs bereits "erhebliche" Ausmasse erreicht hatte, war es erstmals möglich über eine Abstraktion der allgemeinen Grundfunktionen, wie sie sich bisher in den Maschinenbefehlen manifestierten, nachzudenken. Mit den Urvätern der populären Prorammierung FORTRAN und BASIC hielten einige Konzepte Einzug, die bis heute Gültigkeit haben. In weiterer Folge gewannen die Compilersprachen um Pascal und C an Beliebtheit und damit an Bedeutung. Das Betriebssystem UNIX wurde (so geht die Legende) 1969 fast allein von Ken Thompson erstmals implementiert und kurze Zeit später (naja, 1973) mit dem Zeugungsvater der Sprache "C", Dennis Ritchie, in genau dieser Sprache reimplementiert, um kurz darauf einen Siegeszug durch die damalige Datentechnik anzutreten.

Die Hochsprachen der dritten Generation lieferten den Schub für den Einbruch der IT in den Alltag. Mit der Abstraktionsfähigkeit und der relativen Platformunabhängigkeit der neuen Systeme konnte erstmals leicht wartbarer und wiederverwendbarer Code erzeugt werden. Und die Wiederverwendbarkeit ging sogar über die Grenzen des ursprünglichen Rechnersystems hinaus, womit die Erstellung hochkomplexer und unfangreicher Bibliotheken, wie beispielsweise der NAG Libraries die Fortschritte im Bereich der angewandten Informatik potenzierten. Wir werden all die Eigenschaften dieser Hochsprachen im Folgenden detailliert kennenlernen. Das bereits sattsam bekannte Programm sieht in der Hochsprache C so aus:

#include <stdio.h>

int main(int argc, char **argv)

    {
    printf("Hello world\n");
    return 0;
    }

In unserer Traum- und Wunschsprache C# sieht wird das selbe Ergebnis mit folgendem Programmtext erreicht:

using System;

class MainClass
{
    public static void Main(string[] args)
    {
        Console.WriteLine("Hello World!");
    }
}

Einer der legendärsten Computer im Zusammenghang mit dieser Entwicklung ist die DEC PDP-7 der erste Minnicomputer, auf dem Ken Thompsons UNICS lief. Beeindruckend ist dabei auch, dass die PDP-7 damals mit satten 9 KBytes core RAM und einer 18-Bit CPU ungefähr die Leistungsfähigkeit eines Commodore C64 erreichte.


5. Das große Warum

Warum wird programmiert? Wollen wir hier nur die Gegenwart betrachten (die Vergangenheit ist ja immerhin schon vorbei und Vorhersagen gestalten sich schwierig, insbesondere, wenn sie die Zukunft betreffen), so ergeben sich folgendes Varianten:

  1. Customising: Durch Programmierung werden bestehende große Systeme an Kundenanforderungen angepast (z.B. SAP).

  2. Application development: Ein vollständiges Anwendungsprogramm mit (mehr oder weniger) neuen Funktionen wird auf ein Betriebssystem aufgesetzt (z.B. WORD.EXE).

  3. Distributed web applications: Durch Programmierung Applikationssysteme erstellt, die über das Internet einen hohen Grad an Interaktivität und Funktionsumfang zur Verfügung stellen und somit lokal installierte und arbeitende Programme (z.B. WORD.EXE) ablösen.

  4. Embedded Systems: Auf einer sehr kleinen speziellen Plattform wird eine sehr spezifische Funktion direkt implementiert (z.B. ABS, DVB-Receiver, Waschmaschine)

  5. OS development: Ein Betriebssystemkern wird neu oder weiter entwickelt (z.B. Windows Vista, HURD [wird nächstes Jahr fertig], LINUX).

Satz: „C ist eine der am universellsten einsetzbaren Sprachen, da sie sehr effizient compiliert wird und sehr weitgehenden Zugriff auf Hardware-Ressourcen erlaubt. Außerdem ist C eine der gefährlichsten Sprachen, da sie sehr effizient compiliert wird und sehr weitgehenden Zugriff auf Hardware-Ressourcen erlaubt. Zusätzlich wird C auf praktisch allen bekannten CPUs (von 8 bis 64 bit) und Betriebssystemen unterstützt.“

Noch ein Satz: „C# ist eine recht universell einsetzbare Sprache, da sie sehr sicher codiert wird und sehr weitgehenden Schutz vor üblichen Fehlerpotenzialen bietet. Außerdem bietet C# hervorragende Möglichkeiten zum Rapid Application Development (RAD). Zusätzlich wird C# offiziell auf sämtlichen relevanten Betriebssystemplattformen der Welt unterstützt (Laut Microsoft sind das Windows 2000 und XP)“

Die ersten drei Punkte können mit beiden Sprachen abgedeckt werden, wobei der Vorteil klar auf Seiten der moderneren und sichereren Sprache C# liegt. Themen wie Embedded Systems und OS-Development sind in C# aufgrund des CLR-Systems (derzeit) nicht sinnvoll umzusetzen.


6. Das noch größere Wie?

Wie wird programmiert? Im Bereich der Wirtschaftsinformatik kommen lediglich Aufgaben des Typs 1, 2 und 3 vor. Für Customising werden vorwiegend Sprachen der vierten Generation verwendet, für die Applikationsentwicklung vorwiegend Sprachen der dritten Generation wie

Programmieren bedeutet, einem von jeder Art von Verstand und Intelligenz freien Gegenüber zu erklären, was er zu tun hat, um eine geforderte Aufgabe zu lösen. Die Eigenschaft „von jeder Art von Verstand und Intelligenz frei“ stellt hierbei die entscheidende Herausforderung dar. Es gilt, zur Bewältigung dieser Aufgabe eine Kommunikationsform zu wählen, die beim Empfänger weder das eine noch das andere voraussetzt.

Die Standardlösung für diese Aufgabe besteht bis heute meist in der Definition einer speziellen (meist problemorientierten) Sprache, die ein sehr kleines Vokabular und eine extrem strenge Grammatik (Syntax) aufweist.

Satz: „Die Kunst des Programmierens besteht lediglich darin, diese Sprache so gut zu beherrschen, dass damit eine Aufgabenstellung korrekt und vollständig ausgedrückt werden kann.“


7. Wie man C# programmiert, ohne den Verstand zu verlieren

Steinzeit

Der kleine historische Abriss in Kapitel 4 deutet schon den steten Paradigmenwandel in der Methodik der Programmierung an. Während in der ersten Generation der Rechner zuerst für jede neue Aufgabenstellung umgebaut werden musste, ermöglichten ausgefeiltere Systeme schliesslich das direkte Einschreiben des Programmes und der Daten in den Arbeitsspeicher

Um herauszufinden, welche Zahlenwerte an welche Speicheradressen geschrieben werden mussten, war ein aufwändiges Procedere erforderlich. Zuerst musste der gewünschte Algorithmus entwickelt (Kopf & Papier), anschliessend anhand des Systemhandbuches in eine der Rechnerarchitektur (Befehlssatz und Datenwortbreite) angepasste Notation transkribiert (Kopf & Papier), in eine Hexadezimale oder binäre Darstellung gebracht (Kopf & Papier) und schlussendlich vom Papier in den Speicher übertragen (Kopf, Papier & Hand) werden. Um komplexe Programme nach dieser Methode zu erstellen, hätten ganze Wälder geopfert werden müssen.

Die so vom Operator direkt eingeschriebenen Daten und Anweisungen wurden vom Prozessor direkt ausgeführt (und das nur in seltenen Fällen auf Anhieb so, wie vom Programmierer gedacht). Durch den sehr unständlichen Weg der Programmentwicklung waren die Fehlermöglichkeiten umfassend, allgegenwärtig und ärgerlich, weil einfache Schreib- und Übertragungsfehler die häufigste Ursache waren. Es bedarf keiner großen Phantasie, um sich vorzustellen, welch abendfüllendes Vorhaben die Fehlersuche auf solchen Systemen war.

„An dieser Stelle freuen wir uns erstmals über die Gnade der späten Geburt.“

Bronzezeit

Die Rechner wurden interaktiver und bekamen Teletypes. Das Teletype (TTY) war eine rustikale Kombination aus Tastatur und Drucker, über die der Rechner Eingaben entgegennahm und Ausgaben (auf Papier!) darstellte (Wir denken hier wieder an die Wälder…). Das Bild der PDP-7 in Kapitel 4 zeigt ein solches TTY. Die UNIX-Devices für Konsolen /dev/tty erinnern übrigens heute noch daran. Auch aufgrund der dramatisch verbesserten Kommunikationsmöglichkeit auf der Ebene geschriebenen Textes ergab sich die prinzipielle Möglichkeit, einige Schritte zu vereinfachen und zu automatisieren.

Die Befehle der CPU hatten intern immer schon Namen gehabt, die deren Funktion ausdrückten (LOAD, PUSH, ADD usw.). Nun konnte ein verhältnismäßig einfaches Programm (Assembler) dazu dienen, diese, als Text im Speicher vorliegenden Anweisungen automatisch in korrekte Maschinenbefehle (also Zahlen) umzusetzen. Zusammen mit einem kleinen Programm (Editor) zum Einschreiben der Programmexte in den Arbeitsspeicher waren viele Fehlermöglichkeiten ausgeschlossen und gleichzeitig die Lesbarkeit und Wiederherstellbarkeit der Programme enorm verbessert worden. Mit der Kombination Editor/Assembler begann der Siegeszug der modernen Programmierung.

Einem Problem aber hatte die neue Technologie noch nichts entgegenzusetzen. Ein fehlerhaftes Programm brachte das gesamte System weiterhin meist in einen unbrauchbaren Zustand, was weder der Zielerreichung noch der Fehlersuche zuträglich war.

„An dieser Stelle stellen wir uns vor, wie Ken Thompson im Jahre 1969 vor einem TTY sitzt und mal eben mit Editor und Assembler UNICS programmiert.“

Eisenzeit

Rund um UNIX kamen Betriebssysteme in Mode, die hauptsächlich drei Zwecken dienen sollten:

  1. Hardwareabstraktion: Das Betriebssystem stellt standardisierte Funktionen bereit, um grundlegende Operationen ohne Kenntnis der verwendeten Hardware durchzuführen (z.B. Ein Zeichen am TTY ausgeben, ein Zeichen vom TTY lesen).
  2. Bibliotheksfunktion: Grundlegende Funktionen sollten nicht immer wieder neu erfunden werden müssen (z.B. Ein- und Ausgabe)
  3. Systemschutz: Zusammen mit neuen Möglichkeiten der moderneren CPUs (Exceptions, Memory management) konnte ein Betriebssystem schwerwiegendes Fehlverhalten von Programmen erkennen und dessen Auswirkungen verhindern (indem es die Ausführung des Programms kurzerhand beendete; Hat hier jemand „Allgemeine Schutzverletzung“ gerufen?). Damit blieb das System weiterhin interaktiv, was eine Fehlersuche (relativ) enorm vereinfachte.

Mit der von Dennis Ritchie entwickelten Sprache C setzten sich die Compilersprachen in grossem Stil durch. C bot einen für die damalige Zeit optimalen Kompromiß aus Komfort und Features an. Wiederum verschob sich das Paradigma der Programmierung erneut. Die Hochsprache wies einen Befehlssatz auf, der

war. Ausserdem wurde der brave, gutmütige aber dümmliche Assembler durch einen neuen eisernen Besen ersetzt, den Compiler. Der Compiler legte erstmals "strenge formale Kriterien" an die Syntax des Codes. Dennoch übersetzte der Compiler ein korrektes C-Programm in sehr effizienten Maschinencode. Wiederum waren einige Fehlermöglichleiten der Assemblerprogrammierung ausgemerzt. Die Architektur der Sprache erlaubte dennoch dank "Features" wie Pointern beliebeigen Typecasts weitgehenden Zugriff auf die Hardware, sodass auch Betriebssysteme und Treiber in C entwickelt werden konnten.

Die Toolchain des C-Programmierers war anfangs noch ähnlich rustikal wie zu Assembler-Zeiten. Erst mit Aufkommen der seriellen Text-Terminals, wie z.B. des legendären DEC-VT100 wurden revolutionäre Methoden der Benutzerinteraktion möglich. War es auf dem TTY schon Luxus, einzelen Zeilen eines Programms nachträglich verändern zu können, so entstand nun mit der überwältigenden Menge gleichzeitig darstellbarer 24 Zeilen zu 80 Zeichen die technische Grundlage zum revolutionären Full Screen Editing. Auch das Scrollen wurde damit erfunden und die Wälder fürchteten die Programmierer nicht mehr ganz so sehr. In der Folge entstanden dann sehr schnell die beiden Religionen EMACS und VI, die sich einen fundamentalistischen Kampf um den einzig wahren full screen editor lieferten.

Auch der full screen editierende C-Programmierer der ersten Stunde verbrachte seine Zeit damit, in konstanten Iterationen so etwas wie

k3wl6uy@hackbox: ~$ vi main.c
k3wl6uy@hackbox: ~$ cc main.c -o kewlcode
aufzurufen. Erst als das Programmieren aufhörte, eine elitäre Beschäftigung für die toughsten Typen zu sein und zunehmend auch Geeks und Weicheier zu programmieren begannen, setzte sich die Idee durch, den Editor derart zu erweitern, dass er auch den Aufruf des Compilers und die Auswertung der Fehlermeldungen übernahm. So entstand das Integrated Development Enviroment IDE. Seitdem hat ist die IDE aus keinem Software Development Kit (SDK) mehr wegzudenken.

Die Kombination von Betriebssystem und Compilersprache verhalfen dem Programmierer bei den Systemadministratoren und den restlichen Usern zu einem leichteren Stand, da nicht jeder Programmfehler automatisch das gesamte System lahm legte. Nichts änderte sich allerdings an der Problematik, dass fehlerhafte Programme weiterhin zu rituellem Selbstmord neigten.

Zeitgleich und parallel dazu erlangte auch eine andere Philosophie grosse Bedeutung. Der Beginners All-purpose Symbolic Instruction Code (BASIC) mauserte sich zum Superstar der Interpretersprachen, die damals auch im professionellen Einsatz durchaus geschätzt wurden. Der Reiz einer Interpretersprache liegt darin, dass das Benutzerrogramm direkt aus seiner Textdarstellung heraus von einem anderen Programm (dem Interpreter) abgearbeitet wird. Ein interpretiertes Programm läuft natürlich nicht annähernd so schnell wie ein compiliertes, aber wesentlich sicherer. Der Interpreter kann, sofern er korrekt implementiert ist, die Ausführung systemschädigender Anweisungen nicht nur verhinden, sondern sogar die Ursache verständlich dokumentieren.

„An dieser Stelle stellen wir uns vor, wie ein kleines Team von langhaarigen Bastlern rund um Bill Gates BASIC- Interpreter für alle möglichen Computer bastelt und von einem weltweiten Monopol auf PC-Software träumt.“

Neuzeit?

Sogar der dreieinhalbste Schritt in der Entwicklung fand vorerst ohne Microsoft statt. Das Dilemma zwischen den "sicheren" Interpretersprachen und den "schnellen" Compilersprachen schmerzte die Entwickler lange und nachhaltig. Wie so oft wurde nach einer Marketing-Lösung gesucht, die sämtliche Vorteile vereint, ohne irgendeinen Nachteil in Kauf nehmen zu müssen. In diesem Solderfall gelang dies schleisslich sogar fast. Es war - nein, nicht Microsoft und auch nicht SUN Microsystems - die University of California San Diego (UCSD) mit dem wegweisenden UCSD-Pascal , das unter der Federführung von Kenneth Bowles dort entstand. Wiewohl Pascal eigentlich auch zu den Compilersprachen zählt, unterschied sich die UCSD-Version in einem wesentlichen Punkt. Der Compiler generierte keinen Maschinencode für eine spezielle CPU sondern so genannten P-Code. Dieser P-Code wurde dann von einem sehr kleinen und effizient arbeitenden Interpreter mit honer Performance abgearbeitet, ohne die Sicherheitsvorteile des Interpretersystems dabei aufgeben zu müssen.

All das fand allerdings in den siebziger Jahren des letzten Jahrhunderts statt statt und die damals erhältlichen Rechner litten immer noch erheblich unter dem, wenn auch kleinen, Performancenachteil des P-Code systems. Deshalb tauchte diese Idee auch für einige Zeit wieder unter. Viel später griff Sun Microsystems diese Idee wieder auf und setzte sie im industriellen Maßstab um. Das Ergebnis hieß Java. Über die Bedeutung von Java ist müssen hier keine grossen Worte verloren werden, es hat sich als Plattform von Handy-Spiel bis Business-Applikation zwischen PDA und Rechenzentrum einen festen Platz erworben.

„Monopolritter Bill und seine Mannen betreten, wie immer verspätet, von der Seite die Bühne.“

Der Erfolg von Java, insbesondere im Web, hatte Microsoft, wie so oft völlig überrascht, in diesem Fall sogar so, dass MS anfangs SUNs Runtime engine für Windows lizenzierte. War diese Situation schon unbefriedigend genug, entstanden ernstere Probleme, als MS begann, die beliebten MS-spezifischen Erweiterrungen auch in Java zu integrieren. Die Anwälte von Sun Microsystems stellten unmißverständlich klar, daß dies eine Verletzung der Lizenz und somit verboten war. Damit war die Schmerzgrenze überschritten.

Microsoft tat, was Microsoft so oft schon erfolgreich getan hatte. Microsoft erfand Java noch einmal. Immerhin hatte mandieses mal mit Anders Hejlsberg einen wirklich fähigen Mann, der sich in der Vergangenheit mit Turbo Pascal einen Namen gemacht hatte. Weil der Name ja bereits geschützt war, musste ein Neuer her: Das neue Produkt war eine Art Weiterentwicklung der Linie von C nach C++. Deshalb lag ein Name mit "C" auf der Hand. Die Note C# ist immerhin ein Halbton mehr als C, das genügte für Microsoft. Dass die Laufzeitumgebung zu C# (mithin Bestandteil des .NET-Frameworks) nur unter Microsoft-Betriebssystemen lauffähig ist, versteht sich dabei natürlich von selbst. Und - voila - wir sind endlich bei C# angekommen. Uff.

Satz: „C# ist ein proprietäres Microsoft-Derivat von Java, das gegenüber dem Original keine nennenswerten Vorteile, aber einige entscheidende Nachteile aufweist. Deshalb wird es sich mit Sicherheit sehr schnell auf breiter Front durchsetzen.“

Inzwischen gibt es - wichtig fürFreunde freier Software - mit dem MONO Projekt von Miguel de Icaza eine freie Implementierung des .NET Frame works, mit einer gut funktionierenden CLR und einer sehr vollständigen C#-Implementierung. Es bleibt abzuwarten, ob die freie Entwicklergemeinde mit den Entwicklungssprüngen bei Microsoft Schritt halten kann, derzeit liegt Mono recht gut im Rennen und könnte - sehr zum Unwillen von Microsoft - erstmals dafür sorgen, dass ein und das selbe ausführbare Programm sowohl unter Windows als auch unter GNU/Linux unverändert lauffähig ist.

Von diesem historischen Schweinsgalopp beflügelt können wir uns jetzt in medias res begeben.

Na wie jetzt?

Der erste Schritt, im Kontakt mit C# einen augenblicklichen Ausfall aller höheren Hirnfunktionen zu verhindern, ist der Einsatz einer IDE und die sichere Beherrschung derselben. Dies kann einige Tage dauern.

Der zweite Schritt besteht in der Aneignung der geeigneten Fachterminologie, um die eigenen Bemühungen adäquat kommentieren zu können. Dies kann einige Wochen dauern

Der dritte Schritt besteht im Erlernen der eigentlichen Programmiersprache in Vokabular (leicht) und Syntax (schwer). Dies kann einige Monate dauern.

Der letzte Schritt besteht im festigen der Kenntnisse und Erwerben von Erfahrung durch praktischen Einsatz. Dies wird einige Jahre dauern.

Beginnen wir also lieber mit den ersten drei Schritten, so lange noch Zeit dazu ist. Erwartungsgemäß beginnen wir mit Schritt drei.


8. Zum Kern vordringen heißt lallen lernen

Ein gültiges C#-Programm besteht aus einer Aneinanderreihung von Worten, die aus Buchstaben des ASCII-Zeichensatzes bestehen dürfen. Dies bedeutet - im Gegensatz zu den meisten ordentlich definierten Sprachen, aber in Übereinstimmung mit den Microsoft-Sitten - , dass in einem Wort Sonderzeichen wie Umlaute prinzipiell vorkommen dürfen (wie z.B. in Visual Basic). Dennoch rate ich strikt davon ab, diese Möglichkeit auszunutzen, da die dabei entstehenden Probleme den geringen Vorteil nicht rechtfertigen. Stellen Sie sicg ggf. einfach vor, wie unterhaltsam es wäre, ein Programm mit tschechischen oder slowakischen Bezeichnernamen auf einem deutschsprachigen Rechner zu bearbeiten. Das allein genügt aber noch nicht.

Die Worte werden durch Whitespace, also Leerzeichen getrennt. Dabei sind beliebige Mengen von Leerzeichen (Space), Ttabulatoren und Zeilentrenner weitgehend äquivalent. Dies ermöglicht, wie wir noch kennen lernen werden, albtraumhafte Möglichkeiten der Quelltextformatierung.

Erschwerend kommt hinzu, dass strikt zwischen grossen und kleinen Buchstaben unterschieden wird.

Aus den Worten werden Bezeichnerund Schlüsselwörter formuliert und zu Anweisungenzusammengesetzt. Die Anweisungen werden immer mit einem Semikolon „;“ abgeschlossen.

„Man beherrscht C# sicher, wenn man ohne Nachdenken die Strichpunkte richtig setzt“

Das Vokabular von C# besteht aus so wenigen Worten, dass es trügerisch einfach wirkt:

„abstract, as, base, bool, break, byte, case, catch, char, checked, class, const, continue, decimal, default, delegate, do, double, else, enum, event, explicit, extern, false, finally, fixed, float, for, foreach, goto, if, implicit, in, int, interface, internal, is, lock, long namespace, new, null, object, operator, out, override, params, private, protected, public, readonly, ref, return, sbyte, sealed, short, sizeof, stackalloc, static, string, struct, switch, this, throw, true, try, typeof, uint, ulong, unchecked, unsafe, ushort, using, virtual, volatile, void, while“

Diese Schlüsselwörter (keywords) dürfen als einzige nicht als Bezeichner verwendet werden. Unter einem Bezeichner versteht man den Name einer Variablen, einer Funktion oder eines Typen. Ein Bezeichner darf aus allen gültigen (also ASCII) Buchstaben bestehen, muss aber mit einem Buchstaben oder eine Unterstrich (underscore) beginnen.

Gültige Bezeichner sind z.B.:

Hans, saubled, unter13leichen, HierNichtParken_, sie_Idiot, SoASemf,
Private, Dödel, ___kompatibel, lllll
wobei der Dödel hier, wie bereits erwähnt, mit Vorsicht zu geniessen ist.

Ungültige Bezeichner sind z:B.:

.super, 15dosenbier,  was.auch.immer, Micro$oftl, was?, private,
11111

Wozu diese Quäereien dienen, wird vielleicht aus dem nächsten Kapitel klar.


9. Wie variabel sind Sie?

Programmieren heißt Daten zu verarbeiten. Diese Daten müssen direkt adressierbar gespeichert werden und dazu dienen Variablen:

Satz: „In der Mathematik stellt die Variable eine Unbekannte dar, in der Informatik hingegen stellt die Variable eine Bekannte dar. Eine Variable ist ein benannter Speicherort für Daten“

Deshalb hat eine Variable drei grundlegende Eigenschaften

Der Name soll in einer erkennbaren Relation zur Verwendung der Variablen stehen, da hiermit eine gute Selbstdokumentation erreicht wird.

Der Typ gibt einerseits an, wie viel Speicher für die Variable alloziert werden muss, andererseits, wie der Inhalt dieses Speichers interpretiert werden muss.

Der Wert schließlich ist naheliegenderweise das, was in der Variable gespeichert wird, üblicherweise Text oder Zahlenwerte.

C# kennt nur sehr wenige generische Typen:

Angesichts des string-Typs werden C-Kenner ein erleichtertes Lächeln nicht unterdrücken können. Das mit Abstand beliebteste Einfallstor für blackhats ist dadurch drastisch geschrumpft.

Die Typen float und double stellen gewöhnliche IEEE Fließkommazahlen mit 23+1/8 und 52+1/11 bit Mantisse/Exponent.

Der etwas seltsame Typ decimal wird in 128 bit abgebildet, wobei 96+1 bit für die Mantisse und 5 bit für einen dezimalen Exponenten verwendet werden. Damit entsteht eine Zahl mit 28 Stellen Genauigkeit und einem in diesem Bereich frei verschiebbaren Komma. Siehe dazu auch diesen Artikel.

Variablen müssen auch in C# deklariert werden, sodass der Compiler entscheiden kann,

Die Variablendeklaration erfolgt in C# durch das Nennen des Typs, gefolgt von einem oder mehreren, durch Kommata getrennten, Variablennamen und Initialisierungen.Beispiel

int nTest=0, nJohn=7, nAllan=55;
short sDick;  // geht in C# net, wenn sDick verwendet wird!

Im Gegensatz zu C führt das Fehlen der Initialisierung zu einem Compilerfehler (*erleichtertaufseufz*). Wichtig hierbei ist, zu beachten, dass auch C# als high performance system keinerlei implizite Initialisierungen vornimmt. Es folgt somit folgender wichtige

Satz: „Die Deklaration einer Variable in C erfordert auch eine Initialisierung des Wertes.“


10. Ich sehe was, was Du nicht siehst

Variablen sind stets innerhalb des Codeabschnitts sichtbar (und damit verwendbar), in dem sie deklariert wurden. Man unterscheidet in klassischen prozeduralen Programmiersprachen (wie BASIC, Pascal oder C) grob zwischen globalen und lokalen Variablen. Für die exzessive Verwendung globaler Variablen wurde man früher erschossen. In C# und Artverwandten (Java & Co) hingegen wird man nicht erschossen, es ist schlicht nicht mehr möglich, einfache Variablen global zu deklarieren,

Globale Variablen werden ausserhalb von Funktionen im Kopf eines Quelltextmoduls deklariert und sind für alle Funktionen innerhalb dieses Moduls sichtbar. Sie werden üblicherweise im BSS (Block Storage Segment) des Systems angelegt. Die Lebensdauer globaler Variablen erstreckt sich üblicherweise auf die gesamte Programmlaufzeit. In stärker objektorientierten Sprachen wie Smalltalk, Java oder C# erfüllen statische Klassen die Aufgeben der nicht mehr existenten globalen Variablen.

Lokale Variablen werden innerhalb von Namespaces, Klassen, Funktionen oder Anweisungsblöcken deklariert und sind lediglich innerhalb dieses Kontexts sichtbar. Sie werden üblicherweise am Stack des Systems angelegt. Die Lebensdauer lokaler Variablen erstreckt sich üblicherweise nur auf die Laufzeit der Funktion bzw. des Blocks. Guter Programmierstil zeichnet sich dadurch aus, Variablen stets so lokal wie möglich zu deklarieren.

Satz: „In C# ist die Verwendung globaler Variablen so gut wie unmöglich. Variablen sollen stets so lokal wie möglich deklariert werden,“


11. Ausgeben!

Damit das langsam entstehende Programm überhaupt mit dem Benutzer interagieren und ihn über den Inhalt von Variablen informieren kann, benötigt es eine Bildschirmausgabe. Diese kann in Art unserer Vorfahren als einfache Textausgabe am Bildschirm erfolgen. Dazu bemüht C# die Console-Klasse und deren Write Methoden.

Die Console.Write-Methode nimmt als erstes Argument einen string, der die Formatierung beinhaltet sowie eine Variable für jeden im Formatstring enthaltenen numerierten Platzhalter. Beispiel:

int a = 7;
Console.WriteLine("Die Variable \"a\" hat den Wert {0}.", a);
Console.WriteLine("Die Variablen: b = {1},  a = {0}.", a, b);

Das Beispiel zeigt auch anschaulich, dass in C# die Platzhalter in beliebiger Reighenfolge vergeben dürfen und, beginnend mit {0}, die nachfolgend übergebenen Werte adressieren.

Die Formatierung der Werte in den Platzhaltern erfolgt, ähnlich wie in C und doch ganz anders (Java-ähnlich) durch Hinzufügen einer Buchstaben-Zahlen Kombination zur Platzhalter-Nummer. Um beispielsweise den Zahlenwert des Platzhalters {0} mit 8 Stellen rechtsbündig zu formatieren, schreibt wird der Platzhalter {0:D8} verwendet. Alternativ kann auch eine MS-kompatible Schreibweise mit "#" verwendet werden z.B. {0:###0.####}

Folgende Liste der zeigt die möglichen Platzhalter und Escape-Sequenzen:

	C,c	Currency (GELD!, abhängig von der aktuellen Locale)
	D,d	Dezimalzahl (nur für ganzzahlige Typen)
	E,e	Exponentialschreibweise für Fließ-/Fixkommazahlen
	F,f	Fixkommadarstellung mit definierten Nachkommastellen
	N,n	Darstellung mit Tausenderpunkten und Nachkommastellen
	X,x	Hexadezimale Darstellung
	#.#	Literale Formatierungsangabe
	0.0#	erzwingt Nullen
	#,%	Prozentdatstellung (*100, wow!)

        \n      newline
        \r      carriage return
        \t      tabulator
        \b      backspace
        \a      bell
        \f      form feed
        \\      \ (literaler Backslash)
	\"	" (literales Anführungszeichen)
	{{	{ (literale curly brace)
	}}	} (literale curly brace)

Genaueres dazu bietet stets die Online-Hilfe oder (beispielsweise) unter Mono-Doc zum Thema System.IFormattable.


12. Und jetzt wird abgerechnet (und zugewiesen)

Angeblich stellt die herausragendste Fähigkeit der Computer ja das Rechnen dar. Deshalb soll nun erläutert werden, wie Berechnungen in C# notiert werden. Zum Rechnen werden Ausdrücke mit Operatoren verknüpft. Dies ist eine Liste der zulässigen Operatoren:

Operator Erklärung
+, -,*, / Arithmetische Operatoren der Grundrechenarten
% Modulo-Operator (ermittelt den ganzzahligen Rest einer ganzzahligen Division)
+ Stringverkettung
++, -- Inkrement- / Dekrement-Operator
= Zuweisungsoperator
+=, -=, *=, /=, %= Verkürzende Schreibweise für Ausdrücke. (z. B.: a += b entspricht a = a+b)
<, <=, >, >=,= =, !=, is Vergleichsoperatoren, evaluieren nach bool
&& logischer UND-Operator
|| logischer ODER-Operator
! logischer NICHT-Operator (Negation)
&, |, ^ bitweiser UND, ODER, EXOR-Operator
~ bitweise Negation
&variable Referenzierungsoperator: evaluiert die Adresse der Variable (Ja, auch in C#, unsafe)
*pointer Dereferenzierungsoperator:
deklariert einen Variable vom Zeigertyp (Pointervariable)
liefert den Wert eines Zeigers, der per Referenz (Adresse) übergeben wurde
(Sogar sowas gibts in C#!, unsafe)
(<Typ>) <Ausdruck> Cast-Operator:
erzwingt die Umwandlung des Ausdrucks in den durch <Typ> angegebenen Datentyp (Gefährlich)
sizeof(<Typ>) Operator sizeof: liefert die Göße des Typs in Bytes

Ach ja, die Liste ist nicht ganz vollständig, mit der offiziellen Einführung der Objektorientierung (im 2 Semester) kommen noch einige wenige Operatoren dazu. Die offensichtlichen Operatoren sind hierbei die vier binären Operatoren der Grundrechenarten. Es ist zu bemerken, dass C# außer diesen und dem Modulooperator keine weiteren Rechenarten direkt unterstützt. Kompliziertere Funktionen werden in Bibliotheken ausgelagert.

Die Verknüpfung zweier Ausdrücke mit einem binären Operator ergibt einen Ausdruck, der einem lvalue zugewiesen werden kann. Der hinlänglich bekannte Pythagoras würde sagen:

 a*a + b*b = c*c;
 

Daran kann man erkennen, dass Pythagoras nicht C# programmieren konnte. Die Ausdrücke „a*a“ und „b*b“ evaluieren wohl zu einem Ausdruck, der dem jeweiligen Wert der Variablen „a“ und „b“ zum Quadrat entspricht, auch die Summe dieser beiden Teiausdrücke ergibt wieder einen gültigen Ausdruck. Dieser kann allerdings nicht einem weiteren Ausdruck „c*c“ zugewiesen werden. Als Ziel einer Zuweisung ist lediglich eine Variable zulässig.

Außerdem erfolgen Zuweisungen in C# in der mathematisch üblichen Richtung von rechts nach links (so wie der Koran gelesen wird). Der Satz müsste deshalb lauten:

c_quadrat = a*a + b*b;

Womit uns Pythagoras allerdings (wie die meisten Mathematiker) auf dem entscheidenden Problem sitzen lässt, wie den nun aus der Variablen „c_quadrat“noch eine Quadratwurzel gezogen werden kann. Wie so oft tut sich auch hier mit der Lösung eines kleinen Problems ein größeres auf.


13. Rudelzuweisung mit Ausdruck

Im eben gezeigten Beispiel tritt eine Zuweisung auf. Diese Zuweisung ist einerseits eine gewöhnliche Anweisung, andererseits aber auch ein Ausdruck,der zum zugewiesenen Wert evaluiert.Beispiel:

nVariable = 777 – 111 ;

ist eine Zuweisung, die der Variablen nVariable den Wert 666 zuweist. Außerdem ist dies aber auch ein Ausdruck, der zu diesem Wert evaluiert. In klassisch mathematischer Lesart kann deshalb auch eine Konstruktion wie

nHans = nFrans = nZepp = 666;

verwendet werden. Zuerst wird nZepp auf 666 gesetzt. Der Ausdruckswert dieser Zuweisung (also 666) wird nFrans zugewiesen und der Ausdruckswert dieser Zuweisung (also wieder 0) schließlich nHans zugewiesen.


14. Ausdrucksvolle Zuweisung mit Vergleich

Die Ausdruckseigenschaft der Zuweisung eröffnet eine völlig neue Möglichkeit zur Erzeugung unverständlicher und fehleranfälliger Programme. Genau deshalb wird diese Eigenschaft auch in C# weidlich genutzt, wie seinerzeit bei den Erfindern von C, Kernighan & Ritchie exerziert:

while ((c = getch()) != 27)

Keine Sorge, das geht in C# genau so:

while ((nChar = Console.Read()) != 'q');

Hier wird erst der Rükgabewert der Console.Read()-Methode der Variablen nChar zugewiesen, anschließend der Ausdruckswert mit einer Literalkonstanten 'q' verglichen und als Laufbedingung für eine while() Schleife verwendet.

Zum Nachdenken: Was in C und C++ noch funktioniert hat:

while (c = getch() != 27)

klappt in C# dankenswerter Weise nicht mehr. Warum?

while (nChar = Console.Read() != 'q');

Dabei ist die Priorität der Operatoren zu beachten (nach dem Motto „Punkt vor Strich“). In C# gilt folgende Liste (1 ist die höchste Priorität). Auch diese Liste ist nicht ganz vollständig, mit der offiziellen Einführung der Objektorientierung (im 2 Semester) kommen noch einige wenige Operatoren dazu.

Priorität Operatoren
1 func() array[]
class.memb
x++ x--
! ~ new typeof
2 - unäres Minus
+ unäres Plus
++x --x
(<Typ>) Typecast
* Dereferenzierung, unsafe
& Addressoperator, unsafe
sizeof
3 * (Multiplikation) / %
4 + -
5 << >>
6 < <= > >=
7 == !=
8 &
9 ^
10 |
11 &&
12 ||
13 ? :
14 = += -= etc.

Diese Tabelle dient zur Veranschaulichung, was man als Programmierer alles nicht wissen muss. In der Praxis ist es weitaus geschickter, sich nicht auf den Vorrang der Operatoren zu verlassen, sondern sämtliche nichttrivialen Ausdrücke vollständig zu klammern.

Gefährlich war das ganze in C und C++, weil aus einem Vergleich auf Gleichheit (a == 123) durch ein kleines Missgeschick leicht eine Zuweisung (a = 123) werden kann. Aus diesem Grund bevorzugten erfahrene C-Programmierer, die äquivalente Vergleichsnotation (123 = = a), da in diesem Fall die Zuweisung (123 = a) nicht gültig war. In C# sorgt die viel striktere Typprüfung in fast allen denkbaren Fällen für einen Compilerfehler, da der Ausdruckswert einer Zuweisung normalerweise nicht implizit in einen bool gecastet werden kann.

Satz: „In C / C++ wurde aus einem verunglückten Vergleich (= =) sehr leicht eine Zuweisung (=) mit sehr unangenehmen Effekten. Hier ist durch die striktere Typprüfung in C# eine große Erleichterung entstanden.“

Eingedenk des Beispiels weiter oben [while nChar = Console.Read()...] gilt aber auch für das sicherere C#

Satz:„Die Verwendung von Zuweisung und Vergleich in einem Ausdruck ist gefährlich und sollte nur von erfahrenen Programmierern (und mit vollständiger Klammerung) erfolgen.“


15. Wie spielen Boole

Eine besonders wichtige Stellung nehmen die booleschen Operatoren und die booleschen Ausdrücke ein, weil sie unabdingbar für Bedingungen und Iterationen sind. Die Vergleichsoperatorenliefern einen Ausdruck des Typs boolund können über entsprechende Operatoren weiter verknüpft werdenBeispiel:

(nAlter > 18 && nAlter < 85) || nKontoStand > 100000

könnte dazu dienen, den Zugang zu adult content derart zu beschränken, dass keine rechtlichen Konsequenzen für den Anbieter zu befürchten sind.

Achtung: Die booleschen Operatoren (&&, ||, !, anwendbar nur auf boole) müssen unbedingt von den bitweisen logischen Operatoren (&, |, ~, anwendbar auf ganzzahlige Typen) unterschieden werden. Was in C / C++ noch zu schrägen Verwicklungen führte, erzeugt in C# zwar nur mehr Compilerfehler, diese lassen sich aber dennoch nur schwer erklären.


16. Eingeben!

Die komfortable Console.Write[Line]()-Funktion hat auch eine kleine Schwester, die (orthogonal dazu) der Eingabe von Werten dient und den nicht minder einprägsamen Namen Console.Read[Line]().

Im Gegensatz zu der C-library-Funktion scanf(), die ein Füllhorn an Eingabemöglichkeiten eröffnet, bietet die Console-Klasse nur zwei sehr rudimentäre Funktionen:

Die Methode Console.ReadLine() liest einen String ein und zwar wirklich so lange, bis ein <cr> im Eingabestream auftaucht. Im Gegensatz zu scanf() lässt sich Console.ReadLine() von Whitespace nicht beeindrucken. Den Preis für so viel Komfort bezahlt man mit dem Fehlen einer direkten Umsetzung in Zahlenwerte. Weil dies allerdings bei scanf() auch nur meistens wie gewünscht funktionierte, ist der Verlust nicht sehr schmerzhaft. Ausserdem liefert Console.ReadLine() direkt eine Stringreferenz, sodass das gefährliche Hantieren mit Adressoperatoren von scanf() dern Vergangenheit angehört.

Die Methode Console.Read() liest ein einzelnes Zeichen von der Konsole ein und liefert dieses in Form eines int zurück. Die Console.Read()-Methode wartet ggf auf das Drücken einer Taste, wenn der Eingabepuffer leer ist. Auf einigen Konsolen (z.B. XTerm/Bash) wird die Eingabe vom System zeilenweise zwischengepuffert, sodass auch ein einzelner Tastendruck zuerst mit <cr> abgeschlossen werden muss.

Sollen also Zahlenwerte eingegeben werden, so ist eine zweischrittige Vorgehensweise erforderlich. Beispiel:

Console.Write("Wie alt bist denn Du? ");
string strAge = Console.ReadLine();
int nAge      = Int32.Parse(strAge);

Was dabei so alles schief gehen kann, werden die Übungen und das Kapitel zum Exception-Handling zeigen.


17. Entscheiden, jetzt!

Der Ablauf eines Programmes findet stets „von oben nach unten“, also in der programmierten Reihenfolge der einzelnen Abweisungen statt. Das ist auf Dauer sehr langweilig und ineffizient.

Eine spannende Möglichkeit, in den Programmablauf einzugreifen, stellen Kontrollstrukturen dar. Die einfachste Kontrollstruktur ist die if-Bedingung.

Wichtig: „Die Verwendung des Begriffs „if-Schleife“ wird mit schwerer Verspottung nicht unter drei Monaten bestraft.“

Die Syntax der if-Bedingung sieht so aus:

if(<boolescher Ausdruck>)
        Anweisung
[else
        Anweisung]

Die Anweisung, die nach if() folgt, wird nur ausgeführt, wenn der Ausdruck in der Klammer zum Wert true evaluiert. Die Anweisung nach dem Optionalen else wird alternativ dazu nur ausgeführt, wenn der Ausdruck in der Klammer zum Wert falseevaluiert.

Mehrere Anweisungen können jederzeit durch eine beliebige Menge öffnender { und schließender } geschwungener Klammern zu einer Anweisung gruppiert werden, womit sich der Entscheidungsbereich der if-Bedingung auf beliebig viele Anweisungen erweitert.


18. Eine coole Entscheidung

Die Sprachfamilie C kennt auch eine sehr elitäre (1337) Formulierung der Bedingung auf Ausdrucksbasis. Dies ist der conditional operator „?“ Die Syntax des conditional operators sieht so aus:

<boolescher Ausdruck> ?  <ausdruck bei true> : <ausdruck bei
false>

Diese Konstruktion wird vorwiegend eingesetzt, um boolesche Entscheidungen in anderswertige Ausdrücke zu transformieren: Der Gesamtausdruck evaluiert - abhängig vom Wert des Ausdrucks vor dem Fragezeichen zu entweder dem Wert vor (true) oder hinter (false) dem Doppelpunkt. Beispiel:

return NULL != pTest ? OK : ERR_NOMEM; 
oder
Console.WriteLine( "Noch {0} {1}", nZeit, nZeit != 1 ? "Stunden" :  "Stunde");

19. Entweder – oder - oder ganz anders

Gelegentlich ist eine binäre Entscheidung nicht sophisticatedgenug. Dann muss eine mehrfache Fallunterscheidung her. In C kann so etwas mit der switch()-Konstruktion gelöst werden.

Die Syntax der switch()-Konstruktion sieht so aus:

switch(<Ausdruck>)
        {
        case <Konstanter Ausdruck>:
                [Anweisung]
        [case <Konstanter Ausdruck>:
                [Anweisung]]
        [default:
                [Anweisung]]
        }

Mit dem Eintritt in den switch()-Kopf wird der Ausdruck in den Klammern evaluiert. Anschliessend wird in der Liste der case-Konstanten nach diesem Wert gesucht. Wird er gefunden, so wird die Programmausführung dort fortgesetzt, wird er nicht gefunden, so setzt sich das Programm entweder an der Stelle default fort, wenn diese optionale Marke nicht existiert, nach der schließenden geschwungenen Klammer.

Die C-Familie unterscheidet sich bezüglich des switch() historisch begründet deutlich von den komfortableren Sprachen der Pascal- oder BASIC-Familie. Zweieinhalb Dinge sind wichtig:


20. Auf ein Neues!

Wiederholung prägt bekanntlich ein. Außerdem ist mit einem streng linearen Programmablauf auf Dauer kein Blumentopf zu gewinnen. Die Iteration bzw. Schleife ist eine der wichtigsten Kontrollstrukturen jeder Programmiersprache.

Wiederholung prägt bekanntlich ein. Außerdem ist mit einem streng linearen Programmablauf auf Dauer kein Blumentopf zu gewinnen. Die Iteration bzw. Schleife ist eine der wichtigsten Kontrollstrukturen jeder Programmiersprache.

Eben :-)

Die einfachste Form der Schleife ist die Forever-Schleife, eine Endlosschleife: Gemäß der Notation:

for(;;)
  Anweisung  //Schleifenrumpf

Wird die folgende Anweisung unendlich (naja...) oft wiederholt. Auch hier können natürlich mehrere Anweisungen über geschwungene Klammern gruppiert werden. Die Schleife wird verlassen, wenn das Schlüsselwort break im Ablaufpfad auftaucht. Beispiel:

int nSec = 5;
for(;;)
  {
  Console.WriteLine("Noch {0} Sekunden bis zum Start", nSec);
  nSec -= 1;
  if (0 == nSec) break;
  }

Diese Schleife endet mit der Sekunde Null durch einen break.

Satz: „Hinter der Klammer einer for()-Schleife sollte nie ein Semikolon stehen“

Weil Endlosschleifen an sich sehr wenig Sex-Appeal haben, bietet die for()-Schleife wesentlich leistungsfähigere Möglichkeiten. Die Beiden Semikolons innerhalb der runden Klammern teilen deren Inhalt in drei Bereiche.

  for(<Initialisierung>;<Laufbedingung>;<Iteration>)

Der Inhalt des Feldes Initialisierung wird einmalig nur beim ersten Eintritt in die Schleife ausgewertet. Hier werden meist Zählervariablen initialisiert (ab C++ auch deklariert). Mehrere Ausdrücke dürfen durch Kommata getrennt werden.

Der Inhalt des Feldes Laufbedingung wird vor jeder Eintritt in den Schleifenrumpf zu einem booleschen Ausdruck evaluiert. Nur wenn dieser Ausdruck true ist, wird der Rumpf ausgeführt. Im gegenteiligen Fall setzt die Programmausführung hinter dem Schleifenrumpf fort.

Der Inhalt des Feldes Iteration wird immer am Ende des Schleifenrumpfes evaluiert. Hier steht üblicherweise ein Seiteneffekt, der die Zählervariablen aktualisiert.

Ein Beispiel:

int e,p;

for (e = 0, p = 1 ; e < 16 ; e++, p*=2)
  {
  Console.WriteLine("2 hoch {0} = {1}", e, p);
  }

Jedes der drei Felder kann auch leer sein (und wird dementsprechend nicht ausgewertet), im Falle einer leeren Laufbedingung entsteht eine Forever-Schleife.

Die for()-Schleife arbeitet abweisend, was bedeutet, dass der Schleifenrumpf, wenn die Laufbedingung bereits beim Eintritt in die Schleife false liefert, überhaupt nicht ausgeführt wird.


21. Langewhile

Eine weitere Form der Schleife stellt die while()-Schleife dar, die in zwei Geschmacksrichtungen auf den Markt kommt. Die abweisende while()-Schleife hat - analog zur for()-Schleife - nur ein Laufkriterium. Sämtliche Initialisierungen und Iterationen müssen hier ggf. gesondert erledigt werden. Diese Schleife kommt meist zum Einsatz, wenn Iteration und Initialisierung keiner strengen Systematik folgen (z.B. beim Verarbeiten von Benutzereingaben). Die while()-Schleife stellt sich syntaktisch so dar:

while(<Laufbedingung>)
  Anweisung  //Schleifenrumpf

Die zweite Geschmacksrichtung der while()-Schleife ist die nicht abweisende do-while();-Schleife. Sie sieht so aus:

do
  Anweisung  //Schleifenrumpf
while(<Laufbedingung>);

Zwei Dinge sind hier sehr wichtig.


22. Style hat man, oder man codet ihn

Da der C#-Compiler keinerlei Ansprüche an die ästhetische Qualität des Sourcecodes stellt, ist es eminent wichtig, durch Selbstdisziplin für eine durchgängige Versteh- und Lesbarkeit der Sourcen zu sorgen. Unter dem Begriff Coding style fasst man üblicherweise drei Themenbereiche zusammen:

Ein guter eigener Coding style zeichnet einen guten Programmierer aus, in den meisten Fällen wird der style aber vom Auftraggeber (Arbeitgeber) vorgeschrieben.


23. Mit Style oder ohne - Programaufbau

Ein C#-Quelltext kann so unleserlich aussehen, wie er will, er muss dennoch einigen formalen Kriterien genügen. Für die Applikationsentwicklung muss ein Programm folgende Kriterien erfüllen:

// Zu aller erst werden Praeprozessor-Makros definiert.
//Geht spaeter nicht mehr.
#define DEBUG_REALLY_FIES
// Am Anfang werden alle sinnvollen Namespaces importiert
// (auch eigene)
// Spaeter im Programm darf diese Direktive nicht mehr verwendet werden
using System;
using OwnNameSpace;

/*
Es gibt in C# keine modulglobalen Variablen.

Es muss mindestens eine Klasse geben und diese muss mindestens die
statische Methode Main() implementieren.
*/
class MainClass
{

// Membervariablen werden in der Klassendeklaration
// definiert und sind für alle Klassenmitglieder sichtbar.
static int m_nSwitchVal = 0x20; 

public static void Main(string[] args)
	{
// die geschwungenen Klammern sind in 
// Funktionen verpflichtend.

	int nTest = 0;
// Die Deklaration lokaler Variablen muss innerhalb 
// der geschwungenen Klammern (Funktionsrumpf)  stehen.

// jetzt beginnen die Anweisungen

  	while ((nTest = Console.Read()) != 27)
        	{
		Console.Write ("{0:X4}, ", someFunc(nTest));
        	}
  	return 0;
	} // Ende von Main()


// Hier folgen weitere Methoden der Klasse
int someFunc(int nWhatEver)
	{
	return nWhatEver & ~m_nSwitchVal;
	}
} // Ende von MainClass

Vieles in der gezeigten Anordnung ist nicht Vorschrift, spricht abr für guten Coding Style. Zwingend sind:


24. In Reih' und Glied ins Verderben marschier'n

Gewöhnliche skalare Variablen sind der ideale Datenspeicher für einzelne Informationen, versagen aber bei der Verarbeitung tabellarischer Daten, wie sie in Wissenschaft und Wirtschaft oft auftreten. Für derartige Daten wurde der Arraytyp geschaffen.

Die Arraydeklaration kann auf jeden Datentyp angewendet werden und ermöglicht den indizierten Zugriff auf ein benanntes Feld gleich typisierter Daten. Die Arrayeigenschaft wird in der Deklaration durch das Anhängen eckiger Klammern an den Variablentyp ausgedrückt. Innerhalb der eckigen Klammern steht ggf. die konstante Größe des Arrays.

Achtung: Die Deklaration von Arrays in C# unterscheidet sich bei managed code wesentlich von den in C üblichen Varianten. Schon die Deklaration erfolgt nach einer anderen Syntax. In C# unterscheidet man die Deklaration:

Statische Membervariablen gehören zu einer Klasse, sind also quasi fixer Bestandteil des Programms und erfüllen die Funktion der früheren globalen Variablen. Dynamische Variablen werden zur Laufzeit der Funktion am Stack angelegt und anschliessend wieder vernichtet. Die Deklaration unterscheidet sich, wie folgendes Beispiel zeigt:

class ArrayDemoClass
{
// Deklaration eines statischen members mit Initialisierung
static int[] anMonatsUmsatz = {111,122,100,122,123,200,175,95,80,100,121,152};

public static void Main(string[] args)
	{
	// Deklaration eines dynamischen Arrays mit dem new-Operator
	short[] anTagesUmsatz = new short[365];

	anMonatsUmsatz[0] = 100; 
	anMonatsUmsatz[1] = 125;
	anMonatsUmsatz[12] = 666; // PENG! siehe unten.
	}
}

Es zeigt die Deklaration zweier Arrays vom Typ int[] bzw. short[]. Das Array anMonatsUmsatz enthält 12 Einträge, die über Indexausdrücke von 0 bis 11 adressiert werden können. Das Array vom Typ short[] enthält 365 Einträge mit Indizes von 0 bis 364. Ihren großen Vorteil entfalten die Arrays mit variablen Indexausdrücken in einer Schleife. Um beim Beispiel zu bleiben:

for (int i=0, int nSumme=0 ; i<12 ; i++)
        nSumme += anMonatsUmsatz[i];

Summiert in zwei Zeilen sämtliche Monatsumsätze. So weit zu den Vorteilen. Nun zu den Macken:

Satz: „Der gültige Indexbereich eines Arrays der Größe n geht von 0 bis n-1.

Im Gegensatz zu den Vorgängersprachen C und C++ unterliegt der Arrayindex in C# einer Gültigkeitsprüfung. Arrayzugriffe mit ungültigen Indizes adressierten in C unkontrolliert umliegende Speicherbereiche und konnten zur Verfälschung von Daten und Programmcode, im Extremfall sogar zur Ausführung fremden Codes führen (Stichwort buffer overflow). In C# führen derartige Indexverletzungen nur mehr zu einer Exception und somit ungünstigstenfalls zu einem Programmabbruch.


25. Wir iterier'n im Container

Zu den unlängst besprochenen Schleifen gesellt sich mit Einführung des Array-Containers noch ein Derivat der for()-Schleife, das allerdings nur in C# existiert. Vorerst soll die Syntax der foreach()-Schleife an folgendem Beispiel vorgestellt werden:

int[] anZahlen = {1,2,3,4,5,6,7};

foreach (int nZahl in anZahlen)
	Console.Write("{0}, ",nZahl);

Dieser Code gibt alle Elemente des Arrays auf der Konsole aus, indem nZahl mit jedem Durchlauf einen weiteren Wert des Arrays annimmt. Die Syntax von foreach() sieht so aus:

foreach ( <Elementtyp> <Iterator> in <Container>)

Hierbei ist <Iterator> eine Variable des Elementtyps des Arrays (oder allgemeiner. Containers) und <Container> eine Arrayvariable (oder allgemeiner: Eine Instanz der Containerklasse). Die Deklaration des Iterators in der runden Klammer ist verpflichtend. Weitere Containerklassen werden noch folgen.


26. Kleine Häppchen erleichtern die Verdauung

Softwareprojekte mit mehreren Millionen Codezeilen lassen sich nur recht schwer in ein einziges main() quetschen. Deshalb unterstützt C, wie praktisch jede andere Programmiersprache auch, die Modularisierung von Programmen. Diese umfasst zwei wesentliche Aspekte:

In den nächsten Kapiteln werden wir uns vorerst nur mit statischen Klassen und ebensolchen Funktionen beschäftigen. Eine derartige Funktion haben wir bereits kennen gelernt, nämlich die Hauptfunktion jeder C#-Applikation mit dem Namen Main(). Innerhalb einer Klasse kann eine beliebige Menge weiterer Funktionen implementiert werden. Und das geht so:

In C/C++ konnten Funktionen vor ihrer Implementierung als Prototyp deklariert werden. Dies konnte in verantwortungsvollen Händen gut zur Trennung von Interface und Implementierung von Bibliotheksmodulen verwendet werden. Auch die Übersichtlichkeit bzw. Lesbarkeit von Klassendeklarationen in C++ war damit deutlich besser.

Im Gegensatz zu C/C++ können Funktionen in C# nicht deklariert und später im Code implementiert werden. C# fordert die Implementierung einer Funktion stets innerhalb der Klassendeklaration

Ob mit Prototyp oder ohne, eine Funktion hat drei charakteristische EIgenschaften

Eine Funktion muss einen Namen und genau einen Rückgabewert haben. Der return value darf aber auch void, (also nix) sein, wenn's nur brav so hingeschrieben wird (Basicer und Pascalisten sprechen dann von einer Prozedur). Weiterhin ist eine beliebige Menge an Parametern (auch null) zulässig. Im Gegensatz zu C/C++ darf eine leere Parameterliste allerdings nicht als (void) deklariert werden sondern wird durch leere Klammern () dargestellt. Beispiel:

class VektorZeug
{
[...]
// "Funktion"
static float VektorBetrag (float ftX, float ftY)
	{
	// rechnen tun ma a
	return Math.Sqrt (ftX * ftX + ftY*ftY);
	}
// "Prozedur"
static void Beschimpfung()
	{
	Console.WriteLine("Pimpf, bleder!");
	// kein return noetig. void is eh nix.
	}
}

Dies implementiert (wenn auch nicht sehr raffiniert) eine Funktion, die zwei Argumente vom Typ float nimmt und einen float Wert zurückliefert. Die Funktion evaluiert bei ihrem Aufruf zum Ausdruck ihres Rückgabewertes. Beispiel:

float ftBetrag;

ftBetrag = VektorBetrag(4, 5); // oder
ftBetrag = VektorZeug.VektorBetrag(4, 5); 
ftBetrag = Beschimpfung() // Geht natuerlich nicht!

Die Variable ftBetrag hat anschliessend ungefähr den Wert 6.4. Diese Funktion ist prinzipiell innerhalb der gesamten Klasse nutzbar, im Falle einer public Qualifikation sogar von anderen Klassen aus [wie z.B. Console.WriteLine()]. Damit lässt sich Code dann sehr einfach auch ohne Verwendung des Clipboards wiederverwerden.

Die Parameter der Funktion werden, sofern es sich um einfache Typen handelt (also alle, die wir bisher kennen), als Wert übergeben. Dies bedeutet, dass wenn eine Variable in der Parameterliste des Aufrufs steht, eine Kopie des Wertes an die Funktion übergeben wird. Verhunzungen der Parameterwerte wirken so nicht auf die Aufruparameter zurück. Beispiel

float ftX=1.0, ftY=2.0;
// ftX ist 1.0 und ftY ist 2.0
VektorToeten(ftX, ftY);
// ftX ist 1.0 und ftY ist 2.0 (immer noch)

static void VektorToeten(float ftXwert, float ftYwert)
	{
	ftXwert = ftYwert = 0.0;
	}
Das ist übersichtlich und einfach zu merken. Deshalb darf das natürlich nicht so bleiben.


27. Da kommt mehr raus als drin ist: Referenzparameter

Funktionen bieten die Möglichkeit, beliebig viele Argumente zu deklarieren und somit praktisch beliebig viele Parameter zu übergeben. Die Rückgabe eines Ergebnisses ist allerdings in den meisten bekannten Programmerisprachen auf einen einzigen Wert beschränkt. Obwohl das sehr oft zu wenig ist, wird dies in dem meisten Fällen nicht direkt an der Wurzel behoben.

Um ein sexy Beisiel zu zeigen, seil die Vektoralgebra bemüht. Gegeben sei eine Funktion (Wer gibt eigentlich diese Funktionen und wem?), die einen in Komponenten zerlegten 2D-Vektor um einen Winkel phi dreht.

// Hallo Clipboarder, bitte unbedingt den Begleittext lesen! ;-)
double,double Vektor2DDrehen(double dftX, double dftY, double dftPhi)
	{
	double dftNeuX = dftX * Math.Cos(dftPhi) + dftY * Math.Sin(dftPhi);
	double dftNeuY = dftY * Math.Cos(dftPhi) - dftX * Math.Sin(dftPhi);
	
	return dftNeuX, dftNeuY;
	}

Diese Implementeirung ist stringent, elegant und klar verständlich. Sie hat nur einen kleinen Nachteil: Es ist kein gültiger C#-Code!. In Python würde etwas derartiges funktionieren. In C# wird stattdessen die Krücke an die Arschbacke geschraubt - es wird eine neue Methode der Parameterübergabe eingeführt - die Referenzübergabe.

In C# wird eine Referenzübergabe durch voranstellen des Schlüsselwortes ref vereinbart. Die magischen Zeichen "*" (C) und "&" (C++) haben ausgedient. Bei einer Referenzübergabe wird an dei Funktion nicht eine Wertkopie sondern stattdessen eine Adressangabe der übergebenen Variablen übergeben. Weil die Funktion dementsprechend "weiss", wo die Variable zu finden ist, kann sie nun auch deren Wert verändern. Dazu muss nur die Deklaration der Funktion erweitert werden:

void Vektor2DDrehen(ref double dftX, ref double dftY, double dftPhi)

Nur die Vektorkomponenten werden per Referenz übergeben, der Winkel soll ja ohnehin nicht verändert werden. Im Unterschied zu C++, wo auch schon eine funktionierende typsichere Referenzübergabe möglich war, unterscheidet sich in C# dann sogar der Aufruf der Funktion.

// Aufruf fuer Wertuebergabe
Vektor2DDrehen(dftRaumschiffX, dftRaumschiffY, dftSchussPhi);
// Aufruf fuer Referenzuebergabe
Vektor2DDrehen(ref dftRaumschiffX, ref dftRaumschiffY, dftSchussPhi);

Diese Konvention ist zwar eine auf den ersten Blick lästige Abweichung von gewohnten Standards, zeigt sich bei näherer Betrachtung allerdings (obwohl C# von Microsoft stammt) als durchdachtes Sicherheitsfeature. Durch das erzwungene "ref" Präfix wird der Verwender der Funktion gezwungen, die Referenzeigenschaft zur Kenntnis zu nehmen - und mithin damit zu rechnen, dass sich die Werte der Referenzparameter ändern. Das Beispiel aus dem vorigen Kapitel könnte dann so aussehen:

float ftX=1.0, ftY=2.0;
// ftX ist 1.0 und ftY ist 2.0
VektorToeten(ref ftX, ref ftY);
// ftX ist jetzt 0.0 und ftY auch BAZONG!

static void VektorToeten(ref float ftXwert, ref float ftYwert)
	{
	ftXwert = ftYwert = 0.0;
	}

Die Unterscheidung von Wert- und Referenzübergabe wird mit der Einführung von Klasseninstanzen (also Objekten) nochmals an Bedeutung gewinnen.

C# kennt noch einen Sonderfall der Referenzübergabe, der vereinbart werden kann, wenn es sich bei den Referenzparametern ausschliesslich um Rückgabewerte handelt. In diesem Fall kann statt des Präfixes "ref" das Präfix out verwendet werden. In diesem Fall stellt der Compiler sicher, dass das Argument in der Funktion nicht lesend verwendet wird und erlaubt im Gegenzug eine uninitialisierte Übergabe an die Funktion.


28. Zwischendurch: „Pimp my code.“

Die Lesbarkeit und Dokumentationsfähigkeit von Programmcode lässt sich mit kleinen Features manchmal drastisch verbessern. Dazu gehören insbesondere geschickt definierte Konstanten. In der Programmierung unterscheidet man generell drei Arten von Konstanten:

Literalkonstanten bedürfen bezüglich ihrer Verwendung keiner weiteren Erklärung, sie sind bereits im ersten Programm zum Einsatz gekommen (Stichwort: "Hello World!"). Der Einsatz vsolcher Literale sollte sich in guten Programmen auf triviale Werte beschränken. Allgemein sind die Werte 0 und 1 immer OK, in Spezialfällen vielleicht auch 7 Wochentage, 100% oder 12 Monate. Darüber hinaus sollen allerdings stets symbolische Konstanten zum Einsatz kommen.

Im Gegensatz zu C/C++ kennt C# keine Präprozessor-Konstanten, sodass jede symbolische Konstante auch einen Typ haben muss. Deshalb ähnelt die Deklaration der einer Variablen.

const int BOESE_ZAHL = 666;  // oder
const string BOESER_NAME = "Adi";
Der Qualifier const besagt im Grunde nur, dass der Wert dieser Variablen nach ihrer Initialisierung nicht mehr geändert werden darf und erklärt sie somit zur Konstanten.

Auch wenn Arrays in C# viel von ihrem Schrecken verloren haben und bedeutend leistungsfähigere Container zur Verfügung stehen, ist es immer noch guter Stil, die Grösse von Arrays (sofern konstant) zentral über eine Konstante zu dokumentieren (siehe Musterlösungen).

Eine andere Art von Konstanten finden ihre Anwendung in der Zuordnung aufzählbarer Eigenschaften zu numerischen Werten. Beispiele sind Ampelfarben (Rot, Gelb, Gruen) oder Familienstand (Single, InGemeinschaftLebend, Verheiratet, Geschieden) usw. In C# besteht (im Gegensatz zu C/C++) prinzipiell die Möglichkeit, diese Eigenschaften als Strings zu realisieren und komfortabel damit zu arbeiten. In C# können Strings mit den Standardoperatoren verglichen und sogar in switch()-Konstrukten als Selector verwendet werden.

Dennoch ist das keine gute Idee. Man stelle sich folgenden Code vor:

if (AmpelVorMir.Farbe == "Greun")
	FahreMitQuietschendenReifenLos();
Dies ist ein vollkommen korrektes C#-Programm, ich bezweifle aber, dass dieses Programm jemals den Platz von der Ampel verlassen wird. Der Tippfehler in der Konstanten wird natürlich so vom Compiler nicht erkannt.

Lösen kann dieses Problem der Enumerationstyp. Mit der Deklaration

enum AmpelFarben {Rot,Gelb,Gruen};
Wird ein neuer Typ namens AmpelFarben definiert, der nur die Werte Rot, Gelb und Gruen (ohne Anführungszeichen) annehmen kann. Von diesem Typ können dann Variablen deklariert werden, die genau so verwendet werden können wie Variablen eingebauter Typen (z.B. int). Intern werden Enums ohnehin auf Zahlenwerte abgebilset, doch dies geschieht vor dem Programmeirer versteckt (und ist nur durch einen Typecast zu erfahren). Der Bonus des enum-Typs besteht aus zwei Effekten:

Die kommenden Übungen werden reichlich Gelegenheit bieten, derartige Konstanten sinnvoll einzusetzen.


29. Ich helf' mir selbst – und das immer wieder: Rekursion

Im Zuge der Erläuterung von Funktionen wurde bereits angedeutet, dass innerhalb jeder Funktion jede andere Funktion aufgerufen werden darf. Diese Möglichkeit geht sogar so weit, dass innerhalb einer Funktion diese Funktion selbst aufgerufen werden darf. In einem solchen Fall spricht man von eimen rekursiven Aufruf. Ein ebenso beliebtes wie sinnloses Beispiel ist die Berechnung der Fakultät einer Zahl.

Die Fakultät der Zahl n berechnet sich als das Produkt aller natürlichen Zahlen von 1 bis einschliesslich n. Man kann Aufgabe mithilfe einer Schleife iterativ lösen. Sinngemäß sieht der Code der Funktion etwa so aus:

static int fakultaet(int n)
  {
  int i, fac;

  for( i=1, fac=1; i<=n; i++ )
        fac *= i;

  return fac;
  }

Um die Sache nicht so einfach erscheinen zu lassen, wie sie ist, haben Mathematiker auch einen rekursiven Ansatz entwickelt. Dieser definiert die Fakultät von n zu:

n! = n * (n-1)! Für n > 1 und 1! = 1

damit ergibt sich folgende rekursiv arbeitende Funktion, die immerhin den Vorteil hat, dass man bei der Implementierung weniger nachdenken muss:

// langsam zum Mitdenkane hingeschrieben:
static int fakultaet(int n)
  {
  int nResult;

  if (n > 1)
        nResult = n * fakultaet(n-1);
  else
        nResult = 1;

  return nResult;
  }

Das bestechende an dieser Funktion ist, dass sie ohne eine eigene Iteration auskommt. In vielen Fällen ist eine rekursive Implementierung schlanker und eleganter, manchmal sogar verständlicher als eine iterative, sie muss aber mindestens eine Bedingung unbedingt einhalten:

Satz: Der rekursive Selbstaufruf einer Funktion muss immer von einer eigenen Bedingung abhängen, damit sichergestellt ist, dass die Rekursion überhaupt terminiert

Im Fall unseres Beispiels wird die Fakultät von 9 so berechnet:

 fakultaet(9) =  9 * fakultaet(8)
  fakultaet(8) =  8 * fakultaet(7)
   fakultaet(7) =  7 * fakultaet(6)
    fakultaet(6) =  6 * fakultaet(5)
     fakultaet(5) =  5 * fakultaet(4)
      fakultaet(4) =  4 * fakultaet(3)
       fakultaet(3) =  3 * fakultaet(2)
        fakultaet(2) =  2 * fakultaet(1)
         fakultaet(1) =  1                // hier wird der else-Zweig gewählt

und jetzt wird der Aufrufstapel abgebaut

          1
         2*1 
        3*2
       4*6
      5*24
     6*120
    7*720
   8*5040
  9*40320
 362880

Und das ist das erwartete Ergebnis. Zum Zeitpunkt der Ermittlung von fakultaet(1) hat sich die Funktion 8 mal selbst aufgerufen und keiner dieser Aufrufe hat bis dahin ein Ergebnis gebracht. Es wurde jeweils nur ein Rechenschritt begonnen, der teilweise vom Ergebnis eines weiteren Aufrufs der selben Funktion mit anderen Parametern abhängt. Erst wenn mit fakultaet(1) das konstante Ergebnis 1 ermittelt und erstmals auch zurückgegeben wird, kann der Aufruf fakultaet(2) mit dem Ergebnis 2 zurückkehren und daraufhin fakultaet(3) mit dem Ergebnis 6 und so weiter.

Die rekursive Lösung ist elegant, weist aber einen erheblichen Overhead auf. Für jede Rekursionsebene muss ein eigener Funktionsaufruf durchgeführt werden, der nicht unerhebliche Mengen an Rechenzeit und Stackspeicher verbraucht.

Beim Demontieren und Traversieren von Listen und Bäumen wird uns die Rekursion dennoch als willkommenes Werkzeug zu Hilfe kommen.


30. Manchmal läuft nicht alles, wie es soll: Exceptions

Schon in den ersten Übungen zeigte sich, dass manche Funktionen nicht immer ein sinnvolles Ergebnis liefern können. Der Versuch, einen vom Benutzer eingegebenen Text in eine Zahl umzuwandeln muss scheitern, wenn dieser keine Zahl enthält.

In solchen Fällen spricht man von Laufzeitfehlern, weil sich derartige Fehlersituationen, di evon späteren Eingangszuständen abhängen, zum Zeitpunkt des Comlpilerlaufs nicht behandlen lassen. In C war die Behandlung solcher Laufzeitfehler recht einfach gestrickt. Entweder passierte einfach gar nichts (im Beispiel: es wurde keine Umwandlung durchgeführt) oder es wurden Speicherinhalte überschrieben, was früher oder später zum unkontrollierten Absturz des gesamten Programms führte.

C# geht hier, wie die meisten besseren Programmiersprachen weiter und bietet eine Möglichkeit mit Laufzeitfehlern strukturiert umzugehen, die so genannten Exceptions. Diese führen stets zu einem kontrollierten Programmabbruch, wenn sie vom aufrufenden Code nicht behandelt werden. In diesem Zusammenghang sind vier Schlüsselwörter, die einigermassen für sich sprechen, wichtig.

Viele Systemfunktionen (bekanntes Beispiel Int32.Parse()) können Exceptions erzeugen (z.B. System.FormatException, wenn keine Zahl im String steht). Wenn verhindert werden soll, dass diese das gesamte Programm beenden müssen sie abgefangen werden. Dazu wird ein Codeabschnitt als potenziell gefährdet gekennzeichnet, indam man ihn unter try klammert. Tritt in einem solchen Abschnitt eine Exception (direkt oder in einer aufgerufenen Funktion) auf, so endet die Ausführung des try-Blocks sofort.

Nun wird nach eimem dazu passenden catch-Block gesucht. Syntaktisch muss auf jedes try mindestens ein catch (oder finally, was aber wenig Sinn macht) folgen. In diesem Codeblock kann die Ausnahme dann sinnvoll behandelt werden. Hier liegt übrigens dann der bekannte Hase im Pfeffer:

Satz: Der Exception-Mechnismus stellt lediglich eine prinzipielle Methode zur Behandlung von Laufzeitfehlern zur Verfügung. Die Kunst und Schwierigkeit besteht weiter darin, eine Sinnvolle Reaktion auf die Fehler zu definieren und implementieren. In komplexeren Softwareprojekten (und besonders in der Maschinen- und Anlagentechnik) muss oft bis zu 80% der Arbeit in die Fehlerbehandlung investiert werden.

Das einfachste Beispiel zum Exception-Handling könnte so aussehen:

int nZahl;
bool fSuccess = false;

do
	{
	try
		{
		nZahl = Int32.Parse(Console.ReadLine());
		// wenn ich hier her komme, hats geklappt!
		fSuccess = true;
		}
	// Einfach mal alles abfangen
	catch 
		{
		Console.WriteLine("Wahrscheinlich haben Sie keine Zahl eingegeben. Einfach nochmal!");
		}
	}
while (!fSuccess);

In diesem einfachen Fall ist auch eine "sinnvolle" Behandlung möglich. Es wird so lange eine neue Eingabe gefordert, bis der Benutzer eine Zahl eingibt oder entnervt aufgibt. Das catch in diesem Beispiel ist unspezifisch, es fängt jede Exception. Will man dies etwas spezifischer realisieren, kann nach dem Schlüsselwort eine Exception-Klasse angegeben werden:

	catch (System.FormatException ex) 

Jetzt werden nur Exceptions dieses Typs abgefangen, alle anderen bleiben unbehandelt und führen weiterhin zum Programmabbruch. Praktischerweise kann auf einen try-Block eine beliebige Menge von catches folgen, sodass prinzipiell eine beliebig detaillierte Reaktion möglich ist. Dies führt in der Praxis allerdings schnell zu einer beliebigen Menge an Arbeit. Zur Übersicht sei nochmals die grundlegende Geometrie eines Exception-Handlers aufgezeigt:

try
	{
	// hier steht der "gefährliche Code"
	}
catch (System.FormatException ex) 
	{
	// Sonderbehandlung bei spezieller Exception
	}	
catch 
	{
	// unspezifische Behandlung aller restlichen Exception-Typen
	}
finally
	{
	// das hier wird IMMER ausgeführt
	}

In diesem Beispiel taucht nun auch das Schlüsselwort finally auf, das C#-(eigentlich Microsoft-)spezifisch ist. In diesem Block kann Code untergebracht werden, der unabhängig vom Auftreten einer Exception auf jeden Fall anschliessend aufgerufen wird. Dies kann bei der Freigabe geteilter Ressourcen (wir z.B. Files) hilfreich sein. Es wurde ja bereits festgestellt, dass der try-Block mit der ersten Exception abgebrochen wird und die Programmausführung im Falle einer unbehandelten Exception endet. Mit finally können kleinere Aufräumarbeiten erzwungen werden.

Zum Handling von Exceptions sollte (nach meinem persönlichen Geschmack) noch folgendes gesagt sein:


31. Geworfen sein im existentialistischen Sinne...

Das wirklich schöne an Exceptions ist, dass nicht nur Teile des Runtime-Systems CLR solche erzeugen können - nein, jedes Programm ist in der Lage, dies auch selbst zu tun. Dazu dient erwartungsgemäß das noch nicht erwähnte Keyword throw, mit dem eine Exception geworfen werden kann. Wie bei catch schon erklärt, ist jede Exception mit einem Exception-Objekt verbunden. Dieses gehört der Klasse System.ApplicationException an oder ist von ihr abgeleitet. Was das alles bedeutet? Ein längliches Beispiel soll weitere Unklarheit schaffen.

// Eine eigene Exception-Klasse, sie erbt von System.ApplicationException
class UnwantedFakultaetException : System.ApplicationException 
    {
    //Der Konstruktor nimmt einen String als Argument, der dann in der Fehlermeldung steht
    public UnwantedFakultaetException(string strMessage) : base(strMessage)
        {
        }
    }

class MainClass
{
    public static void Main(string[] args)
        {
        
        int nZahl = 0; 
        bool fZahlValid = false;

        Console.Write("Fakultaet von: ");
        
// diese Schleife stellt sicher, dass eine ZAHL eingegeben wird. 
// Hier wird absichtlich eine spezifische Exception-Klasse abgefangen
        do
            {
            try
                {
                nZahl = Convert.ToInt32(Console.ReadLine());
                // hierher kommts nur, wenn Convert() geklappt hat...
                fZahlValid = true;
                }
// Nur eine Format-Exception soll so behandelt werden.
            catch (System.FormatException)
                {
                Console.WriteLine("Bitte eine ZAHL eingeben!");
                }
            }
        while(!fZahlValid);

// jetzt die eigentliche Berechnung                 
        try
            {
            // Macht mir das Leben leichter
            Console.WriteLine("{0}! = {1}",nZahl, fakultaet(nZahl));
            }
// Hier fangen wir jede Exception (um herauszufinden, was nicht geklappt hat)
        catch (Exception ex)
            {
            Console.WriteLine("Exception <{0}> aufgetreten!", ex.Message );
            }
// Das macht wenig Sinn, dient nur zur Anschauung.
        finally
            {
            Console.WriteLine("I'm done");
            }
        Console.WriteLine("Game Over");
        }

// Die iterative Fakultätsberechnung mit eigener Exception
    static int fakultaet(int n)
        {
        int i, fac;
        
// Hier wird unsere private Exception geworfen, wenn der Wertebereich nicht stimmt.     
        if (n < 1)
            {
            throw new UnwantedFakultaetException("Fakultaeten gibts nur fuer n > 0!");
            }
            
// checked aktiviert die Zahlenüberlaufsprüfung der CLR.          
        checked
            {
            for(i=1, fac=1; i<=n; i++)
                {
                try 
                    {
                    fac *= i;
                    }
// wenns beim Berechnen einen ueberlauf gibt...
                catch (System.OverflowException ex)
                    {
                    // Explizit hinweisen und
                    Console.WriteLine("Fakultaet scheitert bei n = {0}",i);
                    // die anstehende Exception "weiterwerfen"
                    throw;
                    }   
                }
            }
        return fac;
        }
} // class

Zum Werfen einer Exception wird also mit dem bekannten new eine Instanz (ein Objekt) einer Exception-Klasse erzeugt und dem Operator throw übergeben. Diese Exception läuft dann aufwärts durch die Aufrufhierarchie der Funktionen, bis sie entwerder von einem passenden catch gefangen wird oder die Main()-Funktion erreicht. Ist die Exception bis dahin nicht gefangen, wird das Programm abgebrochen.

Es ist auch möglich, mit

throw new Exception("So a Semf!");
eine generische Exception zu werfen, diese hat aber den logischen Nachteil dass sie nicht spefifisch gefangen werden kann. Deshalb ist diese Technik absolut PFUI!

Schlussendlich kan, wie die Funktion fakultaet() zeigt, eine Exception aus einem catch-Block heraus auch weitergeworfen werden, wenn eine korrekte Behandlung doch nicht möglich sein sollte. Hierzu genügt ein einfaches throw;


32. Locker von der Platte gehobelt

Der Wichtigtuer spricht von Objektpersistenz, wenn es darum geht, wichtige Daten unauslöschlich (*grins*) auf dem Massenspeicher festzuhalten. Dazu benötigt man in C# die Hilfe der Stream-Klasse. Dies funktioniert koomplett anders als in C, dass hier die historischen Wurzeln igoriert sein sollen.

Um auf ein File zugreifen zu können, genügt es in C#, durch den Aufruf einer statischen Methode der File-Klasse ein Stream-Objekt zu instanziieren. Die Stream-Klassen sind im Namespace System.IO zu finden

using System.IO;

Stream inStream = File.OpenRead("/home/cbx/test001.txt");

Dabei kann es natürlich passieren, dass dieses File nicht existiert. Was passiert? Wenig überraschend reagiert die CLR mit einer Exception vom Typ System.IO.FileNotFoundException. Diese kann wiederum mit catch spezifisch gefangen werden usw. das Spiel ist bekannt.

Für die Benutzer des einzig wahren Betriebssystems stellt bereits die Angabe eines Pfades ein nicht unerhebliches Problem dar. Da MS vor vielen Jahren beschlossen hat, den UNIX-Pfadtrenner "/" nicht zu verwenden, sondern durch den "\" zu ersetzen, entsteht mit den Sprachen der C-Familie das kleine Problem, dass der Backslash eine Steuersequenz einleitet (wie z.B. "\n" oder "\t"). der literale Backslash im Pfad ("C:\WINNT\System32\") muss deshalb auch escaped werden, indem man ihn doppelt angibt.

Stream inStream = File.OpenWrite("C:\\WINNT\\System32\\virus32.exe");

Weil das so uncool ist, hat Microsoft spät aber soch versucht, die Scharte auszuwetzen und ermöglicht mit dem String-Prefix "@" das Erstellen ungeparster literaler Strings.

Stream inStream = File.OpenRead(@"C:\WINNT\System32\virus32.exe");

Sobald die Erzeugung des Streamobjekts geklappt hat, können desse Methoden verwendet werden. Die zwei wichtigsten Methoden sind IOStream.Read() und IOStream.Write(). Mit diesen Funktionen können bereits alle Fileoperationen durchgefügrt werden, alle weiteren Methodne dienen nur der Komfort- und Effizienzstreigerung. Ein Beispielcode zum Ausgeben des Fileinhalts auf der Console könnte so aussehen:

    public static void Main(string[] args)
        {
        const int BUF_SIZ = 16;
        
        byte[] abBuffer = new byte[BUF_SIZ];

        Stream inStream, outStream;
        int nBytesIn;
        
        outStream = Console.OpenStandardOutput();
        try
            {
            inStream = File.OpenRead("/home/cbx/test001.txt");
            }
        catch (System.IO.FileNotFoundException)
            {
            Console.WriteLine("Des Feil gibznet");
            return;
            }

        
        while ((nBytesIn = inStream.Read(abBuffer, 0, abBuffer.Length)) > 0)    
            {
            outStream.Write(abBuffer, 0, nBytesIn);
            }
            
        return; 
        }

Bemerkenswert ist hierbei, dass in guten objektorientierten Systemen kein explizites Schliessen des Files erforderlich ist. Dies erledigt normalerweise der Destruktor des Objekts selbsttätig, sobald das Stream-Objekt zerstört wird (im Beispiel also die Main()-Funktion verlassen wird). Das klappt in C# nicht vernünftig! Deshalb ist ein explizites Schliessen des Files über die Close() Methode auf jeden Fall nicht nur sinnvoll, sondern notwendig, da nur damit sichergestellt ist, dass auch wirklich sämtliche geschriebenne Daten auf dem Speichermedium landen. Files, denen ein Teil des Inhalts fehlt, deuten meist auf ein vergessenes Close() hin.

Weiterhin existieren noch spezialisierte Streamklassen, die beispielsweise das Handling von Textfiles erheblich erleichtern. Die StreamReader-Klasse bietet den Komfort der bereits bekannten Funktionen der Console-Klasse (Read(), ReadLine(), Write(), WriteLine()).

        StreamReader inStreamReader  = new StreamReader("/home/cbx/test001.txt");
        StreamWriter outStreamWriter = new StreamWriter("/home/cbx/test001.bak");
    
        string text;
        int i=1;
        
        while ((text = inStreamReader.ReadLine()) != null)
            {
            Console.WriteLine(text);
            outStreamWriter.WriteLine("Zeile {0}: >{1}<",i++,text);
            }
        
        outStreamWriter.Close();

33. Ein String ohne Arschgeweih

Zur Abrundung der prozeduralen Fähigkeiten von C# soll ein Kapitel über die komfortablen Funktionen der Stringklasse nicht fehlen. Im Gegensatz zu den Vorgängersprachen C und C++ haben Strings in C# jeglichen Schrecken verloren. Dies beruht auf folgenden Unterschieden:

C, C++C#
Der C-String ist ein Metatyp, der durch ein nullterminiertes char[] Array abgebildet wird Der C#-String ist ein integraler Typ und eine vollwertige Klasse
Der C-String ist in seiner maximalen Länge nach der Deklaration unabänderlich begrenzt Der C#-String ist voll dynamisch und in seiner Maximallänge so gut wie unbegrenzt
Der C-String unterstützt keine Standard-Vergleichs- und Zuweisungsoperatoren Der C#-String unterstützt alle sinnvollen Standard-Vergleichs- und Zuweisungsoperatoren
Zur Bearbeitung von C-Strings werden die kryptischen str....() Funktionen der C-Library verwendet Zur Bearbeitung von C#-Strings stehen die komfortablen und leistungsfähigen Methoden des Objekts bereit

Durch diese Unterschiede wird die Verwendung von Strings in C# geradezu zu einem Vergnügen. Die Arraynatur des C-Strings hat sich allerdings als so praktisch erwiesen, dass auch der dynamische Container im C#-Stringtyp einen Indizierer "[]" besitzt, über den auf die einzelnen Zeichen zugegriffen werden kann. Dieses kleine Beispiel eines Deutsch-Türkisch-Übersetzers (oh, schmerz) zeigt eine mögliche Anwendung:

    public static void Main(string[] args)
        {
        string strTester = "Dies ist ein sehr kleiner Test";
        string strNew = "";
        
        for (int i = 0; i < strTester.Length ; i++)
            {
            if ('e'== strTester[i])
                strNew += 'ü';
            else
                strNew += strTester[i];
                            
            Console.Write(strTester[i]);
            }
            
        Console.WriteLine("\n{0}", strNew);
        return; 
        }
} // class

Die Ausgabe:

Dies ist ein sehr kleiner Test
Diüs ist üin sühr klüinür Tüst

Im Unterschied zu C lässt der Indizierer in C# allerdings keinen Schreibzugriff zu, sodass als Übersetzung ein neuer String aufgebaut wird. Dies geschieht dann allerdings wieder ganz einfach mit dem Additionsoperator.

Die Stringklasse bietet noch einige weitere Schmankerln, die sich am besten durch eigene Experimente erschliessen. Zu beachten ist, dass alle Methoden neue Stringobjekte liefern und das Ausgangsobjekt unangetastet lassen. Einige Startpunkte seien hier erwähnt:

Das folgende Schnipsel zeigt einige Tricks:

        foreach (string strWort in strTester.Split(' '))
            {
            Console.WriteLine(">{0}<",strWort.Trim());
            }
        
        int nPos = strTester.IndexOf("sehr");
        
        Console.WriteLine("\"sehr\" steht an Position {0}",nPos);
        

Die Ausgabe:

>Dies<
>ist<
>ein<
>sehr<
>kleiner<
>Test<
"sehr" steht an Position 13

Damit steht auch der String für weitere aufregende Abenteuer zur Verfügung!


34. Wie funktionieren Funktionen und warum?
Oder auch: „Nieder mit dem Spaghetticode“

Wenn es um den schwierigen Schritt von der grundlegenden Beherrschiung einer Programmiersprache zum wirklichen Programmieren geht, stellt sich bei praktisch allen Anfängern eine fatale Vorliebe für Spaghetticode ein. Unter Spaghetticode, der übrigens in jeder Sprache programmiert werden kann, versteht man die Implementierung vieler, wenn nicht sogar aller, Programmfunktionalitäten in einer einzigen Funktion.

Verhänglisvoller Weise erscheint diese Methode der Implementierung auf den ersteh Blick sogar einfacher, da man sich nicht mit Interfaces, Argument- und Rückgabetypen herumschlagen muß. In der Praxis erweist sich dieser Ansatz jedoch sehr schnell als Schuß ins Knie (bzw. Griff ins Klo, je nach persönlicher Vorliebe). Ab ca 200 Zelen verlieren Funktionen extrem an Übersichtlichkeit und Code ohne logische Strukturierung wird innerhalb kürzester Zeit unwartbar. Auf Basis derartigen Codes ein erfolgreiches Programm zu schreiben erweist dich dann als wahrer Fluch.

Um dem Spaghettimonster zu entkommen, bietet sich ein anderer Denkansatz an. In der akademischen Theorie soll Softwareentwicklung ungefähr so ablaufen:

  1. Anforderungen klären (== Aufgabenstellung)
  2. Anforderungen verstehen
  3. Problemstellung gedanklich modellieren
  4. Modell dokumentieren
  5. Lösungsansatz gedanklich entwerfen
  6. Lösungsansatz dokumentieren
  7. Lösung gedanklich entwerfen
  8. Lösung dokumentieren
  9. Lösung implementieren
  10. Lösung testen und ggf iterieren

In der studentischen Praxis (z.B. in der Übung SE) wird manchmal bereits Punkt 2 elegant übergangen, die Punkte 3 bis 8 werden praktisch immer wegoptimiert. Dies ist als Faktum zur Kenntnis zu nehmen, denn den Wert guter Designarbeit lernt jeder Entwickler erst durch eigene Schmerzen schätzen. Dennoch können mit einem Trick mindestens zwei Fliegen mit einer Rakete abgeschossen werden. Und das geht so:

Als Beispiel soll die Übungsaufgabe 8 aus diesem Semester dienen. Es geht um die Implementierung eines Logistikmoduls mit Entnahmefunktion und Persistenzeigenschaften. Hier kann im Hauptprogramm bereits eine Struktur gezeichnet werden, die sowohl dokumentarischen als auch strukturierenden Charakter hat. Angedacht sei folgender Ablauf:

Weiterhin ist das Datenmodell hier sehr einach. Das Inventar ist ein Array von Ganzzahlen, für jedes Produkt steht ein Indexwert zur Verfügung. Dieses Array wird innerhalb der Klasse gekapselt. Weitere Daten sind (eigentlich) nicht erforderlich.

Das Datenmodell und der prinzipielle Ablauf werden nun in Main() genau so durch Funktionen mit beschreibenden Namen festgehalten:

using System;
using System.IO;

class KuehlschrankClass
{

    const int      ARTIKEL_ANZAHL = 4;

    // Das hier ist das gesamte Kühlschrank-Modell! (ein int Array...)
    static private int[]    anArtikelAnzahl = new int[ARTIKEL_ANZAHL];

    public static void Main(string[] args)
        {
        
        if (! KuehlschrankLaden() )
            {
            KuehlschrankFuellen();
            }
            
        if (KuehlschrankZugang())
            {
            for (;;) 
                {
                KuehlschrankInhaltAuflisten();
                if (! KuehlschrankInhaltEntnehmen())
                    break;
                }
            }
        else
            {
            // kein Zugang
            }
        
        KuehlschrankSpeichern();

        return; 
        }

            
} // class

Es sollte klar sein, dass zu diesem Zeitpunkt keine der verwendeten Funktionen existiert, die Nennung des Namens allerdings deutet ihre Notwendigkeit an. Wo Kontrollstrukturen wie if() oder for() schon klar sind, werden sie auch schon eingesetzt. Im nächsten Schritt werden die angegebenen Funktionen leer implementiert. Argument- und Returntypen bleiben vorerst leer.

    public static void KuehlschrankLaden()
        {
        }

    public static void KuehlschrankFuellen()
        {
        }
    
    public static void KuehlschrankZugang()
        {
        }

    public static void KuehlschrankInhaltAuflisten()
        {
        }

    public static void KuehlschrankInhaltEntnehmen()
        {
        }

    public static void KuehlschrankSpeichern()
        {
        }

Das Programm lässt sich noch (immer) nicht compilieren, weil einige Typen noch nicht stimmen. Das Arbeitspensum ist allerdings schon klar. Die nun leeren Funktionen müssen gefüllt werden, dann ist die Aufgabe gelöst.

Anhand der umgebenden Kontrollstrukturen können wir jetzt schon entscheiden, dass nach unserem Ansatz die Funktionen KuehlschrankLaden(), KuehlschrankZugang() und KuehlschrankInhaltEntnehmen() einen return value vom typ bool benötigen. Aus der verwendeten Logik in Main() ergeben sich folgende notwendige Definitionen:

Dementsprechend werden die Prototypen angepasst und, wo erforderlich, return-Operatoren eingefügt. Damit wird unser Programm compilierbar.


    public static bool KuehlschrankLaden()
        {
        return true;
        }

    public static void KuehlschrankFuellen()
        {
        }
    
    public static bool KuehlschrankZugang()
        {
        return true;
        }

    public static void KuehlschrankInhaltAuflisten()
        {
        }

    public static bool KuehlschrankInhaltEntnehmen()
        {
        return true;
        }

    public static void KuehlschrankSpeichern()
        {
        }

Damit haben wir jetzt ein tragfähiges Gerüst für eine übersichtliche und elegante Implementierung. Es stellt sich nun noch die Frage der Funktionsargumente. In unserem Beispiel sind, im Sinne eines objektorientierten Ansatzes, keinerlei Argumente verpflichtend erforderlich, da alle Daten als Membervariablen der Klasse KuehlschrankClass implementiert werden können. Dennoch werden im Interesse einer schöneren Architektur noch einige kleine Retuschen vorgenommen.

Der resultierende Code sieht so aus:

    public static void Main(string[] args)
        {
        
        if (! KuehlschrankLaden(str_DER_PFOAD) )
            {
            KuehlschrankFuellen();
            }
            
        if (KuehlschrankZugang())
            {
            for (;;) 
                {
                KuehlschrankInhaltAuflisten();
                if (! KuehlschrankInhaltEntnehmen())
                    break;
                }
            }
        else
            {
            // kein Zugang
            }
        
        KuehlschrankSpeichern(str_DER_PFOAD);

        return; 
        }

/*****************************************************************/

    public static bool KuehlschrankSpeichern(string strPfoad)
        {
        }

/*****************************************************************/

    public static bool KuehlschrankLaden(string strPfoad)
        {
        }

/*****************************************************************/

    public static void KuehlschrankFuellen()
        {
        }
    
/*****************************************************************/

    public static bool KuehlschrankZugang()
        {
        }       

/*****************************************************************/

    public static void KuehlschrankInhaltAuflisten()
        {
        }

/*****************************************************************/

    public static bool KuehlschrankInhaltEntnehmen()
        {
        }

Jetzt werden noch die erforderlichen Member und Konstanten in der Klasse deklariert, wobei auch ein wenig Komfort nicht fehlen soll:

class KuehlschrankClass
{

    const int      ARTIKEL_ANZAHL = 4;
    // Damit der Pfad nicht eingetippt werden muss... (Komfort)
    const string   str_DER_PFOAD = "/home/cbx/coolshrankrc"; 

    // Das hier ist das gesamte Kühlschrank-Modell! (ein int Array...)
    static private int[]    anArtikelAnzahl = new int[ARTIKEL_ANZAHL];
    // Zuordnung der Indizes zu Namen (Komfort)
    // Die erzwungene Grössenangabe stellt sicher, dass für jeden Artikel ein Name existiert
    static private string[] astrArtikelName = new string[ARTIKEL_ANZAHL]  {"Bier","Wein","Cola","Pizza"};
[...]

Jetzt ist nur noch eine Implementierung der Funktionen erforderlich, wobei nun keine Funktion mehr als 30 Zeilen hat und somit sehr übersichtlich bleibt. Das Ergebnis erfüllt alle Anforderungen und könnte so aussehen:

    public static bool KuehlschrankSpeichern(string strPfoad)
        {
        StreamWriter outStream = new StreamWriter(strPfoad);
        
        for (int i=0; i < anArtikelAnzahl.Length; i++)
            {
            outStream.WriteLine(anArtikelAnzahl[i]);
            }
        
        outStream.Close();
        
        // OK, was anderes kommt hier derzeit nicht zurueck...
        return true;
        }

/*****************************************************************/

    public static bool KuehlschrankLaden(string strPfoad)
        {
        StreamReader inStream;
        
        try
            {
            inStream = new StreamReader(strPfoad);
            }
        catch ( FileNotFoundException ex)
            {
            // das File gibz net
            return false;
            }
        
        for (int i=0; i < anArtikelAnzahl.Length; i++)
            {
            anArtikelAnzahl[i] = Convert.ToInt32(inStream.ReadLine()); 
            }
            
        inStream.Close();   

        return true;
        }


/*****************************************************************/

    public static void KuehlschrankFuellen()
        {
        anArtikelAnzahl[0] = 10; // Bier        
        anArtikelAnzahl[1] = 3; // Wein     
        anArtikelAnzahl[2] = 5; // Cola     
        anArtikelAnzahl[3] = 6; // Pizza      
        }
    
/*****************************************************************/

    public static bool KuehlschrankZugang()
        {
        Console.Write("Entern Sie Ihre Mehladresse: ");
        
        string strEmail = Console.ReadLine();
        int nAt, nDot, nSpace;
        
        nAt = nDot = nSpace = 0;
        
        foreach (char c in strEmail)
            {
            switch (c)
                {
                case '@':
                    nAt++;
                    break;
                    
                case '.':
                    nDot++;
                    break;
                    
                case ' ':
                    nSpace++;
                    break;
                }
            }
            
        return ((nAt == 1) && (nDot >= 1) && (nSpace == 0));
        }       

/*****************************************************************/

    public static void KuehlschrankInhaltAuflisten()
        {
        Console.WriteLine("\n\n *** Hauptmenü *** \n");
        
        for (int i=0; i < anArtikelAnzahl.Length; i++)
            {
            if (anArtikelAnzahl[i] > 0)
                {
                Console.WriteLine("({0})...{1} (noch {2})",
                                   i+1, astrArtikelName[i], anArtikelAnzahl[i]);
                }
            }
        }

/*****************************************************************/

    public static bool KuehlschrankInhaltEntnehmen()
        {
        Console.Write("(q)... Ende\n");  
        Console.Write("\nIhre Wahl: ");  

        int c = Console.Read(); 

        // MONO: wegen bufferung der console das <Return> einfach hinterherlesen,
        // damit es hinterher nicht stoert...
        Console.Read();

        // aus ASCII die Index berechnen '1' ==> 0, '2' ==> 1 usw
        int n = c - '1';

        // Entnahme fuer gueltige Inhalt
        if (n >= 0 && n < anArtikelAnzahl.Length)
            {
            if (anArtikelAnzahl[n] > 0)
                {
                anArtikelAnzahl[n]--;
                Console.WriteLine("Ein mal {0}, bittesehr!", anArtikelAnzahl[n]);
                }
            }
         
        // Die geheime Auffuell-Taste    
        if ('R' == c)
            {
            KuehlschrankFuellen();
            Console.Write ("==> Miraculous refill happened...\n");        
            }
        
        // 1337 für: false, wenn 'q' eingegeben wurde
        return ('q' != c);  
        }

Damit ist die Aufgabe sehr strukturiert und übersichtlich gelöst und hat dennoch kaum mehr Zeilen als eine monolithische Spaghettilösung. Ich rate allen, die bis hierher durchgehalten haben, diese Technik einmal zu versuchen, da sie in den kommenden Semestern sicher noch viel überflüssigen Aufwand vermeiden kann.


→Lesezeichen

35. The C# programmers lifeboat
Oder auch: „Dummheit allein ist keine Schande“

Im Rahmen dieser Vorlesungsreihe können aufgrund des begrenzten Umfangs einige Vereinfachungen und Anleitungen gegeben werden, die das Erstellen einfacher Programme in C# wesentlich vereinfachen. Diese werden im Folgenden einfach lose aufgezählt:

Wir haben nur ein File und nur eine Klasse

Unsere Klasse wird nie instanziiert

Manchmal ist Eleganz nur das Zweitwichtigste.

Manchmal ist wenig schon genug.

Aus dem fetten Angebot an Typen, Klassen und Kontrollstrukturen, die C# anbietet,brauchen wir derzeit sehr wenig.

Bitte langsam stolpern

Aus der unüberschaubaren Menge von Fehlern, die man auch in C# noch machen kann, seien einige Schmankerln herausgegriffen.

Warum leichter als nötig?

Ein paar kleine Tipps aus der Praxis sollen das Codieren erleichtern


Glossar mit Wachstumspotential

Dieses Glossar wird im Lauf der Vorlesung wachsen und sich mit Inhalt füllen.