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.
Table of Contents
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.17
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 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.