Los strings de C# son inmutables: s += "x" no amplía s, sino que crea un string nuevo con los caracteres antiguos más los nuevos, y el string antiguo pasa a ser basura. Una vez no es nada. En un bucle que se ejecuta miles de veces, cada iteración copia todo lo construido hasta ese momento, así que el trabajo total crece con el cuadrado de la longitud.
StringBuilder, de System.Text, es un buffer mutable. Append escribe en el espacio libre del final, ampliando el buffer de vez en cuando, y ToString() crea un solo string al final.
Salida:
Order #1042, total 59.97
Status: shipped
**********
51
Por qué concatenar en un bucle es lento
Aquí está el mismo texto construido de las dos formas. Los tiempos varían según la máquina, así que los números de abajo son solo un ejemplo; lo que importa es la diferencia, que crece a medida que crece el bucle:
Salida de ejemplo:
string += 482 ms, length 188890
StringBuilder 1 ms, length 188890
Con 20.000 llamadas a Append, la versión con += copia en total casi 2.000 millones de caracteres. StringBuilder copia cada carácter unas dos veces, una al añadirlo y otra en ToString(), y por eso se queda cerca de un milisegundo.
Append, AppendLine y AppendFormat
Append tiene sobrecargas para todos los tipos incorporados, y devuelve el mismo builder, así que las llamadas pueden encadenarse. AppendLine añade un salto de línea después de su argumento. AppendFormat recibe un string de formato compuesto como string.Format:
Salida:
RECEIPT
-------
Espresso 2.40
Croissant 3.10
Orange juice 4.50
TOTAL 10.00
Añadir un string interpolado (sb.Append($"...")) está bien y se lee bien. En .NET 6 y posteriores, el compilador incluso escribe las piezas directamente en el builder sin crear el string intermedio.
AppendLine usa Environment.NewLine: \n en Linux y macOS, \r\n en Windows. Cuando un formato de archivo exige un final de línea concreto, añádelo tú.
Insert, Remove, Replace y el indexador
A diferencia de un string, un StringBuilder puede editarse en el sitio:
Salida:
Hello, World
Hello, Maya
Maya
maya
maya!
[] length 0
Poner Length a un valor más pequeño trunca; es la forma más barata de quitar un separador final. Clear() pone la longitud a 0 y conserva el buffer reservado, así que un mismo builder puede reutilizarse en las iteraciones de un bucle exterior.
El indexador es rápido con lo añadido recientemente, pero puede ser lento con accesos aleatorios en un builder muy grande, porque el contenido se guarda en bloques enlazados. Si necesitas leer caracteres, llama una vez a ToString() e indexa el string.
El problema del separador final
Un uso clásico es construir una lista separada por comas. Añadir un separador después de cada elemento deja uno de más al final:
Salida:
csharp, dotnet, linq
csharp, dotnet, linq
Cuando los elementos ya están en una colección, string.Join es más corto y al menos igual de rápido. Reserva StringBuilder para el texto cuya forma depende de la lógica: secciones condicionales, bucles anidados, formato mixto.
Capacidad
Un StringBuilder empieza con espacio para 16 caracteres. Cuando se queda sin espacio, enlaza un nuevo bloque de almacenamiento tan grande como todo lo que ya contiene, hasta 8.000 caracteres por bloque, así que su capacidad se duplica mientras es pequeño y después crece en pasos de 8.000 caracteres. Crecer nunca copia el texto ya guardado. Si sabes más o menos lo largo que será el resultado, pasa la capacidad al constructor y el builder nunca tendrá que crecer:
Salida:
12000
10000
True
True
Reservar el tamaño de antemano es una optimización, no un requisito. Crecer reserva un bloque nuevo pero nunca mueve el texto existente, así que el valor por defecto está bien salvo que construyas muchos strings grandes en un camino crítico.
Cuándo no usar StringBuilder
StringBuilder no es un sustituto más rápido de cualquier +. El compilador ya convierte a + b + c + d dentro de una sola expresión en una única llamada a string.Concat, que calcula la longitud final y copia cada pieza una vez, el mismo trabajo que haría un builder con más código. Usa strings normales cuando:
- El número de piezas es pequeño y fijo:
$"{first} {last} ({age})". - Las piezas están en una colección y necesitan un separador:
string.Join(", ", items). - Las piezas no necesitan separador:
string.Concat(parts). - Concatenas unas pocas veces fuera de cualquier bucle.
Usa StringBuilder cuando añades texto dentro de un bucle, un número de veces que no conoces de antemano, o cuando editas el texto (insertar, sustituir, quitar) mientras lo construyes. Convertir de un lado a otro (sb.ToString() dentro del bucle) tira por la borda la ventaja; llámalo una vez al final.
Los streams son la otra alternativa: para producir un archivo grande, escribe las líneas en un StreamWriter a medida que avanzas en lugar de construir todo el contenido en memoria. Consulta archivos.
Preguntas frecuentes
¿Qué es StringBuilder en C#?
System.Text.StringBuilder es un buffer mutable de caracteres para construir un string en muchos pasos. Append añade al final del buffer en lugar de crear un string nuevo cada vez, y ToString() produce el string final una sola vez. Es la herramienta habitual para construir texto en un bucle.
¿Cuándo debo usar StringBuilder en lugar de concatenar strings?
Cuando añades texto dentro de un bucle o un número desconocido de veces, como al construir un informe, un archivo CSV o HTML a partir de una colección. Para un puñado fijo de piezas (a + b + c, o un solo string interpolado), la concatenación simple es igual de rápida y más fácil de leer, porque el compilador ya la combina en una única llamada a string.Concat.
¿Cómo quito el último carácter de un StringBuilder?
Reduce su Length: if (sb.Length > 0) sb.Length--; quita el último carácter sin copiar nada. sb.Remove(sb.Length - 1, 1) hace lo mismo. Es la forma habitual de quitar una coma final, aunque string.Join evita el separador final desde el principio.
¿Es StringBuilder seguro para hilos?
No. Llamar a Append sobre el mismo StringBuilder desde varios hilos a la vez puede corromper su contenido o lanzar una excepción. Da a cada hilo su propio builder y combina los resultados, o protege el compartido con un lock.
¿Qué añade AppendLine en C#?
El texto seguido de Environment.NewLine, que es \r\n en Windows y \n en Linux y macOS. Si la salida debe usar un final de línea concreto, como \n para un formato de archivo o \r\n para una cabecera HTTP, añádelo de forma explícita con Append("\n").