Zeichenketten-Konkatenierung in C# 👍 👎

Ein leider immer noch häufig vorzufindendes "Problem" in vielen C#-Anwendungen ist die vielen Entwicklern aus Sprachen wie PHP vertraute Verknüpfung von Zeichenketten per entsprechendem Operator ("." in PHP, "+" in C#):
Konkatenierung per Operator
0102030405
string tmp = "";
tmp += "ersteZeichenkette";tmp += "zweiteZeichenkette";tmp += "dritteZeichenkette";
Dies ist unter C# aus Sicht der Performanz besonders problematisch, da Zeichenketten in C# unveränderbar ("immutable") sind und demnach bei jeder solchen Verknüpfung ein neues Objekt erzeugt wird. Die Lösung für dieses Problem ergibt sich jedoch elegant durch Verwendung der StringBuilder-Klasse:
Konkatenierung per StringBuilder
0102030405060708
StringBuilder tmpBuilder = new StringBuilder();{    tmpBuilder.Append("ersteZeichenkette");    tmpBuilder.Append("zweiteZeichenkette");    tmpBuilder.Append("dritteZeichenkette");}
string tmp = tmpBuilder.ToString();
Dies ist bei C# üblicherweise bereits bei unter drei entsprechenden Vorgängen deutlich schneller als per Operator. Bei sehr vielen – jedoch durchaus praxisrelevanten – Vorgängen wird dieser Unterschied schnell so groß, dass mit der Verknüpfung per StringBuilder bis zu zwei Größenordnungen mehr Vorgänge in der gleichen Zeit wie per Operator möglich sind. Zudem wird selbstverständlich deutlich weniger Arbeitsspeicher benötigt. Insbesondere bei umfangreichen Schleifen ergibt sich hier also sehr schnell ein ernstzunehmendes Einsparpotential. Es handelt sich keinesfalls um "Mikrooptimierung" und wird auch von Microsoft selbst betont.

Bei ganz einfachen Verknüpfungen mit zwei Zeichenketten verwende ich durchaus die Operator-Variante, da sie etwas kompakter ist und der Unterschied minimal. Sobald mehrere Verknüpfungen notwendig werden, setze ich für gewöhnlich jedoch bereits auf einen StringBuilder.

Die intern erzeugte Zeichenkette des StringBuilder kann zur weiteren Verwendung wie im Beispiel ersichtlich jederzeit per ToString() abgerufen werden. Darüber hinaus stellt diese Klasse auch noch ein paar komfortable Methoden wie beispielsweise AppendLine(…) zur Verfügung.

Versionierung 👍 👎

Zu meiner Ergänzung zur Darstellung von IP-Adressen hat mich die Frage erreicht, ob ich vielleicht noch ein paar Sätze dazu schreiben möchte, wie ich es mit der Versionierung konkret halte.

Das möchte ich mit diesem Beitrag gerne machen, jedoch gehe ich da ziemlich traditionell und wenig spektakulär vor, so dass ich keine allzu großen Hoffnungen auf bahnbrechende Erkenntnisse machen möchte. Smiley: winking

Vor der Fertigstellung der initial zu veröffentlichenden Version verwende ich lediglich die internen Versions- bzw. Buildnummern. Zur öffentlichen Versionsnummerierung halte ich mich üblicherweise an folgendes Schema:

  x.y.z
  • x: Hauptversion

    Diese Nummer wird bei einer kompletten Neuentwicklung, bzw. einem Technologiewechsel erhöht. Dies ist z. B. bei einem Wechsel der Programmiersprache der Fall, oder aber wenn strukturelle/inkompatible Änderungen (insbesondere an öffentlichen Schnittstellen) stattfinden. Damit einher geht jedoch neben der – durchaus einschneidenden – Änderung an sich auf jeden Fall auch eine Neuentwicklung wesentlicher Bestandteile des gesamten Projektes.

  • y: Funktionalitätserweiterung

    Diese Nummer wird bei Erweiterungen der Funktionalität einer Anwendung erhöht. Dazu werden einige – im Allgemeinen überschaubare – zu erledigende Aufgaben gesammelt und dann mit dieser Version veröffentlicht. Bei besonders umfangreichen Ergänzungen kann diese Veröffentlichung durchaus auch nur einen einzelnen Aspekt betreffen, andererseits aber eben auch sehr viele kleinere.

  • z: Fehlerbehebung, Optimierung

    Diese Nummer wird bei der Behebung von Fehlern einer Anwendung erhöht. Dazu werden im Allgemeinen einige (insbesondere weniger dramatische) Probleme gesammelt und dann mit dieser Veröffentlichung behoben. Bei kritischen Fehlern, die die Funktionsfähigkeit des gesamten Programmes verhindern, oder aus Sicherheitsgründen schnellstmöglich behoben werden müssen, kann eine solche Version auch nur für einige wenige Änderungen für ein einzelnes Problem veröffentlicht werden.

    Sofern bestehende Funktionalität zwar erwähnenswert optimiert wird, jedoch nicht grundlegend erweitert, erscheint mir diese Stelle ebenfalls angemessen.

Zusätzliche Unterscheidungen oder ergänzende Begriffe verwende ich für gewöhnlich nicht. Zur Versionsverwaltung an sich setze ich auf Git per GitLab. Ein aus meiner Sicht gutes Dokument mit weitestgehend sinnvollen Vorschlägen ist außerdem Semantic Versioning

Dynamische Typisierung in C# 👍 👎

Im Artikel zur impliziten Typisierung hatte ich bereits auf einen späteren Artikel zum Thema dynamische Typisierung verwiesen, was mit diesem Beitrag nun auch erfolgen soll.

Seit .NET 4.0 enthält das Framework die sog. Dynamic Language Runtime, was sich bei C# auf den ersten Blick durch das neue Schlüsselwort dynamic, die Basisklasse DynamicObject und das ExpandoObject mit entsprechender Funktionalität bemerkbar macht.

Die DLR ermöglicht zwar "auch" interessante Aspekte im Hinblick auf die Integration dynamisch typisierter Programmiersprachen wie beispielsweise Python (→ IronPython) oder Ruby (→ IronRuby) in das .NET-Framework, ich möchte in diesem Beitrag aber dennoch bei den Möglichkeiten in C# selbst bleiben. Ein paar Gedanken meinerseits zur Typisierung im Allgemeinen sollen in einem späteren Beitrag folgen.

Wie vielen Lesern dieses Blogs bekannt sein dürfte, handelt es sich bei C# um eine grundsätzlich statisch typisierte Programmiersprache, d. h. die Typen aller Variablen sind bereits zum Zeitpunkt der Kompilierung bekannt, was nicht zuletzt auch ermöglicht, dass euch Visual Studio hilfreiche Hinweise schon während der Entwicklung geben kann.

Durch die DLR wird von diesem Prinzip (glücklicherweise Smiley: winking_grinning) nicht grundsätzlich abgewichen, jedoch stellt sich euch die Möglichkeit, von diesem Vorgehen explizit abzuweichen, indem ihr als Typ dynamic deklariert. Dadurch wird die statische Typprüfung deaktiviert und auf die Laufzeit verschoben. Das bedeutet, dass ihr einer Variable dieses Typs beliebige – also auch gar nicht existente – Eigenschaften zuweisen oder darauf Methoden aufrufen könnt, im Problemfall jedoch zur Laufzeit eine entsprechende Ausnahme erhaltet.

Wir beginnen – wie üblich – mit einem kleinen Code-Beispiel zur Verdeutlichung:
Dynamische Variablen erzeugen
0102
dynamic a = 123;dynamic b = "123";
Nun können wir relativ "normal" damit arbeiten, beispielsweise die Werte in einer MessageBox ausgeben:
Dynamische Variablen verwenden (1)
0102030405060708
MessageBox.Show(a.ToString());  //: "123"MessageBox.Show(b.ToString());  //: "123"/** * C# konvertiert hier nicht in eine mathematische Addition, * da "b" zur Laufzeit als Zeichenkette interpretiert wurde.**/MessageBox.Show(a + b);  //: "123123"
Soweit hätte das natürlich auch ohne dynamic funktioniert, der interessante Aspekt ist jedoch, dass sich beispielsweise auch folgender Code kompilieren lässt – Visual Studio zeigt euch bei Eingabe des Punktes (→ IntelliSense) nur mehr "(dynamischer Ausdruck) Der Vorgang wird zur Laufzeit aufgelöst." an:
Dynamische Variablen verwenden (2)
0102
MessageBox.Show(b.Replace("2", "4"));  //: "143"MessageBox.Show(a.Replace("2", "4"));  // Ausnahme!
Ohne dynamic hätte euch Visual Studio bereits während der Entwicklung angezeigt, dass Replace(…) für den Datentyp int nicht zur Verfügung steht (bzw. erst gar nicht zur Auswahl angeboten) und die Kompilierung mit einem Fehler abgebrochen.

Im Gegensatz zu var kann sich der Typ unserer Variable nun auch während der Laufzeit ändern:
Änderung des Typs zur Laufzeit
01020304050607
MessageBox.Show(a.GetType().ToString());  //: "System.Int32"MessageBox.Show(a.ToString());  //: "123"
a = "test";

MessageBox.Show(a.GetType().ToString()); //: "System.String"MessageBox.Show(a.ToString()); //: "test"
Dies entspricht schon sehr dem Verhalten, wie man es von Sprachen wie PHP kennt und bringt die entsprechenden Vorteile, aber eben auch gravierende Nachteile mit sich. Ob eine Operation zu Problemen führt, erkennt man nunmehr erst zur Laufzeit – und mit etwas Pech auch nicht sofort, da die ungültige Operation möglicherweise nicht sofort zur Verwendung kommt, mit statischer Typisierung aber dennoch bereits zur Entwicklungszeit von Visual Studio darauf hingewiesen worden wäre, dass dies niemals möglich sein wird.

Wenn das nun neben ein bisschen Bequemlichkeit beim Tippen doch eher Probleme einführt, stellt sich natürlich die berechtigte Frage, wozu man so etwas verwenden sollte. Hauptsächlich handelt es sich tatsächlich um ein Mittel, dynamische Sprachen in das .NET-Framework zu integrieren, welche nun einmal auf diesem Konstrukt basieren. Auch die Verwendung entsprechender Programmbibliotheken, um beispielsweise einfach über XML-Knoten zu navigieren (man denke an dieser Stelle auch an das DOM) sind sicherlich eine praktikable Idee. Für die reine Bequemlichkeit ist implizite Typisierung jedoch definitiv besser geeignet.

Ich würde auf jeden Fall dringend davon abraten, unter C# selbst exzessiven und vor allem unüberlegten Einsatz davon zu machen. Natürlich kann man sich dadurch z. B. gewisse Umstände, die Reflektion mit sich bringt, ersparen, dennoch würde ich hier nach wie vor zu einem sauberen, auf Schnittstellen basierten Modell raten. Dadurch, das alles "irgendwie" aufgerufen werden kann, handelt man sich nicht selten schwer auffindbare und auch lösbare Fehler ein. Insbesondere sollte bedacht werden, dass das .NET-Framework beim Aufruf entsprechender Funktionalität auch immer passende Datentypen erwartet; Folgendes ergibt beispielsweise umgehend einen Laufzeitfehler:
Gefahren bei der Interaktion mit dem .NET-Framework
010203
dynamic c = 5;
MessageBox.Show(c); // Ausnahme!
Das Problem ist nämlich, dass Show(…) eine Zeichenkette erwartet; bei b hätte es auch so funktioniert, c muss jedoch zwingend mit ToString() aufgerufen werden. Mit statischer Typprüfung sind das alles Probleme, die man bereits beim Schreiben des Quelltextes erfährt. Mit dynamischer Typprüfung möglicherweise aber erst nach einigen Stunden oder gar noch längerer Laufzeit, wenn die Stelle zum ersten Mal zur Ausführung kommt.

Da wir nun umfassend auf die potentiellen Gefahren eingegangen sind und erste Beispiele betrachtet haben, möchte ich noch einmal kurz auf DynamicObject und ExpandoObject zurückkommen und dazu jeweils ein kleines Anwendungsbeispiel demonstrieren. Wir beginnen mit dem ganz einfachen ExpandoObject und fügen diesem Eigenschaften und Verhalten hinzu:
ExpandoObject definieren und verwenden
01020304050607080910111213141516
dynamic obj = new ExpandoObject();{      // Eigenschaften setzen    obj.FirstName = "Holger";    obj.LastName = "Stehle";
obj.Age = 24;}
// Verhalten definierenobj.GetName = new Func<string>(() => { // der Rückgabetyp könnte auch "dynamic" lauten return obj.LastName + ", " + obj.FirstName;});
// Name und Alterstring person = obj.GetName() + " (" + obj.Age + ")"; //: "Stehle, Holger (24)"
Als nächstes wollen wir noch einen (kleinen) Blick auf das DynamicObject werfen, von welchem wir erben und dabei spezifisches Verhalten für bestimmte Aktionen festlegen können. Konkret möchten wir den Zugriff auf Eigenschaften unabhängig von Groß-/Kleinschreibung ermöglichen:
DynamicObject erben und verwenden
01020304050607080910111213141516171819202122232425262728293031
  // Klasse ableiten und Verhalten anpassenpublic class MyDynamicObject : DynamicObject {    private Dictionary<string,object> data = new Dictionary<string,object>();

// Eigenschaft abrufen public override bool TryGetMember(GetMemberBinder binder, out object value) { return data.TryGetValue(binder.Name.ToLower(), out value); }
// Eigenschaft setzen public override bool TrySetMember(SetMemberBinder binder, object value) { data[binder.Name.ToLower()] = value;
return true; }}
// Klasse verwendendynamic obj = new MyDynamicObject();{ // Eigenschaften setzen obj.FirstName = "Holger"; obj.lastName = "Stehle";}
string firstName = obj.FirstName; //: "Holger"string firstName = obj.firstName; //: "Holger"string lastName = obj.lastName; //: "Stehle"string lastName = obj.LastName; //: "Stehle"
Mein Fazit: Sicherlich wünscht man sich das eine oder andere Mal während der Entwicklung ein wenig mehr Dynamik, aber dafür bezahlt man einen nicht unerheblichen Preis zum Nachteil der Wartbarkeit. Zur Implementierung von Standardaufgaben sollte das unter C# nicht das erste Mittel der Wahl sein, eher eines der letzten – C# bietet hier für gewöhnlich mächtige und vor allem typsichere Alternativen. Ja, das ist durchaus mit etwas mehr Schreibarbeit verbunden.

Wie üblich hält das MSDN weitere Hinweise zur Verwendung parat.

Darstellung von IP-Adressen (Ergänzung: Versionskennungen) 👍 👎

Zu meinem letzten Eintrag hat mich eine Mischung aus Frage und Idee erreicht, dieses Verfahren doch auch bei Versionskennungen (z. B. v1.2.3) anzuwenden, welche schließlich ähnlich aussehen, wie eine IP-Adresse.

Grundsätzlich ist das natürlich naheliegend und nicht völlig abwegig, funktioniert jedoch nur bedingt. Das Problem: Die Ziffern unserer Stellen müssten nach bisherigem Vorgehen endlich sein, damit wir die Basis korrekt wählen können, um das Ergebnis eindeutig zu halten. Im Dezimalsystem stehen uns als Ziffern 09 zur Verfügung. Bei IP-Adressen lauten die "Ziffern" 0255 (dezimal).

Bei einer Versionskennung muss im Allgemeinen beispielsweise auf v1.0.9 nicht zwangsläufig v1.1.0 folgen (da die einzelnen "Stellen" traditionell* als Hauptversion, Nebenversion und Fehlerbehebungsversion geführt werden). Das heißt, werden erneut lediglich einige Fehler behoben, wird eher v1.0.10 folgen usw. Bei anders aufgebauten Versionskennungen (variierende Stellenanzahl, "beta" etc.) wird es noch schwieriger.

Sollte jedoch, aus welchen Gründen auch immer, jede Stelle auf z. B. 09 begrenzt sein, so könnten wir natürlich analog zum "üblichen" Dezimalsystem verfahren und als Basis die 10 wählen (oder einfach die Punkte entfernen …), womit das Verfahren wieder funktioniert. Dies dürfte jedoch ein eher seltener und ungewöhnlicher Spezialfall sein, insofern sollte hier der Einfachheit wegen nach wie vor eher stellenweise verglichen werden.


*) Ich persönlich halte daran auch fest. Viele Projekte scheinen diesem Schema jedoch nicht mehr zu folgen. Teilweise gibt es zumindest zur Kommunikation nach Außen gar nur noch eine einzelne Versionsnummer (z. B. v19), was jedoch auf den ersten Blick weniger über den Inhalt der Version aussagt. Es ist sicherlich nicht ganz falsch, dass das den Endanwender auch nicht direkt interessieren muss – praktisch finde ich es trotzdem. Smiley: tongue_out

Darstellung von IP-Adressen 👍 👎

Vorweg sei erwähnt, dass ich mich im weiteren Verlauf des Artikels der Übersicht wegen auf IPv4 beschränken werde. Das vorgestellte Konzept funktioniert prinzipiell jedoch auch bei IPv6 in ähnlicher Form.

Jeder hat bestimmt schon einmal IP-Adressen der Form "127.0.0.1" (lokale Adresse), "192.168.1.1" (private Adresse) oder "80.78.81.19" (öffentliche Adresse) gesehen. Das ist eine für den Menschen bequeme Schreibweise, jedoch nur bedingt zur automatisierten Verarbeitung (z. B. für Vergleiche bei Sortierungen) geeignet.

Eine IPv4-Adresse besteht aus vier Segmenten, die jeweils 8 Bit (≙ 4 * 1 Byte = 32 Bit, bzw. 4 Byte) umfassen. Insofern verwendet eine IPv4-Adresse dieser Schreibweise ("dotted decimal") eine Art Stellenwertsystem wie unser gebräuchliches Dezimalsystem, wobei die einzelnen Stellen jedoch mehrere Ziffern umfassen und daher durch ein Trennzeichen (in diesem Fall ein ".") getrennt werden.

Die Wertigkeit der Stelle einer Zahl unseres Dezimalsystems ergibt sich bekanntermaßen aus dessen Position:
Stellenwertsystem von Dezimalzahlen
01020304050607
  1 =                     1 * 100  5 =                     5 * 100
10 = 1 * 101 + 0 * 100 15 = 1 * 101 + 5 * 100
123 = 1 * 102 + 2 * 101 + 3 * 100
Ähnlich verhält es sich nun bei einer IPv4-Adresse, jedoch mit entsprechend anderen Wertigkeiten:
Stellenwertsystem von IP-Adressen
0102030405
127.  0.  0.  1 = 127 * 224 +   0 * 216 +   0 * 28 +   1 * 20 = 2 130 706 433
192.168. 1. 1 = 192 * 224 + 168 * 216 + 1 * 28 + 1 * 20 = 3 232 235 777
82.165. 40.206 = 82 * 224 + 165 * 216 + 40 * 28 + 206 * 20 = 1 386 555 598
In Anbetracht dieser Tatsache können wir einer IPv4-Adresse also einen ein-eindeutigen ganzzahligen Wert zuordnen, indem wir – analog zu unserem Dezimalsystem – die Summe der einzelnen Stellen bilden. Durch diese Darstellungsweise sind nun beispielsweise sehr einfach "größer"- und "kleiner"-Relationen zu ermitteln.

Die zu dieser Thematik passende Eigenschaft Address der IPAddress-Klasse des .NET-Frameworks ist als veraltet gekennzeichnet. Solltet ihr diese Funktionalität dennoch benötigen, so lässt sich dies selbstverständlich auch schnell selbst schreiben – wobei zu berücksichtigen gilt, dass das nur für IPv4-Adressen in dieser Form verwendet werden kann:
Vorgehensweise zur ganzzahligen Darstellung von IPv4-Adressen
010203040506
IPAddress ipAddress = IPAddress.Parse("80.78.81.19");byte[] segmentList = ipAddress.GetAddressBytes();
long ip2long = Convert.ToInt64((segmentList[0] * Math.Pow(2, 24)) + (segmentList[1] * Math.Pow(2, 16)) + (segmentList[2] * Math.Pow(2, 8)) + segmentList[3]); // – oder auch -long ip2long = (((long) segmentList[0] << 24) | ((long) segmentList[1] << 16) | ((long) segmentList[2] << 8) | (long) segmentList[3]);
PHP bietet hier beispielsweise die Funktion ip2long (und deren Gegenstück long2ip) an.

Projektverweise

Kategorien / Archiv  |  Übersicht RSS-Feed

Schlagworte

Suche