C#:rimuove un set di caratteri da una stringa

C#:rimuove un set di caratteri da una stringa

Il modo più rapido e semplice per rimuovere un insieme di caratteri da una stringa è utilizzare StringBuilder + List, in questo modo:

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)

Ho confrontato questo con altri tre approcci. Ho eseguito 100.000 iterazioni con una stringa di 2500 caratteri e un elenco di 15 caratteri da rimuovere. Questo approccio StringBuilder è quasi 2 volte più veloce del secondo approccio più veloce.

Ecco il riepilogo delle statistiche sulle prestazioni per tutti gli approcci:

Approccio Totale (ms) Media (ms) Min (ms) Massimo (ms)
StringBuilder 4251.91 0,042 0,036 0,42
LINQ + nuova stringa() + ToArray() 7176.47 0,071 0,047 0,74
LINQ + stringa.Concat() 8485.75 0,085 0,059 1.64
Regex 31368.22 0,31 0,25 2.45

Un risultato sorprendente è che List è più veloce di HashSet in ogni approccio che ho confrontato. Tuttavia, in ogni caso, ho utilizzato un elenco di soli 15 caratteri. Con così pochi caratteri, i costi generali di HashSet non superano i suoi vantaggi. All'aumentare del numero di caratteri, mi aspetto che HashSet alla fine superi List.

Nel resto di questo articolo, mostrerò il codice per gli altri approcci che ho confrontato e mostrerò come ho misurato e confrontato le prestazioni.

Altri approcci

Gli approcci seguenti sono più lenti dell'approccio StringBuilder. Gli approcci LINQ possono essere considerati soggettivamente più semplici dell'approccio StringBuilder (se preferisci LINQ rispetto ai cicli foreach).

LINQ + nuova stringa() + ToArray()

Questo utilizza LINQ per filtrare i caratteri, quindi usa new string() + ToArray() per convertire il risultato in una stringa:

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)

Le statistiche sulle prestazioni:

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

LINQ + stringa.Concat()

Questo utilizza LINQ per filtrare i caratteri e quindi utilizza Concat() per convertire il risultato in una stringa:

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)

Le statistiche sulle prestazioni:

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

Regex

L'uso di espressioni regolari per questo problema non è una buona idea. È l'approccio più lento e meno semplice:

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)

Le statistiche sulle prestazioni:

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

Ahi, è lento.

Approccio di confronto delle prestazioni

Per ogni approccio, ho eseguito 100.000 iterazioni e utilizzato una stringa di lunghezza 2500 con un elenco di 15 caratteri da rimuovere.

Ogni volta che si confrontano le prestazioni, è una buona idea controllare i tempi totale, media, minimo e massimo. Non solo fare affidamento sul totale e sulla media. Il minimo e il massimo indicano l'ampiezza della distribuzione dei tempi di esecuzione. Più stretta è la distribuzione, meglio è. Se guardi la tabella di riepilogo delle prestazioni, nota che l'approccio di StringBuilder ha il miglior tempo medio e anche la più stretta distribuzione dei tempi di esecuzione.

La prima esecuzione di qualsiasi codice sarà sempre più lenta delle esecuzioni successive. Quindi, quando si confrontano le prestazioni, è sempre una buona idea "riscaldare" il codice o scartare il primo risultato dell'esecuzione in modo che non distorca notevolmente i risultati. Sto registrando la prima esecuzione (e mostrando che è sempre il massimo), quindi lo scarto.

Ecco il codice che ho usato per testare le prestazioni di ciascun approccio:

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)