Was Entwickler wollen

C# – Eine kurze Geschichte der Sprache (und ihren Fehlern)
Kommentare

C# ist eine weit verbreitete, ausgereifte Universalprogrammiersprache; Millionen professioneller Entwickler weltweit nutzen sie; doch das war nicht immer so.

Welche Schritte führten zum Erfolg? Was konnten die C#-Schöpfer von der Praxis lernen? Wir blicken in die Vergangenheit, Gegenwart und Zukunft von C# und stellen die Top-3-Fehler in echtem C#-Code vor.

Die ersten beiden Sätze der Spezifikation von C# 1.0 zeigen die Entwicklungsziele der Sprache in ihrer Reinform: „C# ist eine einfache, moderne, objektorientierte und typsichere Programmiersprache. C# hat seine Wurzeln in der C-Sprachfamilie und wird Programmierern von C, C++ und Java schnell geläufig sein.“ Der C#-Chefarchitekt, Anders Hejlsberg, hat einmal gesagt: „Neue Sprachen starten mit minus zehntausend Punkten.“ Die Kosten, um eine Sprache zu entwickeln, zu verbreiten, zu pflegen und zu erweitern, sind so gewaltig, dass die Gewinne daraus wirklich enorm sein müssen – verglichen mit denen einer bestehenden Sprache. Daher ist jede neue Sprache ein Versuch, die Fehler ihrer Vorgängerin auszubessern. C# ist hier keine Ausnahme.

Und am Anfang war C# 1.0

C# 1.0 wurde als Antwort auf C++ entwickelt, von ihr übernimmt sie auch ihre grundlegenden syntaktischen Elemente. Die C#-Schöpfer verschafften sich einen Überblick über die vielen Fallstricke, mit denen Nutzer von C++ zu kämpfen haben, und entwickelten Lösungen für viele der Probleme. Einige Lösungen waren zugleich große Abschiede, etwa der Rauswurf der multiplen Klassenvererbung. Die meisten Lösungen waren subtiler, beispielsweise die Regel, dass ein Name in einem Block nur für die gleiche Sache genutzt werden darf. Die Entwickler achteten besonders auf die Typsicherheit der Sprache. Eine Sprache bezeichnet man dann als typsicher, wenn der Compiler bestimmte Operationen verbietet – etwa das Speichern von einem Kundennamen an einem für Währungsbeträge vorgesehenen Speicherplatz. Typsicherheit ist jedoch meist ein zweischneidiges Schwert: Obwohl es die Kompilierung vieler fehlerhafter Programme verhindert, erschwert es doch manche Programmiertechniken. C# 1.0 wurde deshalb als goldene Mitte konstruiert: Es verfügt über ein Typensystem, das zwar strikt genug ist, um Fehler zu verhindern – Entwicklern es aber gleichzeitig erlaubt, einen Teil der Typprüfung auf die Laufzeit zu schieben. Das Ergebnis war, wie die Spezifikation es ausdrückt, eine ziemlich einfache objektorientierte Sprache. C# 1.0 war zwar eine brauchbare Sprache, mit Sicherheit aber noch nicht ausgereift.

Der Reifeprozess und vier Titelfeatures: C# 2.0 (2005) bis 5.0 (2012)

Jedes der vier Releases seit Version 1 hatte ein Merkmal, das die Außenwahrnehmung stärken sollte. Das Merkmal von C# 2.0 waren generische Typen. Das C#-Typensystem hat durch sie enorm an Mächtigkeit, Sicherheit und Umfang gewonnen. Generische Typen reichen in nahezu alle Bereiche der Sprache und ihre Laufzeitbibliotheken und gaben C# letztlich ihre heutige Form. Nach der erfolgreichen Einführung der generischen Typen nahm sich das Entwicklerteam eine Menge bereits bestehenden Code vor und untersuchte ihn. Sie fanden viele verschiedene Techniken zum Datenmanagement, jede mit ihren eigenen Mechanismen für die Sortierung, Suche, Gruppierung etc. – und alle Techniken waren miteinander inkompatibel.

Das Merkmal von C# 3.0 war daher Language Integrated Query, kurz LINQ. Dieses Feature ermöglicht dem Entwickler, Ausdrücke wie „Zeig mir die ersten zehn Kunden, die in London leben, sortiert nach Nachname“, und zwar unabhängig davon, ob die Kundendaten in einem lokalen Array, einem XML-Dokument, einer Datenbank oder einem anderen Speichersystem liegen. Die Herausforderung bei der Entwicklung von C# 3.0 war, die Balance zu finden – zwischen den mächtigen, aber schwer zu erklärenden Grundlagen der Kategorientheorie und einer Syntax, die einfach zu verstehen ist.

Dynamische Sprachen und Typsicherheit

LINQ erwies sich als Erfolg. Als Nächstes verglich das Entwicklerteam die Trends der Branche mit den ursprünglichen Zielen von C#. Eines der Ziele war stets gewesen, Entwickler mithilfe der bestehenden Microsoft-Plattformen zu unterstützen. Zur selben Zeit erlebten jedoch weniger typsichere, „dynamischere“ Sprachen einen Aufschwung: JavaScript, Python und Ruby. Die C#-Entwickler entschieden sich daher, als Titelfeature von C# 4.0 die Interoperabilität mit dynamischen Sprachen zu wählen.

Diese Entscheidung war umstritten. C#-Entwickler lieben Typsicherheit und jeder Versuch, C# weniger typsicher zu machen, stieß auf Widerstand. Das Entwicklerteam durchlief mehrere Zyklen, in denen sie ein Feature vorschlugen, es einer sorgfältig ausgesuchten Gruppe Entwickler zur Revision vorlegten, und damit wieder zum Reißbrett zurückkehrten. Schließlich fanden sie eine zufriedenstellende Balance zwischen Sicherheit, Usability und Leistung. Außerdem eine Möglichkeit, das Feature einer skeptischen Nutzergemeinde zu erklären: Wenn man ein C#-Programm schreibt, das mit Objekten interagiert, die für den Einsatz in einer dynamischen Sprache gedacht sind, so hat man die Typsicherheit ohnehin bereits aufgegeben – wenigstens für diesen Teil des Programms.

Die Interoperabilität mit dynamischen Sprachen sollte nicht als sinkende Typsicherheit wahrgenommen werden; das Feature erleichtert im Gegenteil das Verständnis für und die Pflege von bestehenden unsicheren Programmteilen. Dieser Code ist daher mit höherer Wahrscheinlichkeit korrekt, sogar ohne eine vom Compiler erzwungene Typsicherheit.

Latenzzeit und asynchrone Programme

Bei der Entwicklung von C# 5.0 versuchte das Team erneut, die praktischen Bedürfnisse der Entwickler einzubeziehen und erkannte, dass viele der branchenüblichen Programme mittlerweile mit Latenzzeiten zu kämpfen hatten. Es spannt sich zeitlich eine riesige Kluft – möglicherweise Milliarden von Maschinenzyklen – zwischen der Anfrage der Daten und deren Bereitstellung. Auch die Lichtgeschwindigkeit hat ihre Grenzen. Wenn der Rechner mit den benötigten Daten auf einem anderen Kontinent steht, heißt es eben warten. Hohe Latenzzeiten und traditionell objektorientierte Sprachen machen es äußerst schwierig, Programme zu schreiben, die Prozessoren effektiv nutzen und eine gute Bedienoberfläche bieten.

Mit einem neuen asynchronen wait-Operator bietet C# 5.0 die Möglichkeit, asynchrone Programme so einfach zu schreiben wie traditionelle synchrone Software. Der rote Faden hier ist, dass das Entwicklerteam in jeder Version seit C# 1.0 auf die praktischen Bedürfnisse professioneller Entwickler geachtet hat und diese Bedürfnisse mit zusätzlicher Syntax bedient. So kann die Sprache C# einfacher und natürlicher das ausdrücken, was die Entwickler mitteilen möchten.

Die Zukunft von C#: offener und angepasster

Microsoft ist bereits seit einiger Zeit dabei, den C#-Compiler (und damit auch den Visual Basic-Compiler) unter dem Code Namen „Roslyn“ zu einem „Compiler-as-a-Service“ umzubauen. Bald wird Microsoft jene lexikalischen, syntaktischen und semantischen Analyse-Engines, die auch vom Compiler und der Visual Studio IDE genutzt werden, als dokumentierte und unterstützte Bibliotheken erstmals zur Verfügung stellen. Das wird hauptsächlich zwei Folgen haben. Zuerst werden Drittanbieter aufatmen: Wenn Microsoft seine C#-Analysebibliotheken verfügbar macht, sinken die Entwicklungskosten von Analysetools für Dritte. Analysetools von anderen Anbietern werden sich weiter verbreiten und Entwicklern helfen, C#-Code besser zu verstehen. Sie werden ihn besser erkunden, pflegen und organisieren, leichter annotieren, dokumentieren, überarbeiten und letztlich auch debuggen können. C#-Entwickler lieben Tools, die ihren Code verbessern.

Zweitens wird Microsoft mithilfe der neuen Architektur die Sprache weiter voranbringen. Es ist zu erwarten, dass sich C# über die nächsten Releases weiterentwickelt. Das C#-Entwicklerteam wird auch zukünftig ein Auge auf die Trends der Branche haben. Die Sprache C# hat noch eine lange Zukunft vor sich. Allerdings gibt es noch viele kleine Schönheitsfehler, die auszubessern und viele kleine Produktivitätsfeatures für Entwickler, die noch zu implementieren sind – einfach weil sie bisher nicht weit genug oben auf der langen Liste der möglichen Verbesserungen standen. Im Folgenden finden Sie eine dreiteilige Hitliste mit Fehlern, die bei der Arbeit mit C# auftreten.

Aufmacherbild: Program code and computer keyboard von Shutterstock / Urheberrecht: isak55

[ header = Seite 2: Top-3-Fehler in echtem C#-Code ]

Top-3-Fehler in echtem C#-Code

„Im starken Gegensatz zum anstrengenden Weg zum Erfolg, der für eine Gipfelersteigung oder den Weg durch eine Wüste typisch ist, wollen wir es unseren Anwendern ganz einfach machen, bewährten Prinzipien zu folgen – indem Sie unsere Plattform und Frameworks nutzen.“ (Rico Mariani, Architekt von .NET, 2003). Ricos Philosophie heißt „Der gerade Weg führt zum Erfolg“ und durchdringt das Design von C# und .NET Framework: Der einfache Weg soll stets zur korrekten Lösung führen. Nun hat noch niemand eine universelle Sprache erfunden, mit der man keine Fehler machen könnte; auch das tatsächliche Design von C# und .NET hält noch immer einige Fallstricke bereit.

Wenn man mit echtem Code arbeitet, sieht man die gleichen Fehler wieder und wieder auftauchen. Ich habe meine drei Favoriten an dieser Stelle einmal zusammengefasst: Zuerst die Schwächen von Class Libraries im .NET Framework, dann ein Problem mit dem Sprachendesign und zuletzt ein überraschend häufiges, aber mangelhaftes Coding-Muster.

Platz 3: Fehlerhafte Zufallszahlen

public static int RollSixSidedDie()
{
  var random = new Random();
  return random.Next(1, 6);
}

Bevor Sie weiterlesen: Sehen Sie die Fehler in dieser einfachen Methode in Listing 1? Dieser Fehler wird häufig auf stackoverflow.com gemeldet. Wenn man den Code in einer Schleife aufruft, wird normalerweise immer die gleiche Zahl ausgegeben – nicht gerade besonders zufällig. Der Pseudozufallszahlengenerator, den der Standardkonstruktor erstellt, wird von einem Zeitstempel gefüttert, der lediglich auf eine Millisekunde genau ist. Da Computer in einer Millisekunde aber Millionen von Rechenoperationen durchführen können, wird mit ziemlich hoher Wahrscheinlichkeit bei einer engen Schleife jede neue Instanz von Random mit dem gleichen Input gefüttert. Der zweite Fehler ist, dass die zurückgegebene pseudozufällige Zahl „größer oder gleich dem minValue und kleiner als der maxValue“ ist, wie es in der Dokumentation heißt. Dieser Code produziert also eine Zahl zwischen eins und fünf. Eine korrektere Art, diesen Code zu schreiben, zeigt Listing 2.

static Random random = new Random();
public static int RollSixSidedDie()
{
  return random.Next(1, 7);
}

Doch auch diese Schreibweise bringt Probleme mit sich, da die Random-Klasse nicht threadsicher ist – obwohl man von einer solch simplen statischen Methode Threadsicherheit erwarten könnte. Diese Klasse lässt ihren Benutzer ins Messer laufen – denn der natürlichste Weg, Code zu schreiben, ist hier der falsche. Die Klasse hätte so entworfen werden sollen, dass sie das, was der Nutzer am meisten benötigen wird – eine Sequenz pseudozufälliger Zahlen in einer bestimmten Größenordnung – ohne diese Stolperfallen liefert.

Platz 2: Fehlerhafter Umgang mit Closures

Dieses Problem wurde in den C#-Versionen 3 und 4 am häufigsten gemeldet als „Ich habe einen Bug im Compiler gefunden.“ Der Code ist in Listing 3 stark vereinfacht, um das Problem deutlicher zu zeigen – im echten Code tritt er mit einiger Häufigkeit auf.

var multipliers = new List<Func<int, int>>();
var multiplicands = new[] { 10, 20, 30 };
foreach(int m in multiplicands)
multipliers.Add( x => x * m );
var timesTen = multipliers[0];
Console.WriteLine(timesTen(50));

Man denkt, es würde 500 ausgegeben, in C# 3 und 4 gibt es jedoch 1500 aus. Sehen Sie, warum? Der Lambda-Ausdruck x => x * m bedeutet „multipliziere x mit dem aktuellen Wert von m“ und nicht „multipliziere x mit dem Wert, den m hatte, als der Delegat erstellt wurde“. Ein Informatiker würde sagen: Lambda-Ausdrücke werden über einer Variable geschlossen, nicht über einem Wert. Dieses Problem wurde dem C#-Compilerteam so häufig gemeldet, dass das Sprachendesign mit C# 5 geändert wurde, um es zu verhindern. Lambdas schließen noch immer über Variablen, aber die foreach-Schleife wurde so angepasst, dass sie bei jedem Durchlauf eine neue Variable erstellt. Das entsprechende Problem bei regulären Schleifen besteht noch immer. In diesem Fall hat jedes einzelne Feature der Sprache für sich seinen eigenen Sinn, aber die Kombination der Features bereitet dem Nutzer Schwierigkeiten.

Platz 1: Fehlerhafte Nullreferenzprüfung

Dies ist ein wirklich häufiger Fehler. Die C#-Analyse-Engine von Coverity findet einen davon in vier bis 5 000 analysierten echten Codezeilen. Das Beispiel in Listing 4 ist ebenfalls stark vereinfacht, um den Fehler zu verdeutlichen.

public Animal FindAnimal(string name, List<Filter> filters, bool onlyFish)
{
  // Performance optimization for the common case of looking for a fish with no filters:
  if (onlyFish && (filters == null || filters.Count == 0))
  return fish[name];
  Animal result = animals[name];
  if (onlyFish && !(result is Fish))
  return null;
  foreach(var filter in filters)
  if (!filter(result)) 
  return null;
  return result;
}

Sehen Sie den Fehler? Die Abfrage, ob filters null ist, legt nahe, dass der Autor dieses Codes erlaubt, dass das Argument null ist. Dies hätte einen Programmabsturz zur Folge, würde die foreach-Schleife mit diesem Wert ausgeführt. Der Verfasser muss eine von drei Sachen im Sinn gehabt haben:

1. filters darf dann null sein, wenn onlyFish wahr ist, sonst jedoch nicht
2. filters darf immer null sein
3. filters darf niemals null sein

Im ersten Fall hat sich der Entwickler selbst ein Bein gestellt. Die Methode sollte wahrscheinlich in zwei Methoden aufgespalten werden, jede mit einem klareren Kontrakt. Wenn die zweite Aussage wahr sein soll, so hat der Code einen Bug, der zum Absturz führt. Sollte die dritte Aussage die Absicht gewesen sein, dann führt die null-Abfrage den Leser in die Irre; der Code sollte umgeschrieben werden und mit einer Abfrage beginnen, die eine ArgumentNullException wirft. Egal, welcher der drei Fälle zutrifft – der Code hat ernsthafte Mängel.

Fazit

Die gezeigten Fehler sind nur drei Beispiele aus Dutzenden Fehlermustern, die immer wieder in echten C#-Codebasen auftauchen. Bei jedem Fehler frage ich mich: Lässt sich ein Codeanalysator schreiben, der diesen Bug automatisch findet? In einer idealen Welt würden Sprache und Framework den Entwickler von Anfang an davon abhalten, defekte Software zu schreiben – aber Universalsprachen sind nicht perfekt. Zum Glück können wir für viele Fälle Analysen schreiben, die häufig auftretende Fehler erkennen und den Entwickler darauf aufmerksam machen, bevor der Fehler an den Kunden ausgeliefert wird.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -