Best way to convert IEnumerable<char> to string?
Why isn't it possible to use fluent language on string
?
For example:
var x = "asdf1234";
var y = new string(x.TakeWhile(char.IsLetter).ToArray());
Isn't there a better way to convert IEnumerable<char>
to string
?
Here is a test I've made:
class Program
{
static string input = "asdf1234";
static void Main()
{
Console.WriteLine("1000 times:");
RunTest(1000, input);
Console.WriteLine("10000 times:");
RunTest(10000,input);
Console.WriteLine("100000 times:");
RunTest(100000, input);
Console.WriteLine("100000 times:");
RunTest(100000, "ffff57467");
Console.ReadKey();
}
static void RunTest( int times, string input)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < times; i++)
{
string output = new string(input.TakeWhile(char.IsLetter).ToArray());
}
sw.Stop();
var first = sw.ElapsedTicks;
sw.Restart();
for (int i = 0; i < times; i++)
{
string output = Regex.Match(input, @"^[A-Z]+",
RegexOptions.IgnoreCase).Value;
}
sw.Stop();
var second = sw.ElapsedTicks;
var regex = new Regex(@"^[A-Z]+",
RegexOptions.IgnoreCase);
sw.Restart();
for (int i = 0; i < times; i++)
{
var output = regex.Match(input).Value;
}
sw.Stop();
var third = sw.ElapsedTicks;
double percent = (first + second + third) / 100;
double p1 = ( first / percent)/ 100;
double p2 = (second / percent )/100;
double p3 = (third / percent )/100;
Console.WriteLine("TakeWhile took {0} ({1:P2}).,", first, p1);
Console.WriteLine("Regex took {0}, ({1:P2})." , second,p2);
Console.WriteLine("Preinstantiated Regex took {0}, ({1:P2}).", third,p3);
Console.WriteLine();
}
}
Result:
1000 times:
TakeWhile took 11217 (62.32%).,
Regex took 5044, (28.02%).
Preinstantiated Regex took 1741, (9.67%).
10000 times:
TakeWhile took 9210 (14.78%).,
Regex took 32461, (52.10%).
Preinstantiated Regex took 20669, (33.18%).
100000 times:
TakeWhile took 74945 (13.10%).,
Regex took 324520, (56.70%).
Preinstantiated Regex took 172913, (30.21%).
100000 times:
TakeWhile took 74511 (13.77%).,
Regex took 297760, (55.03%).
Preinstantiated Regex took 168911, (31.22%).
Conclusion: I'm doubting what's better to prefer, I think I'm gonna go on the TakeWhile
which is the slowest only on first run.
Anyway, my question is if there is any way to optimize the performance by restringing the result of the TakeWhile
function.
Solution 1:
How about this to convert IEnumerable<char>
to string
:
string.Concat(x.TakeWhile(char.IsLetter));
Solution 2:
Edited for the release of .Net Core 2.1
Repeating the test for the release of .Net Core 2.1, I get results like this
1000000 iterations of "Concat" took 842ms.
1000000 iterations of "new String" took 1009ms.
1000000 iterations of "sb" took 902ms.
In short, if you are using .Net Core 2.1 or later, Concat
is king.
I've made this the subject of another question but more and more, that is becoming a direct answer to this question.
I've done some performance testing of 3 simple methods of converting an IEnumerable<char>
to a string
, those methods are
new string
return new string(charSequence.ToArray());
Concat
return string.Concat(charSequence)
StringBuilder
var sb = new StringBuilder();
foreach (var c in charSequence)
{
sb.Append(c);
}
return sb.ToString();
In my testing, that is detailed in the linked question, for 1000000
iterations of "Some reasonably small test data"
I get results like this,
1000000 iterations of "Concat" took 1597ms.
1000000 iterations of "new string" took 869ms.
1000000 iterations of "StringBuilder" took 748ms.
This suggests to me that there is not good reason to use string.Concat
for this task. If you want simplicity use the new string approach and if want performance use the StringBuilder.
I would caveat my assertion, in practice all these methods work fine, and this could all be over optimization.
Solution 3:
Assuming that you're looking predominantly for performance, then something like this should be substantially faster than any of your examples:
string x = "asdf1234";
string y = x.LeadingLettersOnly();
// ...
public static class StringExtensions
{
public static string LeadingLettersOnly(this string source)
{
if (source == null)
throw new ArgumentNullException("source");
if (source.Length == 0)
return source;
char[] buffer = new char[source.Length];
int bufferIndex = 0;
for (int sourceIndex = 0; sourceIndex < source.Length; sourceIndex++)
{
char c = source[sourceIndex];
if (!char.IsLetter(c))
break;
buffer[bufferIndex++] = c;
}
return new string(buffer, 0, bufferIndex);
}
}
Solution 4:
Why isn't it possible to use fluent language on string?
It is possible. You did it in the question itself:
var y = new string(x.TakeWhile(char.IsLetter).ToArray());
Isn't there a better way to convert
IEnumerable<char>
to string?
(My assumption is:)
The framework does not have such a constructor because strings are immutable, and you'd have to traverse the enumeration twice in order to pre-allocate the memory for the string. This is not always an option, especially if your input is a stream.
The only solution to this is to push to a backing array or StringBuilder
first, and reallocate as the input grows. For something as low-level as a string, this probably should be considered too-hidden a mechanism. It also would push perf problems down into the string class by encouraging people to use a mechanism that cannot be as-fast-as-possible.
These problems are solved easily by requiring the user to use the ToArray
extension method.
As others have pointed out, you can achieve what you want (perf and expressive code) if you write support code, and wrap that support code in an extension method to get a clean interface.
Solution 5:
You can very often do better performance-wise. But what does that buy you? Unless this is really the bottle neck for your application and you have measured it to be I would stick to the Linq TakeWhile()
version: It is the most readable and maintainable solution, and that is what counts for most of all applications.
If you really are looking for raw performance you could do the conversion manually - the following was around a factor 4+ (depending on input string length) faster than TakeWhile()
in my tests - but I wouldn't use it personally unless it was critical:
int j = 0;
for (; j < input.Length; j++)
{
if (!char.IsLetter(input[j]))
break;
}
string output = input.Substring(0, j);