C# – Kürzen Sie einen UTF-8-String auf die angegebene Anzahl von Bytes

C# – Kürzen Sie einen UTF-8-String auf die angegebene Anzahl von Bytes

Hier ist der einfachste Weg, einen UTF-8-String effizient auf die angegebene Anzahl von Bytes zu kürzen:

public static string TrimToByteLength(this string input, int byteLength)
{
	if (string.IsNullOrEmpty(input))
		return input;
	
	var currentBytes = Encoding.UTF8.GetByteCount(input);
	if (currentBytes <= byteLength)
		return input;

	//Are we dealing with all 1-byte chars? Use substring(). This cuts the time in half.
	if (currentBytes == input.Length)
		return input.Substring(0, byteLength);

	var bytesArray = Encoding.UTF8.GetBytes(input);
	Array.Resize(ref bytesArray, byteLength);
	var wordTrimmed = Encoding.UTF8.GetString(bytesArray, 0, byteLength);

	//If a multi-byte sequence was cut apart at the end, the decoder will put a replacement character '�'
	//so trim off the potential trailing '�'
	return wordTrimmed.TrimEnd('�');
}
Code language: C# (cs)

Eine UTF-8-Zeichenfolge kann aus einer Mischung von Zeichen zwischen 1 und 4 Byte bestehen. Wenn Sie nur einen Teil des Byte-Arrays verwenden, schneiden Sie möglicherweise Multibyte-Zeichen in zwei Hälften, die dann beim Dekodieren durch das Ersetzungszeichen ( ‘�’ ) ersetzt werden. Aus diesem Grund wird das nachgestellte Ersatzzeichen abgeschnitten.

Es gibt andere Ansätze, wie Schleifen und selbst auf ungültige Multibyte-Sequenzen zu prüfen, aber das führt zu schwerer verständlichem und nicht effizienterem Code (laut Benchmarks mit 1 Million Zeichenketten). Darüber hinaus ist eine der besten Optimierungen, die Sie vornehmen können, die Verwendung von string.Substring(), wenn Sie nur mit 1-Byte-Zeichen arbeiten. Das führt zu einer 2-fachen Beschleunigung.

In diesem Artikel werde ich näher darauf eingehen, wie man mit halbierten Multibyte-Zeichen umgeht. Am Ende zeige ich alle Komponententests, die verwendet wurden, um zu beweisen, dass die Methode TrimToByteLength() funktioniert.

Umgang mit einem Multi-Byte-Zeichen, das halbiert wurde

Wenn Sie nur mit 1-Byte-Zeichen umgehen müssen, wäre das Trimmen des Byte-Arrays kein Problem. In der Tat, wenn das der Fall wäre, könnten Sie einfach string.Substring() anstelle von Codierung/Decodierung verwenden.

Aber UTF-8-kodierte Zeichen können zwischen 1-4 Bytes haben. Da Sie basierend auf der Bytelänge trimmen, können Sie am Ende einen Teil eines Multibyte-Zeichens halbieren.

Nehmen wir zum Beispiel an, Sie haben die folgende Zeichenfolge mit einem japanischen Zeichen „か“. In UTF-8 ist dies ein Multibyte-Zeichen mit den folgenden drei Bytes:

11100011 10000001 10001011Code language: plaintext (plaintext)

Nehmen wir nun an, Sie trimmen dies auf nur 2 Bytes. Dies würde die ersten beiden Bytes verlassen:

11100011 10000001

Dies ist eine ungültige Sequenz, und standardmäßig würde der Decoder sie durch das Ersetzungszeichen „�“ ersetzen.

Jeder Code, der versucht, eine Zeichenfolge auf eine bestimmte Bytelänge zu kürzen, muss mit diesem Problem umgehen. Sie können entweder selbst versuchen, die ungültige Multi-Byte-Sequenz zu erkennen, indem Sie das Byte-Array rückwärts durchgehen und die Bytes untersuchen, oder Sie können den Decoder die Arbeit für Sie erledigen lassen und einfach das Ersetzungszeichen am Ende entfernen. Der in diesem Artikel gezeigte Code verfolgt den letzteren Ansatz, da es viel einfacher ist, das Rad nicht neu zu erfinden.

Wie wird die ungültige Multibyte-Sequenz erkannt?

UTF-8 wurde entwickelt, um anhand des folgenden Schemas bestimmen zu können, zu welchem ​​Zeichen ein Byte gehört:

1. Byte beginnt mit 2. Byte beginnt mit 3. Byte beginnt mit Viertes Byte beginnt mit
1-Byte-Zeichen 0
2-Byte-Zeichen 110 10 10
3-Byte-Zeichen 1110 10 10
4-Byte-Zeichen 11110 10 10 10

Das erste Byte in der Sequenz gibt an, um welche Art von Sequenz es sich handelt, was Ihnen sagt, wie viele Fortsetzungsbytes zu suchen. Fortsetzungsbytes beginnen mit 10 .

Kommen wir zurück zum Byte-Array mit dem japanischen Zeichen „か“:

11100011 10000001 10001011

Wenn dies auf 2 Bytes gekürzt wird:

11100011 10000001

Wenn der Decoder dies durchläuft, sieht er, dass das erste Byte in der Sequenz mit 111, beginnt was bedeutet, dass es sich um eine 3-Byte-Sequenz handelt. Es erwartet, dass die nächsten zwei Bytes Fortsetzungsbytes sind (Bytes, die mit 10 beginnen ), aber es sieht nur das eine Fortsetzungsbyte (10 000001). Daher ist dies eine ungültige Bytefolge und wird durch das Ersetzungszeichen „�“ ersetzt.

Weitere Beispiele für Zeichen und ihre UTF-8-Bytesequenzen

Hier sind weitere Beispiele für Zeichen und ihre Byte-Folgen.

Zeichen Unicode Bytefolge
ein U+0061 0 1100001
Ć U+0106 11 000100 10 000110
ꦀ (javanesisches Zeichen) U+A980 111 01010 10 100110 10 000000
𒀃 (sumerisches Keilschriftzeichen) U+12003 1111 0000 10 010010 10 000000 10 000011

Beachten Sie das Muster in den Bytefolgen. Die ersten 4 Bits des ersten Bytes geben die gewünschte Sequenz an, gefolgt von Fortsetzungsbytes (die alle mit 10 beginnen ).

Einheitentests

Die TrimToByteLength()-Methode wurde mit den folgenden parametrisierten Komponententests getestet. Dadurch wird jedes Szenario durchgespielt, einschließlich der Überprüfung, was passiert, wenn Multibyte-Sequenzen zerhackt werden.

[TestClass()]
public class TrimToByteLengthTests
{
	[DataRow(null)]
	[DataRow("")]
	[TestMethod()]
	public void WhenEmptyOrNull_ReturnsAsIs(string input)
	{
		//act
		var actual = input.TrimToByteLength(10);

		//assert
		Assert.AreEqual(input, actual);
	}
	[DataRow("a")] //1 byte
	[DataRow("Ć")] //2 bytes
	[DataRow("ꦀ")] //3 bytes - Javanese
	[DataRow("𒀃")] //4 bytes - Sumerian cuneiform
	[DataRow("a𒀃")] //5 bytes
	[TestMethod()]
	public void WhenSufficientLengthAlready_ReturnsAsIs(string input)
	{
		//act
		var actual = input.TrimToByteLength(byteLength: 5);

		//assert
		Assert.AreEqual(input, actual);
	}
	[DataRow("abc", 1, "a")] //3 bytes, want 1
	[DataRow("abĆ", 2, "ab")] //4 bytes, want 2
	[DataRow("aꦀ", 1, "a")] //4 bytes, want 1
	[DataRow("a𒀃c", 5, "a𒀃")] //6 bytes, want 5
	[DataRow("aĆ𒀃", 3, "aĆ")] //7 bytes, want 3
	[TestMethod()]
	public void WhenStringHasTooManyBytes_ReturnsTrimmedString(string input, int byteLength, string expectedTrimmedString)
	{
		//act
		var actual = input.TrimToByteLength(byteLength);

		//assert
		Assert.AreEqual(expectedTrimmedString, actual);
	}
	[DataRow("Ć", 1, "")] //2 byte char, cut in half
	[DataRow("ꦀ", 2, "")] //3 byte char, cut at 3rd byte
	[DataRow("ꦀ", 1, "")] //3 byte char, cut at 2nd byte
	[DataRow("𒀃", 3, "")] //4 byte char, cut at 4th byte
	[DataRow("𒀃", 2, "")] //4 byte char, cut at 3rd byte
	[DataRow("𒀃", 1, "")] //4 byte char, cut at 2nd byte
	[DataRow("a𒀃", 2, "a")] //1 byte + 4 byte char. Multi-byte cut in half
	[TestMethod()]
	public void WhenMultiByteCharSequenceIsCutInHalf_ItAndReplacementCharAreTrimmedOut(string input, int byteLength, string expectedTrimmedString)
	{
		//act
		var actual = input.TrimToByteLength(byteLength);

		//assert
		Assert.AreEqual(expectedTrimmedString, actual);
	}
}
Code language: C# (cs)