Die Subdivisionen eines Strings
Naiv betrachtet besteht ein String aus Zeichen (in .NET vom Typ Character). Wie wir gleich feststellen werden, ist der Begriff eines „Zeichens“ aber nicht mehr so trivial, wenn wir uns mit Unicode beschäftigen.
Unicode zuerst einmal nur eine Zuordnung von Schriftbestandteilen zu einer Zahl, üblicherweise geschrieben als U+xxxx, wobei xxxx die Zahl in Hexadezimalschreibweise ist. So ist dem großen lateinischen A die Zahl U+0041 zugeordnet, dem großen kyrillischen А hingegen die Zahl U+0410. Zudem kann ein Text-Element (also ein Zeichen, wie es der Endanwender sieht) aus mehreren Unicode-Einheiten bestehen. Häufiger gesehene Beispiele sind die Niqqudot im Hebräischen oder Hautfarben für Emojis. Es gibt verschiedene Repräsentationen für Unicode, für uns sind aber nur zwei wirklich relevant.
UTF-16
UTF-16 ist das interne String-Format von Windows. Es wurde aus dem 16-Bit-Encoding UCS-2 entwickelt, kann im Gegensatz zu diesem allerdings auch Zeichen jenseits 0xFFFF, repräsentieren. Dazu bedient es sich der sogenannten Surrogate Pairs, die zwei 2048-Zeichen-starke Unicode-Unterbereiche belegen von U+D800 bis U+DFFF belegen.
UTF-16 hat einige Schwächen: Als 16-Bit-Repräsentation ist die Reihenfolge der Bytes relevant, weshalb UTF-16-Dokumente (aber nicht Strings im Speicher) üblicherweise mit der Byte-Order-Mark (U+FEFF) beginnen. Zudem werden Unicode-Zeichen ab U+10000 als 4-Byte-Sequenzen gespeichert, die mit U+Dxxx beginnen, was die lexikographische Sortierung erschwert, da diese vor den Zeichen U+E000 bis U+FFFF eingeordnet werden. Da die Implementation ursprünglich auf UCS-2 beschränkt war, gibt String.Length die Anzahl der Codepoints (also zwei statt einer für Zeichen ab U+10000). Es ist in mehreren Windows-Funktionen (z. B. CreateFile) auch möglich, nur eine Hälfte eine Surrogate Pairs zu benutzen, obwohl das kein gültiges UTF-16 ist. Dazu zudem in der modernen Datenübertragung das Byte die atomare Einheit ist (es geht also nie weniger als ein ganzes Byte verloren), kann die Hälfte einer UTF-16-Einheit verlorengehen, ohne dass dieses leicht erkennbar ist – wenn kein Byte im String im Bereich von D8 bis DF liegt, bleibt sogar legales, aber nutzloses UTF-16 übrig.
UTF-8
UTF-8 ist die am weitesten verbreitete Textkodierung im Internet, mit deutlich über 90% aller Websites, die in UTF-8 (oder reinem ASCII) angeboten werden. UTF-8 kodiert jedes Unicode-Zeichen in ein bis vier Bytes, wobei die ersten 128 Zeichen identisch mit ASCII sind, jeder ASCII-Text also automatisch auch ein UTF-8-Text ist. Obwohl UTF-8 für fernöstliche Zeichensätze ineffizienter als UTF-16 ist – es benötigt drei statt zwei Bytes – ist es das auch dort die am meisten genutzte Textkodierung. Das hängt auch damit zusammen, dass moderne Websites zu einem enormen Teil aus HTML und JavaScript bestehen, die auf ASCII beschränkt sind und somit in UTF-8 doppelt so effizient gespeichert werden können wie in UTF-16.
Neben der ASCII-Kompatibilität hat UTF-8 noch ein paar weitere Vorteile gegenüber UTF-16. Zum einen ist das erste Byte einer Sequenz leicht erkennbar (es ist < 128 oder ≥ 192), zu anderen zeigt das erste Byte einer Multibytesequenz die Anzahl der folgenden Bytes (welche also ≥128 und <192 sind) an. Verlorene Bytes sind somit leicht erkennbar und leicht zu kompensieren. Weiterhin lassen sich die einzelnen Zeichen einfach als Bytestream lesen, ohne auf Big- oder Little-Endianness achten zu müssen – es gibt nur eine korrekte Reihenfolge. Dennoch besitzen insbesondere unter Windows Textdokumente eine UTF-8-kodierte Byte-Order-Mark; beispielsweise speichert Notepad Textdateien so ab. Diese ist normalerweise unnötig, sollte aber bei Konvertierung von UTF-16 beibehalten werden.
UTF-8 erlaubt, unter leichter Verletzung des Standards, ein paar hilfreiche Tricks. Zum einen ist es möglich, das NUL-Zeichen (U+0000) als 9-Bit-Zeichen aufzufassen und so mit zwei Bytes zu kodieren, von denen keines 0 ist. Das erlaubt es, NUL in C-Strings zu speichern, die bekanntlich NUL-terminiert sind, ohne separat ihre Länge zu erfassen. Diese Variante ist als „Modified UTF-8“ bekannt. Außerdem ist es trivial möglich, einzelne Surrogate-Pair-Einheiten per UTF-8 zu kodieren (vollständige Surrogate Pairs sollten über das übliche Verfahren kodiert werden), was es ermöglicht, auch invalides UTF-16, z. B. in NTFS-Dateinamen, in UTF-8 zu kodieren. Diese Variante ist als „Wobbly Tranformation Format (WTF-8)“ bekannt.
Konsequenzen
Die Mehrheit der externen Schnittstellen spricht heutzutage UTF-8. Nur sehr selten trifft man auch einen Dienst an, der noch ein überholtes 8-Bit-Encoding verwendet. BizTalk selbst benutzt aber intern, wie auch Windows und das .NET Framework, UTF-16. Mit externen Systemen kann BizTalk aber problemlos UTF-8 sprechen, und sollte das auch möglichst oft tun. Dieser Punkt sollte in der Design-Phase geklärt werden. Ebenfalls müssen in der XML-Assembler-Pipeline die Option PreserveBOM und TargetCharset korrekt eingestellt sein.
Wie oben beschrieben ist der naive Umgang mit dem eingebauten String-Typ von .NET (und mit WCHAR* in Win32) nicht ganz einfach. Glücklicherweise gibt es aber mit System.Globalization.StringInfo Abhilfe. Dort gibt es zum einen die Methode SubstringByTextElements. Diese Methode agiert, wie der Name impliziert, nicht auf Bytes, UTF-16-Characters oder sogar Code Points, sondern auf ganzen Text-Elementen, also Basiszeichen komplett mit allen kombinierenden Zeichen agiert. Entsprechend ist also
new StringInfo("שָׁלוֹם").SubStringByTestElements(0, 2) == "שָׁל"
statt "שָ" – man beachte hier auch den fehlenden Schin-Punkt rechts oben.
Analog dazu gilt
StringInfo.LengthInTextElements("שָׁלוֹם") == 4
und nicht etwa 7. Zum Indizieren der Zeichen existiert die Methode ParseCombiningCharacters, die ein Array mit den Positionen aller vollständigen Zeichen zurückgibt. Mit GetTextElementEnumerator existiert auch ein IEnumerator; dieser ist leider kein IEnumerator<string>, benötigt unter Umständen also etwas zusätzliches Casting.
Auch bei regulären Ausdrücken sollte man alte ASCII-Gewohnheiten loswerden. Um Worte zu matchen, ist der Ausdruck [A-Za-z]+ (oder gar [A-Za-zÄÖÜäöüß]+) nicht nur völlig ungeeignet, sondern auch unnötig komplizierter als das korrekte \w+, welches in .NET Unicode-fähig ist.
Will man herausfinden, ob ein Zeichen z. B. ein Buchstabe oder eine Zahl ist, kann man die Klasse System.Globalization.CharUnicodeInfo bemühen, die nicht nur die korrekte Unicode-Kategorie finden kann, sondern auch den Zahlenwert eines Zeichens, unabhängig davon, ob es sich z. B. um arabische oder tibetische Zahlen oder Varianten einer Zahl (z. B. ² oder ½) handelt.