Back to Blog
C#

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.

C#String.JoinStringBuilderLINQPerformanceCollections
Illustration of C# string.Join concatenating an array of strings with a comma separator

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 as List<string> or a LINQ result.
  • string.Join<T>(string separator, IEnumerable<T> values) – joins a sequence of objects, calling ToString() on each element.
  • string.Join(char separator, params string[] value) and string.Join(char separator, IEnumerable<string> values) – accept a char separator 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.