C# String Join: Syntax, Overloads, and Practical Usage
c# string join: Learn how to use string.Join in C# to concatenate collections, handle separators, and choose the right overload for performance and readability.
When you need to produce a comma-separated list from an array, string.Join is the most direct tool in C#. The c# string join operation takes a separator and a sequence of strings, then returns a single concatenated string. For example:
string[] names = { "Alice", "Bob", "Carol" }; string result = string.Join(", ", names); // result: "Alice, Bob, Carol"
This method is part of the .NET base class library and provides several overloads that accept arrays, IEnumerable<T>, and other collection types. This article covers the syntax, overloads, and practical usage of string.Join for common developer scenarios, including performance considerations and common mistakes.
Understanding the string.Join Overloads
string.Join has multiple overloads, each designed for a specific input type. The most commonly used are:
string.Join(string separator, params string[] value)– joins an array of strings.string.Join(string separator, IEnumerable<string> values)– joins any sequence of strings, such asList<string>or a LINQ result.string.Join<T>(string separator, IEnumerable<T> values)– joins a sequence of objects, callingToString()on each element.string.Join(char separator, params string[] value)andstring.Join(char separator, IEnumerable<string> values)– accept acharseparator instead of a string.
The generic overload is particularly useful when you have a collection of non-string types, like integers or custom objects. For example:
List<int> numbers = new List<int> { 1, 2, 3 }; string result = string.Join(", ", numbers); // result: "1, 2, 3"
Here, the compiler infers T as int and calls int.ToString() for each element. This avoids manual Select calls when you simply need a readable representation.
Joining Arrays, Lists, and Other Collections
The most common use case is joining an array or list of strings. The method works with any IEnumerable<string>, so you can pass a List<string>, a HashSet<string>, or the result of a LINQ query directly.
List<string> words = new List<string> { "one", "two", "three" }; string joined = string.Join(" - ", words); // joined: "one - two - three"
You can also join a filtered sequence without creating an intermediate list:
string[] files = Directory.GetFiles(@"C:\temp"); string csv = string.Join(",", files.Where(f => f.EndsWith(".txt")));
This is efficient because string.Join enumerates the sequence once and builds the result in a single pass.
Choosing a Separator: String vs Char
If your separator is a single character, you can use the char overload to avoid allocating a string object for the separator. This is a micro-optimization, but it makes the intent clearer:
string[] items = { "apple", "banana", "cherry" }; string withChar = string.Join(',', items); // char overload string withString = string.Join(",", items); // string overload
Both produce the same output. The char overload is slightly more efficient because it avoids creating a string for the separator, but the difference is negligible in most applications. Choose the overload that best matches the data you have.
Handling Null and Empty Elements
string.Join does not throw when an element is null; it treats null as an empty string. This behavior is consistent across all overloads. For example:
string[] values = { "a", null, "b" }; string result = string.Join(",", values); // result: "a,,b"
If you need to skip null or empty elements, you must filter them explicitly before calling string.Join. A common pattern is:
string result = string.Join(",", values.Where(s => !string.IsNullOrEmpty(s)));
This is important when building CSV or log lines where empty fields might cause parsing issues.
Performance Considerations: string.Join vs StringBuilder
A frequent question is whether string.Join is faster than manually building a string with StringBuilder. For simple joins of a known collection, string.Join is usually the better choice. It internally calculates the total length of the result and allocates the final string exactly once, avoiding the repeated reallocation that occurs when using += in a loop.
StringBuilder is still useful when you need to build a string incrementally with conditional logic, or when the number of elements is not known in advance and you are appending in multiple steps. However, for a straightforward join of a collection, string.Join is more readable and typically performs just as well.
If you are joining a very large collection, consider that string.Join will allocate a single large string. If memory is a concern, you might stream the output instead, but that is rarely necessary.
Common Mistakes and Pitfalls
One common mistake is using a loop with += to concatenate strings, which creates many intermediate strings and can hurt performance. string.Join avoids this problem.
Another pitfall is forgetting that the separator is placed between elements, not after each one. This is usually what you want, but it can be surprising when you need a trailing separator for a CSV header.
When using the generic overload with custom objects, be aware that ToString() is called on each element. If your object's ToString() returns null, it will be treated as an empty string. Override ToString() if you need a specific representation.
Using string.Join with Non-String Collections
The generic overload is especially handy when you have a collection of numbers or custom types. You can also use LINQ to transform elements before joining:
var products = new List<Product> { ... }; string names = string.Join("; ", products.Select(p => p.Name));
This gives you full control over the formatting without writing a loop. For simple cases, the generic overload is sufficient:
double[] values = { 1.5, 2.5, 3.5 }; string result = string.Join(" | ", values); // result: "1.5 | 2.5 | 3.5"
The generic overload calls ToString() on each element, so you get a culture-aware representation by default. If you need a specific format, use Select with a format string.