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>()
{
	"127.0.0.1",
	"127.0.0.10",
	"127.0.0.17"
};

System.IO.File.WriteAllLines(@"C:\temp\ipAddresses.txt", ipAddresses);
Code language: C# (cs)

This creates (or overwrites) the file and writes each string on a new line. The resulting file looks like this:

127.0.0.1\r\n
127.0.0.10\r\n
127.0.0.17\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 with a separator character, and then use File.WriteAllText().

var ipAddresses = new List<string>()
{
	"127.0.0.1",
	"127.0.0.10",
	"127.0.0.17"
};

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:

127.0.0.1,127.0.0.10,127.0.0.17Code 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 either read the file line by line with File.ReadLines() or read the entire file all at once with File.ReadAllLines(). Here’s an example:

//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 memory-efficient 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