Question
Why are strings immutable in .NET, what benefits does immutability bring (correctness, thread-safety, security, caching), how does string interning reduce duplicate allocations via the intern pool, and when should you (not) use string.Intern and string.IsInterned?
Brief Answer
Strings are immutable: once created, their contents never change. This guarantees thread-safety, enables hash/codepoint caching, prevents aliasing bugs, and improves security (e.g., paths, role names, dictionary keys).
String interning keeps one canonical instance per distinct content in an intern pool (all literals are interned by default; dynamically created strings can be interned via string.Intern). Interning can lower memory and speed equality checks (reference equality works for interned duplicates), but it holds references for the lifetime of the AppDomain, so don’t intern high-cardinality or unbounded data.
Because strings are immutable, any “modification” returns a new string. Equality by content is common and safe; equality by reference can be an optimization when interning ensures a single instance for the same text.
Example A — “Modifying” a string creates a new instance
var s1 = "hello"; var s2 = s1.ToUpper(); // "HELLO" (a different object) Console.WriteLine(object.ReferenceEquals(s1, s2)); // False
APIs like ToUpper, Replace, and Substring return new strings; the original is untouched.
Example B — Immutability aids thread-safety
string shared = "ready";
Task.WaitAll(
Task.Run(() => { var x = shared; /* safe read */ }),
Task.Run(() => { var y = shared; /* safe read */ })
);
// No locks needed for reading the same string reference.
Because contents can’t change, multiple threads can share a string safely without synchronization.
Example C — Literals are interned automatically
var a = "token"; var b = "token"; Console.WriteLine(object.ReferenceEquals(a, b)); // True (same interned literal)
The runtime stores one literal per distinct text in the intern pool. Re-using the literal yields the same reference.
Example D — Dynamically produced equal strings are not necessarily interned
var x = new string("id-42".ToCharArray()); // same content as literal, new object
var y = "id-42";
Console.WriteLine(object.ReferenceEquals(x, y)); // False (x not interned)
Constructed strings aren’t automatically interned. They compare equal by content, but not by reference unless interned.
Example E — Interning dynamically with string.Intern
var x = new string("id-42".ToCharArray());
var y = string.Intern(x); // returns canonical interned instance
Console.WriteLine(object.ReferenceEquals(y, "id-42")); // True
Intern returns the canonical instance from the pool, inserting your string if missing.
Example F — Check if a string is interned
var maybe = "alpha" + "beta"; // compiler may fold to literal "alphabeta" var interned = string.IsInterned(maybe); // null if not interned; otherwise the pool reference
IsInterned returns the pooled reference or null. This can be used to avoid inserting high-cardinality data.
Example G — Referential equality is a fast check for interned strings
var u1 = string.Intern("user:42");
var u2 = string.Intern(new string("user:42".ToCharArray()));
if (object.ReferenceEquals(u1, u2))
{
// same canonical object without content compare
}
For interned values with identical content, ReferenceEquals is a constant-time equality test.
Example H — Pitfall: unbounded interning can leak memory
// Avoid: interning unbounded, unique values like GUIDs or timestamps: string.Intern(Guid.NewGuid().ToString()); // keeps growing the pool forever
Interned strings are rooted for the app lifetime. Don’t intern data with many distinct values.
Example I — Security & correctness: immutability prevents mutation bugs
void UsePath(string path)
{
// No caller can mutate 'path' contents under your feet
if (!path.StartsWith("/safe/")) throw new UnauthorizedAccessException();
}
If strings were mutable, a caller could alter contents after validation. Immutability guarantees stable values.
Example J — Dictionaries benefit from immutable keys
var dict = new Dictionary<string, int>(StringComparer.Ordinal); dict["key"] = 1; // The key string never changes; hash remains valid.
Immutable keys ensure hash and equality remain stable, avoiding subtle bugs.