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.