Una delle differenze tra il vecchio (e a me non troppo caro) VB6 ed il mondo .NET sta nella gestione delle stringhe, in particolare nella concatenazione delle stringhe; se nel vecchio VB6 la concatenazione si esprimeva con la sintassi
stringaA = stringaB & stringaC
dove stringaA
e stringaB
potrebbero anche coincidere; in VB.NET si scrive
stringaA = stringaB + stringaC
(in realtà volendo è ancora possibile usare l’&
per esprimere la concatenazione, ma personalmente preferisco usare il segno +
, coerentemente con tutti gli altri tipi .NET).
Tuttavia questo modo di concatenare le stringhe non è molto performante, perlomeno quando si tratta di concatenare un numero elevato di stringhe: il Framework.NET infatti alloca un nuovo blocco di memoria ogni volta che la stringa viene modificata; per ovviare a questo problema abbiamo a disposizione nella namespace System.Text
la classe StringBuilder
da usare proprio per questi scopi. Per testare le differenze tra i vari metodi, creiamo un semplice progetto di tipo Console Application:
Imports System.Text Module Module1 Sub Main() Const tries As Integer = 1000 Dim index As Integer Dim startTime As DateTime Dim diff As TimeSpan Debug.Write("Number of tries: ") Debug.WriteLine(tries.ToString) ' *** String startTime = Now Dim testString As String For index = 1 To tries testString = testString + "@" Next diff = Now.Subtract(startTime) Debug.Write("String: ") Debug.WriteLine(diff.ToString) ' *** IndexToString startTime = Now Dim testIndexToString As String For index = 1 To tries testIndexToString = testIndexToString + index.ToString Next diff = Now.Subtract(startTime) Debug.Write("IndexToString: ") Debug.WriteLine(diff.ToString) ' *** String.Format startTime = Now Dim testStringFormat As String For index = 1 To tries testString = testString + String.Format("{0}", index) Next diff = Now.Subtract(startTime) Debug.Write("String.Format: ") Debug.WriteLine(diff.ToString) ' *** StringBuilder.AppendString startTime = Now Dim testBuilderAppendString As New StringBuilder For index = 1 To tries testBuilderAppendString.Append("abcdef") Next diff = Now.Subtract(startTime) Debug.Write("StringBuilder.AppendString: ") Debug.WriteLine(diff.ToString) ' *** StringBuilder.Append startTime = Now Dim testBuilderAppendArgument As New StringBuilder For index = 1 To tries testBuilderAppendArgument.Append(index) Next diff = Now.Subtract(startTime) Debug.Write("StringBuilder.AppendArgument: ") Debug.WriteLine(diff.ToString) ' *** StringBuilder.Append startTime = Now Dim testBuilderAppendFormat As New StringBuilder For index = 1 To tries testBuilderAppendFormat.AppendFormat("{0}", index) Next diff = Now.Subtract(startTime) Debug.Write("StringBuilder.AppendFormat: ") Debug.WriteLine(diff.ToString) End Sub End Module
Questo semplice programmino di test produce sul mio computer di sviluppo (Pentium 4 2.8GHz, 2GB RAM) il seguente output:
Number of tries: 1000 String: 00:00:00 IndexToString: 00:00:00 String.Format: 00:00:00.0161025 StringBuilder.AppendString: 00:00:00 StringBuilder.AppendArgument: 00:00:00 StringBuilder.AppendFormat: 00:00:00
Incrementando il valore di tries prima a 10.000 e poi a 50.000, otterremmo i seguenti risultati:
Number of tries: 10000 String: 00:00:00.0805125 IndexToString: 00:00:00.3163560 String.Format: 00:00:00.6441000 StringBuilder.AppendString: 00:00:00 StringBuilder.AppendArgument: 00:00:00.0161025 StringBuilder.AppendFormat: 00:00:00.0161025 Number of tries: 50000 String: 00:00:02.7535275 IndexToString: 00:00:22.0289897 String.Format: 00:00:32.7846900 StringBuilder.AppendString: 00:00:00 StringBuilder.AppendArgument: 00:00:00.0322050 StringBuilder.AppendFormat: 00:00:00.0483075
Devo premettere che questo test non ha un rigore scientifico assoluto: ripetendo più volte gli stessi test con lo stesso numero di cicli è possibile ottenere valori leggermente diversi; quello che però non cambia tra un’esecuzione e l’altra è la “classifica” se così si può dire in termini di velocità di esecuzione tra le varie soluzioni possibili.
Come possiamo notare, per un numero sufficientemente piccolo di iterazioni le differenze non sono poi così rilevanti; ma se il numero di cicli cresce, crescono anche le differenze soprattutto tra l’utilizzo della StringBuilder
, sempre molto veloce, e l’uso della String
, decisamente più lenta.
Il motivo di tale lentezza è dovuto al fatto che il Framework ad ogni ciclo alloca una nuova stringa nell’heap contenente il risultato della concatenazione e poi assegna il risultato alla stringa originale; la StringBuilder
invece ha una gestione molto più efficiente della memoria.
Tra le varie proprietà della classe StringBuilder
vi sono la Length
, che specifica la dimensione attuale della stringa contenibile, e MaxCapacity
, che specifica la dimensione massima allocabile.
La classe prevede diversi costruttori: quello senza parametri alloca una dimensione predefinita (Length
) pari a 16 caratteri e una dimensione massima (MaxCapacity
) pari a 2,147,483,647; nel caso dovessimo sforare la dimensione attuale, la classe allocherebbe nuova memoria, purché entro il limite pari alla dimensione massima.
La cosa migliore, quando è possibile determinarla, è usare il costruttore che richiede come parametro la dimensione reale (Length
) così da evitare nuove allocazioni; le differenze prestazionali comunque non sono così penalizzanti nel caso non sia determinabile a priori la dimensioni massima.
String
e StringBuilder
sono due classi del Framework non interscambiabili tra loro: per utilizzare il risultato delle StringBuilder
, dovremo usarne il metodo ToString
:
Const tries As Integer = 1000 Dim index As Integer Dim builder As New StringBuilder(tries) Dim result As String For index = 1 To tries builder.Append("@") Next result = builder.ToString
Un ultima cosa…
Un’ultima cosa: la maniera più efficiente per inizializzare una stringa vuota, consiste nello scrivere la riga:
Dim myString As String = String.Empty
Infatti scrivendo
Dim myString As String = ""
il Framework allocherebbe una variabile di tipo String
cui assegnerebbe il valore String.Empty
e che poi verrebbe assegnata a myString. Anche qui la differenza nel caso di una ricorrenza singola è risibile, mentre potrebbe essere più rilevante all’interno di loop sostanziosi.