In .NET, a string is not “effectively constant.” It is immutable by construction. Once an instance exists, its character data cannot be rewritten in place. That is a memory-model and security decision, not a documentation nicety.
The storage is assigned once
Internally, a string holds its characters in a private buffer. That buffer is set in the constructor and is not exposed for mutation. The readonly contract on that storage means instance methods are not allowed to re-point or rewrite it after construction.
If a method needs a different sequence of characters, it copies, transforms, and returns a new string. Replace, ToUpper, and Trim all follow that pattern:
public string Replace(char oldChar, char newChar)
{
char[] chars = this.ToCharArray();
for (int i = 0; i < chars.Length; i++)
{
if (chars[i] == oldChar)
chars[i] = newChar;
}
return new string(chars);
}
The original instance is untouched. Callers who still hold the previous reference keep seeing the previous value.
Why string is sealed
string is a sealed class. If it were open for inheritance, a subclass could introduce mutable storage or override members in a way that broke the immutability invariant. Sealing the type keeps every string on the same contract: hash codes stay stable, interned instances stay safe, and APIs can treat the value as a snapshot.
Why the runtime cares
Immutability is what makes string interning legal. It is also why strings are safe dictionary keys — the hash cannot drift after insert — and why passing a string across threads does not require a lock on the character data. In security-sensitive code, a permission check on a path string must not race against another thread mutating that same instance.
The cost is allocation. Tight loops that concatenate or rewrite text belong on StringBuilder or Span<char>, not on repeated string operators.
Immutability here is not a slogan. It is sealed type + one-time storage + copy-on-change. That is the hidden mechanism.
Expanded from a public note I first published on LinkedIn in March 2025.