C# Strings Explained — Creation, Manipulation and Common Mistakes
Every application that has ever existed talks to humans using text. Your banking app says 'Transfer successful.' Your favourite game says 'Player 1 wins.' Your email client says 'No messages found.' All of that is text, and text in C# lives inside strings. Before you can build anything meaningful, you need to understand how C# handles text — and it turns out C# has some very specific and powerful opinions about it.
The problem strings solve is simple: computers only understand numbers. Underneath the hood, the letter 'A' is just the number 65, and 'B' is 66. Strings give you a friendly, human-readable wrapper around all those numbers so you can write code that works with words and sentences naturally, without worrying about the raw numeric values underneath.
By the end of this article you'll know how to create strings, combine them, search inside them, change their content, compare them correctly, and avoid the classic traps that trip up beginners and even some experienced developers. You'll also walk away with a solid mental model of why strings in C# behave the way they do — and that 'why' is what separates developers who just copy code from developers who actually understand it.
What a String Actually Is in C# — and Why It's Immutable
In C#, a string is an object of the built-in System.String class. When you write string with a lowercase 's', it's just a convenient alias — C# quietly replaces it with System.String behind the scenes. Either spelling works; most C# developers use the lowercase version by convention.
Here's the most important thing to understand about strings right away: they are immutable. Immutable means once a string is created, it cannot be changed. Ever. If you think you're changing a string, you're actually creating a brand new string and throwing away the old one.
Think of it like a sticky note. Once you write on it, you can't erase and rewrite it — you have to throw it away and grab a fresh sticky note. This design choice makes strings safe to share across your program without worrying that one piece of code secretly modifies text that another piece depends on.
This immutability has a very real performance consequence we'll revisit in the gotchas section — it's one of the most common C# interview topics.
using System; class StringBasics { static void Main() { // Declaring a string with a literal value string greeting = "Hello, World!"; // string (lowercase) is just an alias for System.String // Both of these lines are identical in behaviour string firstName = "Alice"; System.String lastName = "Johnson"; // same type, different spelling // Strings can contain letters, numbers, symbols, even spaces string emailAddress = "alice.johnson@example.com"; string productCode = "ITEM-2024-XL"; // An empty string — valid and very common string emptyText = ""; // string.Empty is the recommended way to represent an empty string // It's more expressive and avoids any confusion with null string alsoEmpty = string.Empty; // Proving immutability — 'greeting' is not modified. // ToUpper() creates and returns a BRAND NEW string. // The original 'greeting' still holds "Hello, World!" string shoutedGreeting = greeting.ToUpper(); Console.WriteLine(greeting); // original — unchanged Console.WriteLine(shoutedGreeting); // new string Console.WriteLine(firstName + " " + lastName); Console.WriteLine(emailAddress); Console.WriteLine("Is emptyText empty? " + string.IsNullOrEmpty(emptyText)); } }
HELLO, WORLD!
Alice Johnson
alice.johnson@example.com
Is emptyText empty? True
Creating and Combining Strings — Concatenation, Interpolation and Verbatim Strings
Once you have strings, you immediately need to combine them. Imagine building a personalised message like 'Good morning, Alice! You have 3 new notifications.' — that sentence is assembled from separate pieces of data. C# gives you several ways to do this, and each has its place.
The oldest way is concatenation using the + operator. It works, but it gets messy fast when you have more than two or three pieces.
String interpolation (using the $ prefix) is the modern, readable approach. You embed expressions directly inside curly braces {} inside the string. It reads almost like plain English and is the recommended approach in modern C# code.
Format strings using string.Format() are older but still appear heavily in legacy codebases, so you need to recognise them.
Verbatim strings (using the @ prefix) let you write strings exactly as they appear — including backslashes and multiple lines — without needing escape characters. They're invaluable for file paths and multi-line text.
Understanding which tool to reach for keeps your code readable and your colleagues happy.
using System; class StringCombining { static void Main() { string userName = "Alice"; int notificationCount = 3; double accountBalance = 1025.50; // --- METHOD 1: Concatenation with + --- // Works fine for simple cases, gets messy with many variables string welcomeMessage = "Good morning, " + userName + "! You have " + notificationCount + " new notifications."; Console.WriteLine(welcomeMessage); // --- METHOD 2: String Interpolation (RECOMMENDED for modern C#) --- // The $ prefix enables interpolation. Expressions go inside { } // You can put ANY valid C# expression inside the braces string interpolatedMessage = $"Good morning, {userName}! You have {notificationCount} new notifications."; Console.WriteLine(interpolatedMessage); // Interpolation also supports formatting specifiers after a colon // :C formats as currency, :F2 forces 2 decimal places string balanceMessage = $"Your balance is {accountBalance:C} as of today."; Console.WriteLine(balanceMessage); // You can even call methods inside the braces string upperCaseName = $"Username in capitals: {userName.ToUpper()}"; Console.WriteLine(upperCaseName); // --- METHOD 3: string.Format() — older style, still common in legacy code --- // {0}, {1} etc. are placeholders replaced by the arguments that follow string formattedMessage = string.Format("Hello, {0}. Your balance is {1:C}.", userName, accountBalance); Console.WriteLine(formattedMessage); // --- METHOD 4: Verbatim strings with @ --- // Without @, backslashes need escaping: "C:\\Users\\Alice\\Documents" // With @, you write it exactly as it looks — no escaping needed string filePath = @"C:\Users\Alice\Documents\report.pdf"; Console.WriteLine(filePath); // Verbatim strings also support multiple lines — the line breaks are included string multiLineAddress = @"123 Maple Street Springfield IL 62701"; Console.WriteLine(multiLineAddress); } }
Good morning, Alice! You have 3 new notifications.
Your balance is $1,025.50 as of today.
Username in capitals: ALICE
Hello, Alice. Your balance is $1,025.50.
C:\Users\Alice\Documents\report.pdf
123 Maple Street
Springfield
IL 62701
Essential String Methods — Searching, Slicing and Transforming Text
A string sitting still isn't very useful. Real applications constantly need to inspect and reshape text: check if an email address contains '@', extract a username from a full name, remove accidental spaces from user input, replace offensive words, or check if a password meets minimum length requirements.
C# strings come loaded with built-in methods that handle all of these scenarios. You don't need to write complex loops — the heavy lifting is already done for you.
The most important methods to master are: Length (how many characters?), ToUpper() / ToLower() (change case), Trim() (remove whitespace from edges), Contains() (does this text exist inside?), StartsWith() / EndsWith() (checks at boundaries), IndexOf() (where does this text start?), Substring() (cut out a piece), Replace() (swap text for other text), and Split() (break a string into an array of pieces).
These ten methods will handle about 90% of the string manipulation you'll ever need in day-to-day development.
using System; class StringMethods { static void Main() { string userInput = " alice.johnson@example.com "; string productDescription = "The Red Bicycle is fast, reliable, and red."; string fullName = "Alice Johnson"; string csvLine = "London,Paris,Tokyo,Sydney"; // --- Length --- // Returns the number of characters (spaces count!) Console.WriteLine($"Raw input length: {userInput.Length}"); // includes the spaces // --- Trim --- // Removes leading and trailing whitespace — essential for cleaning user input string cleanEmail = userInput.Trim(); Console.WriteLine($"Cleaned email: '{cleanEmail}'"); Console.WriteLine($"Clean length: {cleanEmail.Length}"); // --- ToLower / ToUpper --- // Useful for case-insensitive comparisons or display formatting string normalizedEmail = cleanEmail.ToLower(); Console.WriteLine($"Normalised: {normalizedEmail}"); // --- Contains --- // Returns true or false — checks if a substring exists anywhere inside bool hasAtSymbol = cleanEmail.Contains("@"); Console.WriteLine($"Is it an email? {hasAtSymbol}"); // --- StartsWith / EndsWith --- bool isHttps = "https://thecodeforge.io".StartsWith("https"); bool isPdf = "report.pdf".EndsWith(".pdf"); Console.WriteLine($"Secure URL: {isHttps}, Is PDF: {isPdf}"); // --- IndexOf --- // Returns the ZERO-BASED position of the first match, or -1 if not found int atPosition = cleanEmail.IndexOf("@"); Console.WriteLine($"'@' is at index: {atPosition}"); // --- Substring --- // Substring(startIndex) extracts from startIndex to the end // Substring(startIndex, length) extracts exactly 'length' characters string domainPart = cleanEmail.Substring(atPosition + 1); // everything after @ Console.WriteLine($"Domain: {domainPart}"); string extractedFirstName = fullName.Substring(0, 5); // first 5 chars Console.WriteLine($"First name extracted: {extractedFirstName}"); // --- Replace --- // Replaces ALL occurrences, not just the first one string correctedDescription = productDescription.Replace("red", "blue"); Console.WriteLine(correctedDescription); // --- Split --- // Breaks a string into an array at every occurrence of the separator string[] cities = csvLine.Split(','); Console.WriteLine($"Number of cities: {cities.Length}"); foreach (string city in cities) { Console.WriteLine($" City: {city}"); } } }
Cleaned email: 'alice.johnson@example.com'
Clean length: 25
Normalised: alice.johnson@example.com
Is it an email? True
Secure URL: True, Is PDF: True
'@' is at index: 13
Domain: example.com
First name extracted: Alice
The Blue Bicycle is fast, reliable, and blue.
Number of cities: 4
City: London
City: Paris
City: Tokyo
City: Sydney
Comparing Strings Correctly and Building Strings Efficiently with StringBuilder
String comparison sounds trivial — just use ==, right? In C# it's mostly safe for simple cases, but the moment you deal with user input, data from APIs, or mixed-case text, naive comparison breaks down. 'Alice' and 'alice' are the same username but == says they're different.
The correct tool is string.Equals() with a StringComparison option, or string.Compare(). These let you explicitly say whether case matters and whether to use the current culture's rules or a stable, predictable ordinal comparison.
Now for performance: remember how strings are immutable? Every time you use + to build a string inside a loop, you're creating and discarding a new string object on every single iteration. If you're building a 10,000-row report by appending line by line, that's 10,000 string objects created and thrown away. That's a serious memory and speed problem.
StringBuilder from System.Text solves this exactly. It's like a mutable notepad — you keep writing on the same object, and only convert it to a final string at the very end. For any string built in a loop, always use StringBuilder.
using System; using System.Text; // Required for StringBuilder class StringComparisonAndBuilder { static void Main() { // ===================================================== // PART 1: String Comparison Done Right // ===================================================== string storedUsername = "Alice"; string inputFromUser = "alice"; // user typed lowercase // Naive comparison — WRONG for usernames (case-sensitive) bool naiveMatch = (storedUsername == inputFromUser); Console.WriteLine($"Naive == comparison: {naiveMatch}"); // False — incorrect! // Correct comparison — OrdinalIgnoreCase ignores casing // Use this for usernames, file names, command inputs bool correctMatch = string.Equals(storedUsername, inputFromUser, StringComparison.OrdinalIgnoreCase); Console.WriteLine($"OrdinalIgnoreCase comparison: {correctMatch}"); // True — correct! // For text displayed to users (like sorting a list of names) // use CurrentCultureIgnoreCase — it respects locale rules bool cultureMatch = string.Equals(storedUsername, inputFromUser, StringComparison.CurrentCultureIgnoreCase); Console.WriteLine($"CurrentCultureIgnoreCase comparison: {cultureMatch}"); // Null-safe check: always handle null before comparing string possiblyNullInput = null; bool isSafe = string.Equals(storedUsername, possiblyNullInput, StringComparison.OrdinalIgnoreCase); Console.WriteLine($"Null-safe comparison result: {isSafe}"); // False, no crash Console.WriteLine(); // ===================================================== // PART 2: Building Strings Efficiently with StringBuilder // ===================================================== int reportRowCount = 5; // imagine this is 10,000 in production // BAD approach — creates a new string on every + in the loop // Do NOT do this in loops with many iterations string badReport = "Sales Report\n"; for (int rowNumber = 1; rowNumber <= reportRowCount; rowNumber++) { badReport += $"Row {rowNumber}: ${ rowNumber * 100}.00\n"; // new string each time! } Console.WriteLine("--- Bad Report (illustrative only) ---"); Console.Write(badReport); Console.WriteLine(); // GOOD approach — StringBuilder mutates ONE internal buffer // Only call .ToString() once at the very end StringBuilder reportBuilder = new StringBuilder(); reportBuilder.AppendLine("Sales Report"); // AppendLine adds text + newline for (int rowNumber = 1; rowNumber <= reportRowCount; rowNumber++) { // Append builds up the content without creating intermediate strings reportBuilder.AppendLine($"Row {rowNumber}: ${rowNumber * 100}.00"); } // Convert to a real string exactly once string efficientReport = reportBuilder.ToString(); Console.WriteLine("--- Efficient Report ---"); Console.Write(efficientReport); // StringBuilder also supports Insert, Remove and Replace reportBuilder.Insert(0, "=== CONFIDENTIAL ===\n"); Console.WriteLine("First 30 chars after Insert: " + reportBuilder.ToString().Substring(0, 20) + "..."); } }
OrdinalIgnoreCase comparison: True
CurrentCultureIgnoreCase comparison: True
Null-safe comparison result: False
--- Bad Report (illustrative only) ---
Sales Report
Row 1: $100.00
Row 2: $200.00
Row 3: $300.00
Row 4: $400.00
Row 5: $500.00
--- Efficient Report ---
Sales Report
Row 1: $100.00
Row 2: $200.00
Row 3: $300.00
Row 4: $400.00
Row 5: $500.00
First 30 chars after Insert: === CONFIDENTIAL ===...
| Feature / Aspect | string | StringBuilder |
|---|---|---|
| Mutability | Immutable — every change creates a new object | Mutable — modifies the same internal buffer |
| Memory usage in loops | High — discards and recreates on every operation | Low — reuses a single buffer throughout |
| Performance for many appends | Slow — O(n²) for n concatenations in a loop | Fast — amortised O(n) for n appends |
| Syntax convenience | High — clean literals, interpolation, all methods | Medium — uses Append/AppendLine/Insert/Remove |
| Thread safety | Safe to share — immutability prevents race conditions | Not thread-safe by default |
| Best use case | Fixed text, single expressions, method returns | Building reports, generating HTML, CSV, SQL |
🎯 Key Takeaways
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.