2011-11-12 6 views
26

Perché non è possibile utilizzare un linguaggio scorrevole su string?Il modo migliore per convertire IEnumerable <char> in stringa?

Ad esempio:

var x = "asdf1234"; 
var y = new string(x.TakeWhile(char.IsLetter).ToArray()); 

Non c'è un modo migliore per convertire IEnumerable<char>-string?

Ecco un test che ho fatto:

class Program 
{ 
    static string input = "asdf1234"; 
    static void Main() 
    { 
    Console.WriteLine("1000 times:"); 
    RunTest(1000, input); 
    Console.WriteLine("10000 times:"); 
    RunTest(10000,input); 
    Console.WriteLine("100000 times:"); 
    RunTest(100000, input); 
    Console.WriteLine("100000 times:"); 
    RunTest(100000, "ffff57467"); 


    Console.ReadKey(); 

    } 

    static void RunTest(int times, string input) 
    { 

    Stopwatch sw = new Stopwatch(); 

    sw.Start(); 
    for (int i = 0; i < times; i++) 
    { 
     string output = new string(input.TakeWhile(char.IsLetter).ToArray()); 
    } 
    sw.Stop(); 
    var first = sw.ElapsedTicks; 

    sw.Restart(); 
    for (int i = 0; i < times; i++) 
    { 
     string output = Regex.Match(input, @"^[A-Z]+", 
     RegexOptions.IgnoreCase).Value; 
    } 
    sw.Stop(); 
    var second = sw.ElapsedTicks; 

    var regex = new Regex(@"^[A-Z]+", 
     RegexOptions.IgnoreCase); 
    sw.Restart(); 
    for (int i = 0; i < times; i++) 
    { 
     var output = regex.Match(input).Value; 
    } 
    sw.Stop(); 
    var third = sw.ElapsedTicks; 

    double percent = (first + second + third)/100; 
    double p1 = (first/percent)/ 100; 
    double p2 = (second/percent)/100; 
    double p3 = (third/percent )/100; 


    Console.WriteLine("TakeWhile took {0} ({1:P2}).,", first, p1); 
    Console.WriteLine("Regex took {0}, ({1:P2})." , second,p2); 
    Console.WriteLine("Preinstantiated Regex took {0}, ({1:P2}).", third,p3); 
    Console.WriteLine(); 
    } 
} 

Risultato:

1000 times: 
TakeWhile took 11217 (62.32%)., 
Regex took 5044, (28.02%). 
Preinstantiated Regex took 1741, (9.67%). 

10000 times: 
TakeWhile took 9210 (14.78%)., 
Regex took 32461, (52.10%). 
Preinstantiated Regex took 20669, (33.18%). 

100000 times: 
TakeWhile took 74945 (13.10%)., 
Regex took 324520, (56.70%). 
Preinstantiated Regex took 172913, (30.21%). 

100000 times: 
TakeWhile took 74511 (13.77%)., 
Regex took 297760, (55.03%). 
Preinstantiated Regex took 168911, (31.22%). 

Conclusione: sto dubitando che cosa è meglio preferire, penso che sto andando andare sul TakeWhile che è il più lento solo al primo tentativo.

In ogni caso, la mia domanda è se c'è un modo per ottimizzare le prestazioni restringendo il risultato della funzione TakeWhile.

+1

Spiegare cosa intendi per "migliore": il più veloce? Meno affamato di memoria? Più facile da capire? – LukeH

+0

@LukeH Ho già preso la mia decisione su cosa scegliere: il più veloce. La mia domanda è se c'è un modo più carino di 'nuova stringa (x.TakeWhile (p) .ToArray)' – Shimmy

+2

@LukeH: Potrebbe voler ripristinare la soluzione: è più veloce della mia con un margine molto ampio – BrokenGlass

risposta

13

Supponendo che si sta cercando principalmente per le prestazioni, poi qualcosa come questo dovrebbe essere sostanzialmente più veloce di qualsiasi dei tuoi esempi:

string x = "asdf1234"; 
string y = x.LeadingLettersOnly(); 

// ... 

public static class StringExtensions 
{ 
    public static string LeadingLettersOnly(this string source) 
    { 
     if (source == null) 
      throw new ArgumentNullException("source"); 

     if (source.Length == 0) 
      return source; 

     char[] buffer = new char[source.Length]; 
     int bufferIndex = 0; 

     for (int sourceIndex = 0; sourceIndex < source.Length; sourceIndex++) 
     { 
      char c = source[sourceIndex]; 

      if (!char.IsLetter(c)) 
       break; 

      buffer[bufferIndex++] = c; 
     } 
     return new string(buffer, 0, bufferIndex); 
    } 
} 
+0

Hmmm, ho appena notato che hai bisogno di lettere solo dall'inizio della stringa, nel qual caso mi aspetterei [risposta di BrokenGlass] (http://stackoverflow.com/questions/8108313/best-way-to-convert-ienumerablechar -to-string/8108584 # 8108584) per essere il più veloce. (Anche in questo caso non ho ancora un benchmark per confermare.) – LukeH

+1

+1 La pre-allocazione del buffer è probabilmente ciò che rende questo più veloce, ma questa è solo un'ipotesi: test limitati mostrano la sua strada più veloce dell'uso di 'Substring()' – BrokenGlass

9

Si può spesso fare una migliore performance-saggio. Ma cosa ti compra? A meno che non si tratti del collo della bottiglia per la tua applicazione e tu l'hai misurato, mi attengo alla versione Linq TakeWhile(): è la soluzione più leggibile e manutenibile, e questo è ciò che conta per la maggior parte delle applicazioni.

Se davvero alla ricerca di prestazioni pure si potrebbe fare la conversione manualmente - il seguente è stato di circa un fattore 4+ (a seconda della lunghezza della stringa di input) più veloce di TakeWhile() nel mio test - ma io non lo uso personalmente a meno che non era critica:

int j = 0; 
for (; j < input.Length; j++) 
{ 
    if (!char.IsLetter(input[j])) 
     break; 
} 
string output = input.Substring(0, j); 
+3

+ 1. E non c'è niente di sbagliato nel confezionarlo in un metodo di supporto di qualche tipo per il riutilizzo. Qualcosa come 'source.LeadingLettersOnly()' sarebbe più leggibile di 'nuova stringa (source.TakeWhile (char.IsLetter) .ToArray())', imo. – LukeH

+1

@LukeH: la tua soluzione è molto più veloce, per favore undelete! – BrokenGlass

+0

La funzione dovrebbe confrontare una query di ricerca con alcuni primi caratteri di una stringa di centinaia (100000), quindi le prestazioni sono tutto ciò che conta. – Shimmy

11

Perché non è possibile usare un linguaggio fluente su stringa?

È possibile. L'hai fatto nella domanda stessa:

var y = new string(x.TakeWhile(char.IsLetter).ToArray()); 

non c'è un modo migliore per convertire IEnumerable<char> a stringa?

(la mia ipotesi è :)

Il quadro non avere un tale costruttore perché le stringhe sono immutabili, e che avrebbe dovuto attraversare l'enumerazione due volte al fine di pre-allocare la memoria per la stringa . Questa non è sempre un'opzione, specialmente se il tuo input è uno stream.

L'unica soluzione è di inviare prima un array di supporto o StringBuilder e riallocarlo man mano che l'input aumenta. Per qualcosa di basso livello come una stringa, questo probabilmente dovrebbe essere considerato un meccanismo troppo nascosto. Inoltre, spingerebbe verso il basso i problemi perf nella classe di stringa incoraggiando le persone a utilizzare un meccanismo che non può essere il più veloce possibile.

Questi problemi sono risolti facilmente richiedendo all'utente di utilizzare il metodo di estensione ToArray.

Come altri hanno sottolineato, è possibile ottenere ciò che si desidera (perf e codice espressivo) se si scrive codice di supporto e avvolgere il codice di supporto in un metodo di estensione per ottenere un'interfaccia pulita.

+0

BTW, La cosa migliore per farlo "fluente", è che ho aggiunto alla mia libreria di estensioni un overload 'Join' che prende un' IEnumerable 'e restituisce' string'. – Shimmy

+6

I downton di Anonymous non aiutano nulla. Indica le tue ragioni e affronterò le tue preoccupazioni. –

31

ne dite di questo convertire IEnumerable<char>-string:

string.Concat(x.TakeWhile(char.IsLetter)); 
+3

+1 Molto breve e non richiede .ToArray() – Alex

+0

Immagino che stringa.Concat usi internamente un StringBuilder. Sarebbe molto strano se non lo facesse. Quindi questa soluzione dovrebbe anche funzionare davvero bene. –

+0

. Solo rete 4.0. Anche se scrivi il tuo .TakeWhile in 3.5, string.Concat (IEnumerable ) non fa quello che ti aspetti. –

13

Ho fatto questo oggetto di another question ma sempre più, che sta diventando una risposta diretta a questa domanda.

Ho fatto qualche test delle prestazioni di 3 semplici metodi di conversione di un IEnumerable<char> ad un string, questi metodi sono

nuova stringa

return new string(charSequence.ToArray()); 

Concat

return string.Concat(charSequence) 

StringBuilder

var sb = new StringBuilder(); 
foreach (var c in charSequence) 
{ 
    sb.Append(c); 
} 

return sb.ToString(); 

Nel mio test, che è dettagliata nella linked question, per 1000000 iterazioni di "Some reasonably small test data" ottengo risultati come questo,

1000000 le iterazioni di "Concat" hanno richiesto 1597 ms.

1000000 iterazioni di "nuova stringa" hanno richiesto 869 ms.

1000000 iterazioni di "StringBuilder" hanno richiesto 748 ms.

Questo mi suggerisce che non ci sono buoni motivi per utilizzare string.Concat per questa attività. Se si desidera la semplicità, utilizzare l'approccio nuova stringa e, se si desidera ottenere prestazioni, utilizzare lo StringBuilder.

Vorrei mettere in guardia la mia affermazione, in pratica tutti questi metodi funzionano bene, e questo potrebbe essere tutto l'ottimizzazione.

+0

Vorrei sacrificare 121 millisecondi per usare 'nuova stringa' al posto di scrivere tre righe aggiuntive di codice per usare' StringBuilder'. #cleanCode. – RBT

4

restituisce una nuova stringa (foo.Select (x => x) .ToArray());