string is the C# alias for the sealed reference type System.String. Its contents are immutable. Assignment copies a reference, while an operation that changes text produces a different string. That boundary explains most string behavior, including when concatenation is clear and when incremental construction starts wasting allocations.
Core Properties
stringis a sealed reference type.- Strings are immutable in safe managed code.
==andEqualscompare string content, not object identity.
var a = "hello";
var b = a;
b = b + "!";
Console.WriteLine(a); // hello
Console.WriteLine(b); // hello!b = b + "!" creates a new string object. a remains unchanged.
String Interning
String literals can share an interned instance, and identical literals typically do. Object identity is not contractual. Compilation can request NoStringInterning, and runtime behavior may still vary:
var s1 = "dotnet";
var s2 = "dotnet";
Console.WriteLine(object.ReferenceEquals(s1, s2)); // Typically True; identity is not contractualRuntime interning is available through string.Intern(s), and string.IsInterned(s) checks whether an equivalent value is already present. Interned entries are generally retained until the runtime terminates. Feeding the pool high-cardinality dynamic text trades ordinary garbage collection for long-lived retention, usually a bad deal. Compile-time constant concatenations ("a" + "b") are folded by the compiler. Runtime-built strings are not automatically interned.
StringBuilder
StringBuilder fits text assembled through repeated appends, especially in a loop or another hot path.
var sb = new StringBuilder(capacity: 256);
for (var i = 0; i < 5; i++)
{
sb.Append("item-").Append(i).AppendLine();
}
var result = sb.ToString();The usual decision is small:
- Interpolation or concatenation fits small, one-off composition.
StringBuilderfits repeated incremental construction after the allocation cost matters.
Encoding and Unicode
A .NET string stores UTF-16 code units. A char is one 16-bit code unit, not necessarily a complete Unicode scalar value. Characters outside the Basic Multilingual Plane use a surrogate pair, so "👍".Length == 2 and s[0] returns only half of that pair.
Rune and EnumerateRunes() operate on Unicode scalar values. User-perceived characters are a different boundary again: one grapheme may contain a base character plus combining marks, so text-element APIs such as StringInfo are needed. string.Normalize() can put canonically equivalent sequences into the same normalization form before comparison.
Low-Allocation String Handling
Hot paths sometimes need construction without intermediate strings:
ReadOnlySpan<char>slices and parses existing text without allocating another string. Many BCL parse APIs accept spans directly.string.Create(length, state, callback)fills a new string of known length directly, avoiding a separate builder buffer.- Interpolated string handlers (C# 10) let an API consume interpolation pieces without first creating the final string. The benefit applies only when the receiving overload accepts a handler, as
StringBuilder.Appenddoes. ArrayPool<char>rents scratch buffers for transformations. Pooling is worthwhile only when buffer allocations show up in measurement. Every rental adds ownership and clearing rules.
Pitfalls
- Repeated
+=in a loop creates intermediate strings. In a measured hot path,StringBuilderor a pre-sized buffer cuts that churn. ReferenceEqualschecks identity, and interning makes identity depend on how the string was created. Semantic comparison should use APIs that acceptStringComparison, includingstring.Equals,StartsWith,EndsWith, andIndexOf.- Culture-implicit comparison can change with the current culture. Identifiers and protocol values normally need
OrdinalorOrdinalIgnoreCase. Natural-language UI text may need a culture-aware comparison instead.
Tradeoffs
- Interpolation or concatenation vs
StringBuilder: interpolation is clearer for one-off formatting. A builder pays off when text grows through repeated appends. - Ordinal vs culture-aware comparison: ordinal rules are stable for technical keys. Culture-aware comparison belongs to natural-language text where locale behavior is intentional.
- Interning vs ordinary allocation: literals can share interned instances, but code must not depend on their identity. Forcing dynamic values into the pool can save duplicates at the price of process-long retention, so it needs a bounded value set and evidence.
Questions
When is
StringBuildera better choice than string concatenation?
StringBuilderis the better choice when text is built through many appends, especially in a loop or another frequently executed path where allocations matter. For a small, fixed expression, interpolation or concatenation is clearer.The difference comes from string immutability:
- Repeated concatenation: a
stringcannot change, so eachresult += partallocates a new string holding the accumulated text plus the new part. Each loop step leaves behind an intermediate string, and the growing text is copied again every time.StringBuilder: each append writes into a mutable buffer, andToString()creates the final string once. The buffer reallocates only when it runs out of capacity, so an initial capacity close to the expected final size avoids that growth.No fixed number of appends marks the switch point. The signal for a builder is repeated construction in code where measured allocation cost matters.
Why can
ReferenceEquals(a, b)be false whena == bis true for strings?They test different things. For strings,
==compares contents: it returns true when both strings hold the same characters in the same order, using an ordinal, case-sensitive comparison.ReferenceEqualsreturns true only when both references point to the same object.Equal text can therefore live in two separate objects. Strings built at run time are not interned automatically, so two strings built from the same pieces are normally distinct instances:
==returns true whileReferenceEqualsreturns false.Interning can make identity coincide through two different mechanisms:
- Explicit
String.Intern: it returns the intern pool’s reference for a string with that value, adding the argument to the pool if no equal string is there yet. Equal strings passed throughString.Interntherefore always come back as the same reference.- Automatic literal interning: identical string literals typically share one pooled instance, so
ReferenceEqualsoften returns true for them. A compilation can requestNoStringInterning, and runtime behavior may vary, so literal identity is not a dependable contract.Because identity depends on how each string was produced,
ReferenceEqualsis not a test of content. Content comparisons belong to==orstring.Equals, with an explicitStringComparisonstating the rule the data needs: ordinal comparison (OrdinalorOrdinalIgnoreCase) for protocol keys and identifiers, and culture-aware comparison for natural-language text shown to users.Sources: Microsoft Learn: Object.ReferenceEquals · Microsoft Learn: String equality operator · Microsoft Learn: String.Intern · Microsoft Learn: Best practices for comparing strings