Skip to main content

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!";
Key Fact

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
warning

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
StringComparison Guidelines
  • Use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase for programmatic comparisons (dictionary keys, file paths).
  • Use StringComparison.CurrentCulture for 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

ScenarioRecommendation
A few concatenations+ operator or string.Concat
Many concatenations in a loopStringBuilder
Embedding variables in textString interpolation $""
File paths, regex patternsVerbatim strings @""
JSON, SQL, multi-line templatesRaw string literals """ """
Culture-aware displayStringComparison.CurrentCulture
Programmatic comparisonStringComparison.Ordinal

Common Pitfalls

  1. Using + in loops — Creates a new string per iteration. O(n^2) memory allocation. Use StringBuilder instead.

  2. Case-sensitive comparison bugs== is case-sensitive. "hello" == "Hello" is false. Use StringComparison.OrdinalIgnoreCase when case should not matter.

  3. Using == for culture-aware comparison== uses ordinal comparison, not culture rules. Use string.Equals(a, b, StringComparison.CurrentCulture) for user-facing sorting.

  4. Substring out of ranges.Substring(start, length) throws ArgumentOutOfRangeException if start + length > s.Length. Always validate bounds first.

  5. Calling .ToString() on null — Throws NullReferenceException. Use Convert.ToString(obj) or null-conditional obj?.ToString().

Key Takeaways

  • Strings are immutable — every modification creates a new instance
  • Use StringBuilder for concatenation inside loops or building large strings
  • Always specify StringComparison explicitly in comparisons
  • Use raw string literals (""") for multi-line content with no escaping
  • Use string.IsNullOrEmpty() and string.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.

References