Strings
Definition
A string in C# is an immutable sequence of Unicode characters. Strings are reference types (instances of System.String) but behave like value types in many respects — equality comparison uses value semantics, and they are immutable by design.
string greeting = "Hello, World!";
// string is an alias for System.String
System.String greeting2 = "Hello, World!";
Strings are reference types stored on the managed heap, but the compiler treats them with value-type semantics for equality (== compares contents, not reference identity).
Core Concepts
String Immutability
Once created, a string object cannot be modified. Any operation that appears to modify a string actually creates a new string instance.
string a = "hello";
string b = a;
a += " world";
// a = "hello world" (new string)
// b = "hello" (original unchanged)
Why it matters:
- Thread-safe by default — no synchronization needed for reads
- Enables string interning — the runtime reuses identical string literals to save memory
- Prevents accidental mutation bugs
String interning means the CLR maintains a pool of unique string literals. Identical literals in code share the same memory location:
string s1 = "hello";
string s2 = "hello";
// s1 and s2 reference the same object in the intern pool
Console.WriteLine(ReferenceEquals(s1, s2)); // True
Runtime-constructed strings (e.g., via StringBuilder or concatenation) are not automatically interned unless you call string.Intern().
String Creation and Initialization
// String literal
string fromLiteral = "Hello";
// From constructor (char array)
char[] chars = { 'H', 'e', 'l', 'l', 'o' };
string fromChars = new string(chars);
// From a single character repeated
string repeated = new string('a', 5); // "aaaaa"
// Empty string
string empty1 = "";
string empty2 = string.Empty; // preferred
String Concatenation
// + operator
string result = "Hello" + " " + "World";
// String.Concat
string concat = string.Concat("Hello", " ", "World");
// String.Join — joins with a separator
string joined = string.Join(", ", "apple", "banana", "cherry");
// "apple, banana, cherry"
String Interpolation
string name = "Alice";
int age = 30;
decimal price = 19.99m;
// Basic interpolation
string msg = $"My name is {name}, age {age}";
// Format specifiers
string formatted = $"Price: {price:F2}"; // "Price: 19.99"
string currency = $"Total: {price:C}"; // "Total: $19.99"
string padded = $"ID: {42:D5}"; // "ID: 00042"
string percent = $"Rate: {0.85:P0}"; // "Rate: 85 %"
Verbatim Strings
Prefix with @ to treat escape sequences as literal characters. Ideal for file paths, regex patterns, and multi-line text:
// File paths — no need to escape backslashes
string path = @"C:\Users\Alice\Documents\file.txt";
// Multi-line
string multiline = @"Line one
Line two
Line three";
// Regex patterns
string pattern = @"\d+\.\d+"; // matches decimal numbers
Raw String Literals (C# 11)
Enclose in triple (or more) quotes """. No escaping needed — perfect for JSON, SQL, and embedded code:
string json = """
{
"name": "Alice",
"age": 30,
"regex": "^\d+$"
}
""";
// With interpolation (add $ before """)
string sql = $"""
SELECT * FROM Users
WHERE Name = '{name}'
AND Age > {age}
""";
String Comparison
string a = "Hello";
string b = "hello";
// == operator (ordinal, case-sensitive)
bool equal = a == b; // False
// String.Equals — supports StringComparison
bool ignoreCase = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // True
// String.Compare — returns -1, 0, or 1
int cmp = string.Compare(a, b, StringComparison.Ordinal);
// Best practice: always specify StringComparison
bool same = string.Equals(a, b, StringComparison.Ordinal); // byte-by-byte
bool sameCulture = string.Equals(a, b, StringComparison.CurrentCulture); // culture-aware
- Use
StringComparison.OrdinalorStringComparison.OrdinalIgnoreCasefor programmatic comparisons (dictionary keys, file paths). - Use
StringComparison.CurrentCulturefor displaying sorted results to users.
String Manipulation Methods
string s = " Hello, World! ";
s.Substring(2, 5); // "Hello" (start, length)
s.Trim(); // "Hello, World!"
s.TrimStart(); // "Hello, World! "
s.ToUpper(); // " HELLO, WORLD! "
s.ToLower(); // " hello, world! "
s.Replace("World", "C#"); // " Hello, C#! "
s.Contains("Hello"); // True
s.StartsWith(" Hel"); // True
s.EndsWith("! "); // True
s.IndexOf("World"); // 9
s.Split(", "); // [" Hello", "World! "]
s.PadLeft(20, '.'); // "..... Hello, World! "
string.Join("-", s.Split(", ")); // " Hello-World! "
Code Examples
StringBuilder for Efficient Concatenation
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 10_000; i++)
{
sb.AppendLine($"Item {i}");
}
string result = sb.ToString();
// With initial capacity to reduce reallocations
var sb2 = new StringBuilder(capacity: 50_000);
sb2.Append("INSERT INTO Users (Name) VALUES ");
sb2.AppendLine("('Alice'),");
sb2.AppendFormat("('{0}'),", "Bob");
sb2.Replace("Alice", "Alicia");
sb2.Insert(0, "-- SQL Insert\n");
sb2.Remove(0, 16); // remove inserted comment
Culture-Aware Interpolation with FormattableString
decimal value = 1234.56m;
// FormattableString captures the format for deferred rendering
FormattableString fs = $"Value: {value:C}";
// Render with invariant culture (always uses '.' decimal separator)
string invariant = FormattableString.Invariant(fs); // "Value: 1234.56"
// Render with current culture
string local = fs.ToString(); // "Value: $1,234.56" (varies by culture)
Checking for Null or Empty
string? input = null;
// Preferred methods
bool isNullOrEmpty = string.IsNullOrEmpty(input); // True
bool isNullOrWhiteSpace = string.IsNullOrWhiteSpace(" "); // True
When to Use
| Scenario | Recommendation |
|---|---|
| A few concatenations | + operator or string.Concat |
| Many concatenations in a loop | StringBuilder |
| Embedding variables in text | String interpolation $"" |
| File paths, regex patterns | Verbatim strings @"" |
| JSON, SQL, multi-line templates | Raw string literals """ """ |
| Culture-aware display | StringComparison.CurrentCulture |
| Programmatic comparison | StringComparison.Ordinal |
Common Pitfalls
-
Using
+in loops — Creates a new string per iteration. O(n^2) memory allocation. UseStringBuilderinstead. -
Case-sensitive comparison bugs —
==is case-sensitive."hello" == "Hello"isfalse. UseStringComparison.OrdinalIgnoreCasewhen case should not matter. -
Using
==for culture-aware comparison —==uses ordinal comparison, not culture rules. Usestring.Equals(a, b, StringComparison.CurrentCulture)for user-facing sorting. -
Substring out of range —
s.Substring(start, length)throwsArgumentOutOfRangeExceptionifstart + length > s.Length. Always validate bounds first. -
Calling
.ToString()on null — ThrowsNullReferenceException. UseConvert.ToString(obj)or null-conditionalobj?.ToString().
Key Takeaways
- Strings are immutable — every modification creates a new instance
- Use
StringBuilderfor concatenation inside loops or building large strings - Always specify
StringComparisonexplicitly in comparisons - Use raw string literals (
""") for multi-line content with no escaping - Use
string.IsNullOrEmpty()andstring.IsNullOrWhiteSpace()for null checks - String interning reuses identical literals in memory but does not apply to runtime-constructed strings
Interview Questions
Q: Why are strings immutable in C#? A: Immutability provides thread safety (strings can be shared across threads without locks), enables string interning (memory optimization by reusing identical literals), simplifies equality semantics, and prevents hard-to-track mutation bugs.
Q: What is string interning? A: The CLR maintains an internal pool (intern pool) of unique string literals. When the same literal appears multiple times in code, they all reference the same string object in memory. Runtime-constructed strings are not automatically interned.
Q: When should you use StringBuilder vs string concatenation?
A: Use StringBuilder when concatenating strings inside loops or building large dynamic strings (e.g., generating SQL, HTML). For simple one-off concatenations of a few strings, the + operator or string.Concat is fine — the compiler even optimizes simple cases.
Q: What is the difference between == and String.Equals?
A: The == operator on strings performs ordinal comparison (byte-by-byte, case-sensitive). String.Equals offers overloads that accept a StringComparison enum, giving you control over culture and case sensitivity.
Q: What does StringComparison.Ordinal mean?
A: It performs a byte-by-byte comparison using the raw character code points, ignoring cultural linguistics. It is the fastest and most predictable comparison — ideal for internal logic, dictionary keys, and file paths.