C# – Entfernt eine Reihe von Zeichen aus einer Zeichenfolge

C# – Entfernt eine Reihe von Zeichen aus einer Zeichenfolge

Der schnellste und einfachste Weg, eine Reihe von Zeichen aus einer Zeichenfolge zu entfernen, ist die Verwendung von StringBuilder + List, wie folgt:

public static string RemoveChars(string input, List<char> charsToRemove)
{
	if (string.IsNullOrEmpty(input))
		return input;

	var sb = new StringBuilder();

	foreach (var c in input)
	{
		if (!charsToRemove.Contains(c))
			sb.Append(c);
	}

	return sb.ToString();
}
Code language: C# (cs)

Ich habe dies mit drei anderen Ansätzen verglichen. Ich habe 100.000 Iterationen mit einer Zeichenfolge mit 2500 Zeichen und einer Liste von 15 zu entfernenden Zeichen durchgeführt. Dieser StringBuilder-Ansatz ist fast doppelt so schnell wie der zweitschnellste Ansatz.

Hier ist die Zusammenfassung der Leistungsstatistiken für alle Ansätze:

Ansatz Gesamt (ms) Durchschnitt (ms) Minute (ms) Max (ms)
StringBuilder 4251.91 0,042 0,036 0,42
LINQ + neue Zeichenfolge() + ToArray() 7176.47 0,071 0,047 0,74
LINQ + string.Concat() 8485.75 0,085 0,059 1,64
Regex 31368.22 0,31 0,25 2,45

Ein überraschendes Ergebnis ist, dass List bei jedem Ansatz, den ich verglichen habe, schneller ist als HashSet. Allerdings habe ich in jedem Fall eine Liste mit nur 15 Zeichen verwendet. Bei so wenigen Zeichen wiegen die Overhead-Kosten des HashSet seine Vorteile nicht auf. Wenn die Anzahl der Zeichen zunimmt, würde ich erwarten, dass HashSet schließlich List übertrifft.

Im Rest dieses Artikels zeige ich den Code für die anderen Ansätze, die ich verglichen habe, und zeige, wie ich die Leistung gemessen und verglichen habe.

Andere Ansätze

Die folgenden Ansätze sind langsamer als der StringBuilder-Ansatz. Die LINQ-Ansätze können als subjektiv einfacher angesehen werden als der StringBuilder-Ansatz (wenn Sie LINQ gegenüber Foreach-Schleifen bevorzugen).

LINQ + neue Zeichenfolge() + ToArray()

Dies verwendet LINQ, um Zeichen herauszufiltern, und verwendet dann new string() + ToArray(), um das Ergebnis in eine Zeichenfolge umzuwandeln:

public static string RemoveChars(string input, List<char> charsToRemove)
{
	if (string.IsNullOrEmpty(input))
		return input;

	return new string(input.Where(c => !charsToRemove.Contains(c)).ToArray());
}
Code language: C# (cs)

Die Leistungsstatistik:

Total Time: 7176.47ms Avg=0.071ms Min=0.047ms Max=0.74msCode language: plaintext (plaintext)

LINQ + string.Concat()

Dies verwendet LINQ, um die Zeichen zu filtern, und verwendet dann Concat(), um das Ergebnis in einen String umzuwandeln:

public static string RemoveChars(string input, List<char> charsToRemove)
{
	if (string.IsNullOrEmpty(input))
		return input;

	return string.Concat(input.Where(c => !charsToRemove.Contains(c)));
}
Code language: C# (cs)

Die Leistungsstatistik:

Total Time: 8485.75ms Avg=0.085ms Min=0.059ms Max=1.64msCode language: plaintext (plaintext)

Regex

Die Verwendung von Regex für dieses Problem ist keine gute Idee. Es ist der langsamste und am wenigsten einfache Ansatz:

static Regex charsToRemoveRegex = new Regex("[<>?;&*=~^+|:,/m]", RegexOptions.Compiled);

public static string RemoveChars(string input)
{
	if (string.IsNullOrEmpty(input))
		return input;

	return charsToRemoveRegex.Replace(input, "");
}
Code language: C# (cs)

Die Leistungsstatistik:

Total Time: 31368.22ms Avg=0.31ms Min=0.25ms Max=2.45msCode language: plaintext (plaintext)

Autsch, das ist langsam.

Ansatz zum Leistungsvergleich

Für jeden Ansatz habe ich 100.000 Iterationen durchgeführt und eine Zeichenfolge der Länge 2500 mit einer Liste von 15 zu entfernenden Zeichen verwendet.

Wenn Sie die Leistung vergleichen, ist es eine gute Idee, die Gesamt-, Durchschnitts-, Mindest- und Höchstzeiten zu überprüfen. Nicht nur Verlassen Sie sich auf die Summe und den Durchschnitt. Das Minimum und das Maximum geben die Breite der Verteilung der Ausführungszeiten an. Je enger die Verteilung, desto besser. Wenn Sie sich die Leistungsübersichtstabelle ansehen, stellen Sie fest, dass der StringBuilder-Ansatz die beste durchschnittliche Zeit und auch die engste Verteilung der Ausführungszeiten aufweist.

Die erste Ausführung eines beliebigen Codes ist immer langsamer als nachfolgende Ausführungen. Wenn Sie also die Leistung vergleichen, ist es immer eine gute Idee, den Code „aufzuwärmen“ oder das erste Ausführungsergebnis zu verwerfen, damit es die Ergebnisse nicht wesentlich verzerrt. Ich protokolliere die erste Ausführung (und zeige, dass es immer das Maximum ist) und verwerfe sie dann.

Hier ist der Code, den ich verwendet habe, um die Leistung jedes Ansatzes zu testen:

static void Main(string[] args)
{
	List<char> charsToRemove = new List<char>
	{
		'<','>','?',';','&','*',
		'=','~','^', '+','|',':',','
		,'/','m'
	};

	var testSb = new StringBuilder();
	for(int i = 0; i < 100; i++)
	{
		testSb.Append("<>?hello;&*=~world^+|:,/m");
	}
	var testString = testSb.ToString();
	Console.WriteLine(testString.Length);

	List<double> elapsedMS = new List<double>();
	Stopwatch sw = Stopwatch.StartNew();
	for (int i = 0; i < 100_000; i++)
	{
		var cleanedString = RemoveChars(testString.ToString(), charsToRemove);
		elapsedMS.Add(sw.Elapsed.TotalMilliseconds);
		sw.Restart();
	}
	sw.Stop();
	//First() is always much larger and skews the Sum() and Average(). Print it here, but then remove it for the other aggregates
	Console.WriteLine($"First={elapsedMS.First()}ms Max={elapsedMS.First()}ms");
	elapsedMS.RemoveAt(0);
	Console.WriteLine($"Total Time: {elapsedMS.Sum()}ms Avg={elapsedMS.Average()}ms Min={elapsedMS.Min()}ms Max={elapsedMS.Max()}ms");
}
Code language: C# (cs)