2

I'm building a string based on an IEnumerable, and doing something like this:

public string BuildString()
{
    var enumerable = GetEnumerableFromSomewhere(); // actually an in parameter,
                                                   // but this way you don't have to care
                                                   // about the type :)

    var interestingParts = enumerable.Select(v => v.TheInterestingStuff).ToArray();

    stringBuilder.Append("This is it: ");

    foreach(var part in interestingParts)
    {
        stringBuilder.AppendPart(part);

        if (part != interestingParts.Last())
        {
            stringBuilder.Append(", ");
        }
    }
}

private static void AppendPart(this StringBuilder stringBuilder, InterestingPart part)
{
    stringBuilder.Append("[");
    stringBuilder.Append(part.Something");
    stringBuilder.Append("]");

    if (someCondition(part)) 
    {
         // this is in reality done in another extension method,
         // similar to the else clause
         stringBuilder.Append(" = @");
         stringBuilder.Append(part.SomethingElse");
    }
    else
    {
         // this is also an extension method, similar to this one
         // it casts the part to an IEnumerable, and iterates over
         // it in much the same way as the outer method.
         stringBuilder.AppendInFilter(part);
    }
}

I'm not entirely happy with this idiom, but I'm struggling to formulate something more succinct.

This is, of course, part of a larger string building operation (where there are several blocks similar to this one, as well as other stuff in between) - otherwise I'd probably drop the StringBuilder and use string.Join(", ", ...) directly.

My closest attempt at simplifying the above, though, is constructs like this for each iterator:

stringBuilder.Append(string.Join(", ", propertyNames.Select(prop => "[" + prop + "]")));

but here I'm still concatenating strings with +, which makes it feel like the StringBuilder doesn't really contribute much.

How could I simplify this code, while keeping it efficient?

Gilad Green
  • 36,708
  • 7
  • 61
  • 95
Tomas Aschan
  • 58,548
  • 56
  • 243
  • 402
  • You can use `prop => $"[{prop}]"` or `prop => string.Format("[{0}]", prop)` – Anton Oct 14 '16 at 06:36
  • If your goal is to reduce memory allocations, then you can't really simplify this code, this is how the StringBuilder works. If the content of the `foreach` loop is much bigger than you should split it into small methods for readability, but that's about all you can do – Kevin Gosse Oct 14 '16 at 06:37

4 Answers4

2

You can replace this:

string.Join(", ", propertyNames.Select(prop => "[" + prop + "]"))

With c# 6 string interpolation:

string.Join(", ", propertyNames.Select(prop => $"[{prop}]"))

In both cases the difference is semantic only and it doesn't really matter. String concatenation like in your case in the select isn't a problem. The compiler still creates only 1 new string for it (and not 4, one for each segment and a 4th for the joint string).

Putting it all together:

var result = string.Join(", ", enumerable.Select(v => $"[{v.TheInterestingStuff}]"));

Because body of foreach is more complex that to fit in a String Interpolation scope you can just remove the last N characters of the string once calculated, as KooKiz suggested.

string separator = ", ";
foreach(var part in interestingParts)
{
    stringBuilder.Append("[");
    stringBuilder.Append(part);
    stringBuilder.Append("]");

    if (someCondition(part)) 
    {
        // Append more stuff
    }
    else
    {
        // Append other thingd
    }
    stringBuilder.Append(separator);
}
stringBuilder.Length = stringBuilder.Lenth - separator;

In any case I think that for better encapsulation the content of the loop's scope should sit in a separate function that will receive a part and the separator and will return the output string. It can also be an extension method for StringBuilder as suggested by user734028

Gilad Green
  • 36,708
  • 7
  • 61
  • 95
  • That's still one string created where there could be zero. In most cases it wouldn't matter, but OP's requirements are unclear – Kevin Gosse Oct 14 '16 at 06:39
  • Check with string.IsNullOrEmpty or string.IsNullOrWhitespace and you can calculate capacity to reduce memory allocations in StringBuilder – Anton Oct 14 '16 at 06:45
  • 1
    I know that my strings will never be null, so that's a non-issue. However, the body is usually too complex to replace with string interpolation. – Tomas Aschan Oct 14 '16 at 06:47
  • @TomasLycken - May you please show a more complex version of the body? So I can adjust my answer? – Gilad Green Oct 14 '16 at 06:48
  • String interpolation uses `String.Format` in background, where `string.Format` uses cached instance of `StringBuilder` in background. – Fabio Oct 14 '16 at 07:01
  • @GiladGreen: See my edit to the question. I made the examples much more like my real case - I hope it helps illustrate the complexity. – Tomas Aschan Oct 14 '16 at 07:07
  • `"[" + prop + "]"` is not identical to `$"[{prop}]"` – Maarten Oct 14 '16 at 07:59
0

Aggregate solution:

var answer = interestingParts.Select(v => "[" + v + "]").Aggregate((a, b) => a + ", " + b);

Serialization solution:

var temp = JsonConvert.SerializeObject(interestingParts.Select(x => new[] { x }));
var answer = temp.Substring(1, temp.Length - 2).Replace(",", ", ");
Slava Utesinov
  • 13,410
  • 2
  • 19
  • 26
  • 3
    I wish people stopped using `Aggregate` for concatenation (damn you Resharper). It's not more readable than `string.Join` and is **way** less efficient – Kevin Gosse Oct 14 '16 at 06:41
  • I love Aggregate, for purposes where it's appropriate. This isn't one of them :) – Tomas Aschan Oct 14 '16 at 06:46
  • @KooKiz, Resharper? You think it is impossible to use Aggregate without it's help? – Slava Utesinov Oct 14 '16 at 06:47
  • @SlavaUtesinov Haha no, just that I've seen many developers blindly replace a loop by an Aggregate without thinking, just because Resharper suggested it, even though they would never have considered it – Kevin Gosse Oct 14 '16 at 06:49
0

Use Aggregate extension method with StringBuilder.
Will be more efficiently then concatenate strings if your collection are big

        var builder = new StringBuilder();
        list.Aggregate(builder, (sb, person) =>
        {
            sb.Append(",");
            sb.Append("[");
            sb.Append(person.Name);
            sb.Append("]");
            return sb;
        });
        builder.Remove(0, 1); // Remove first comma

As pure foreach is always more efficient then LINQ then just change logic for delimeter comma

var builder = new StringBuilder();
foreach(var part in enumerable.Select(v => v.TheInterestingStuff))
{
    builder.Append(", ");
    builder.Append("[");
    builder.Append(part);
    builder.Append("]");
}

builder.Remove(0, 2); //Remove first comma and space
Fabio
  • 31,528
  • 4
  • 33
  • 72
  • How is this better than using a for loop and not special-casing the last element? – Tomas Aschan Oct 14 '16 at 06:46
  • Only because you don't use special-casing for last element. One operation less per loop – Fabio Oct 14 '16 at 06:47
  • @Fabio When writing an algorithm to trim a separator, it's more efficient to put the separator at the end. At the beginning, the StringBuilder needs to reallocate the chunk. At the end, it's just a matter of changing the length. In fact, you can do it yourself directly: `builder.Length -= 2;` – Kevin Gosse Oct 14 '16 at 07:01
0

the code:

public string BuildString()
{
    var enumerable = GetEnumerableFromSomewhere();
    var interestingParts = enumerable.Select(v => v.TheInterestingStuff).ToArray();

    stringBuilder.Append("This is it: ");

    foreach(var part in interestingParts)
    {
        stringBuilder.AppendPart(part)

    }
    if (stringBuilder.Length>0)
        stringBuilder.Length--;
}

private static void AppendPart(this StringBuilder stringBuilder, InterestingPart part)
{
    if (someCondition(part)) 
    {
        stringBuilder.Append(string.Format("[{0}] = @{0}", part.Something));         

    }
    else
    {
         stringBuilder.Append(string.Format("[{0}]", part.Something));
         stringBuilder.AppendInFilter(part); //
    }
}

much better now IMO.

Now a little discussion on making it very fast. We can use Parallel.For. But you would think (if you would think) the Appends are all happening to a single shareable resource, aka the StringBuilder, and then you would have to lock it to Append to it, not so efficient! Well, if we can say that each iteration of the for loop in the outer function creates one single string artifact, then we can have a single array of string, allocated to the count of interestingParts before the Parallel for starts, and each index of the Parallel for would store its string to its respective index.

Something like:

string[] iteration_buckets = new string[interestingParts.Length];
System.Threading.Tasks.Parallel.For(0, interestingParts.Length,
    (index) =>
    {
        iteration_buckets[index] = AppendPart(interestingParts[index]);
    });

your function AppendPart will have to be adjusted to make it a non-extension to take just a string and return a string. After the loop ends you can do a string.Join to get a string, which is what you may be doing with the stringBuilder.ToString() too.

user734028
  • 1,021
  • 1
  • 9
  • 21