C# – Save a list of strings to a file

The simplest way to save a list of strings to a file is to use File.WriteAllLines().

var ipAddresses = new List<string>() { "", "", "" }; System.IO.File.WriteAllLines(@"C:\temp\ipAddresses.txt", ipAddresses);
Code language: C# (cs)

This creates (or overwrites) the specified file and writes each string on a new line. The resulting file looks like this:\r\n\r\n\r\n
Code language: plaintext (plaintext)

Note: Showing non-printable newline characters \r\n for clarity.

Specifying the separator character

What if you want to separate each string with a comma (or some other separator character of your choice), instead of writing each string on a new line?

To do that, you can join the strings and specify the separator character you want to use, and then use File.WriteAllText().

var ipAddresses = new List<string>() { "", "", "" }; var commaSeparatedIPs = string.Join(',', ipAddresses); System.IO.File.WriteAllText(@"C:\temp\ipAddresses.txt", commaSeparatedIPs);
Code language: C# (cs)

This creates (or overwrites) the specified file, outputting the strings separated with a comma:,,
Code language: plaintext (plaintext)

Reading the strings from a file into a list

When each string is on a new line

To read the strings from a file into a list, you can use File.ReadAllLines(), which returns a string array. If you’re just processing the strings and don’t need to keep them in memory, use File.ReadLines() instead to get an IEnumerable<string>.

//As an array string[] ipAddressesArray = System.IO.File.ReadAllLines(@"C:\temp\ipAddresses.txt"); //As a list using System.Linq; List<string> ipAddresses = System.IO.File.ReadAllLines(@"C:\temp\ipAddresses.txt").ToList(); //As an enumerable if you don't need to keep the strings around IEnumerable<string> ipAddresses = System.IO.File.ReadLines(@"C:\temp\ipAddresses.txt");
Code language: C# (cs)

When the strings are separated with a different character

To get the strings back into a list, you have to read the file and split the string with the separator character.

//As an array string[] ipAddresses = System.IO.File.ReadAllText(@"C:\temp\ipAddresses.txt").Split(','); //As a list using System.Linq; var ipAddresses = System.IO.File.ReadAllText(@"C:\temp\ipAddresses.txt").Split(',').ToList();
Code language: C# (cs)

Notice that this is reading the entire file. This is necessary because there’s no high-level built-in function equivalent to File.ReadLines() that allows you to specify a different separator. If you don’t want to read the entire file into memory at once in this scenario, see the generator method below.

Get an IEnumerable<string> when using a different separator character

If you don’t want to read the entire file into memory, and you’re dealing with non-newline separated characters, then you can use the following ReadStrings() generator method. This reads blocks of characters from the file stream and looks for separator characters. Once a separator is encountered, it yields a string.

using System.IO; public static IEnumerable<string> ReadStrings(string path, char separator) { var sb = new StringBuilder(); using (var sr = new StreamReader(path)) { char[] buffer = new char[1024]; int charsRead = 0; //Keep track of how many chars to copy into StringBuilder int charBlockIndex = 0; int charBlockCount = 0; while (!sr.EndOfStream) { charBlockIndex = 0; charBlockCount = 0; charsRead = sr.Read(buffer, 0, buffer.Length); for (int i = 0; i < charsRead; i++) { if (buffer[i] == separator) { //Once a separator is found, copy block to StringBuilder and yield it sb.Append(buffer, charBlockIndex, charBlockCount); yield return sb.ToString(); sb.Clear(); charBlockIndex = i + 1; charBlockCount = 0; } else { charBlockCount++; } } //Copy remaining chars since separator was found if (charBlockCount > 0) sb.Append(buffer, charBlockIndex, charBlockCount); } if (sb.Length > 0) yield return sb.ToString(); } yield break; }
Code language: C# (cs)

Note: Instead of copying one character at a time to the StringBuilder, when it encounters a separator (or runs out of characters in the buffer), it copies blocks of chars from the buffer to the StringBuilder. This is harder to understand, but improves the performance quite a bit.

Here is the performance comparison between this generator method and the ReadAllText().Split() approach:

| Method | NumStrings | Mean | Allocated | |------------------ |----------- |-----------:|-----------:| | ReadAllText_Split | 10000 | 2.771 ms | 2,562 KB | | Generator | 10000 | 2.291 ms | 947 KB | | ReadAllText_Split | 100000 | 42.998 ms | 25,440 KB | | Generator | 100000 | 22.552 ms | 9,385 KB | | ReadAllText_Split | 1000000 | 419.261 ms | 254,254 KB | | Generator | 1000000 | 235.808 ms | 93,760 KB |
Code language: plaintext (plaintext)

The generator is about 2x faster and allocates far less memory overall. More importantly, the generator minimizes memory usage throughout the lifetime of the process. In the 1 million string test, the generator process used a max of 8 MB, whereas the ReadAllText().Split() process used 200 MB.

Leave a Comment