← Open in the full interactive course (progress tracking, search & more)

Every application works with text — and strings are how .NET represents it.

Imagine you're building a login form. The user types a username, an email address, maybe a password. Behind every input box is a string — a sequence of characters that travels from the screen into your application, gets validated, stored, searched, and displayed back.

In C#, string is one of the most used types. It looks simple, but it hides a lot of important behaviour. Understanding how strings work — and how to avoid the most common performance traps — is one of the most useful skills you can develop early.

In this lesson, you'll learn what strings really are, why they're immutable, how to manipulate them safely, and when to use StringBuilder or modern span-based APIs.

What Is It?

The Simple Explanation

A string is a sequence of characters. It can be a single word, a sentence, a JSON payload, a file path — anything made of text.

The Technical Definition

string in C# is an alias for System.String, a reference type that represents a sequence of UTF-16 code units. It is immutable, which means once a string is created, its contents can never change.

Some important facts:

Type What It Represents Example
char One UTF-16 code unit 'A', '7', '' (surrogate pair in practice)
string A sequence of characters "Hello", " Launch"

Why Does It Exist?

The Problem

Computers fundamentally work with numbers. But users, files, networks, and APIs work with text. Somewhere there must be a bridge between raw numeric data and meaningful human-readable information.

Text is also tricky:

The Solution

The .NET framework provides a rich string type, a mutable builder (StringBuilder), and modern low-allocation APIs (ReadOnlySpan<char>, SearchValues<char>) to handle all of these concerns.

The most important design decision is immutability — strings cannot change after creation. This gives us thread safety, simpler reasoning, and the ability to share string data without fear of accidental mutation.

Big Picture

Here's how text flows through a typical .NET application:

TEXT IN A .NET APPLICATION
INPUT
  • User input (forms, console)
  • File contents (text, JSON, CSV)
  • Network responses (APIs, web sockets)
STRING OPERATIONS
  • Concatenation & formatting
  • Searching & parsing
  • Splitting & joining
  • Comparison & sorting
OUTPUT
  • Displayed in UI / console
  • Written to files / logs
  • Sent as API responses

At the center of all this is the string object. Understanding its behaviour is essential to writing correct and performant text-processing code.

How It Works

Let's trace what happens when you create and modify strings.

Step 1 — A string is created

string greeting = "Hello";

The runtime allocates a block of memory on the managed heap to hold the characters H, e, l, l, o. The variable greeting holds a reference to that object.

Step 2 — Modifying creates a new string

string name = greeting + " Buddy";

Because strings are immutable, this does not change the original "Hello". Instead, the compiler calls String.Concat, which allocates a new string that contains "Hello Buddy". The original string remains unchanged in memory.

Step 3 — Assignment updates the reference

greeting = greeting + " Buddy";

Now the variable greeting points to the new string. The old "Hello" string may become garbage and eventually be collected by the Garbage Collector.

Step 4 — Many edits need a mutable builder

var builder = new StringBuilder();
for (int i = 0; i < 10; i++)
{
    builder.Append(i);
}
string result = builder.ToString();

StringBuilder maintains an internal mutable buffer. Each Append writes into that buffer without creating a new string until you call ToString(). This is dramatically more efficient for loops.

Step 5 — Encoding bridges memory and I/O

In memory, a string is stored as UTF-16 code units. When writing to a file or sending over the network, you usually convert to UTF-8 using Encoding.UTF8.GetBytes(...). The reverse process uses Encoding.UTF8.GetString(...). This encoding step is where many real-world text bugs appear.

Simple Example

using System.Text;

string firstName = "Ada";
string lastName = "Lovelace";

// String interpolation (modern C#)
string fullName = $"{firstName} {lastName}";
Console.WriteLine(fullName);          // Ada Lovelace

Console.WriteLine(fullName.Length);   // 12
Console.WriteLine(fullName.ToUpperInvariant()); // ADA LOVELACE

// Mutable building with StringBuilder
var builder = new StringBuilder();
for (int i = 0; i < 5; i++)
{
    builder.Append(i);
}
string result = builder.ToString();
Console.WriteLine(result);            // 01234

Code → Meaning → Result

Real-World Example

Imagine an e-commerce system generating an order confirmation message. You need to combine a customer name, order ID, and total into a human-readable email:

string customer = "Grace";
int orderId = 1024;
decimal total = 249.99m;

string message = $"""
    Dear {customer},

    Your order {orderId} has been confirmed.
    Total: {total:C}
    """;

Console.WriteLine(message);

This uses a raw string literal (C# 11+), which lets you write multi-line text without escaping quotes or newline characters. Interpolation with {total:C} formats the decimal as currency using the current culture.

If this message were built by concatenating many pieces inside a loop, using string += would allocate a new string on every iteration. Instead, a production system would likely use StringBuilder for the loop, then interpolation for the final template.

Analogy

Three mental images

string — like a fixed sign. Once the letters are printed, you can't erase or change them. To make a different sign, you must create a completely new sign and throw the old one away.

StringBuilder — like a whiteboard. You can write, erase, and append as much as you like. Only when you're done do you take a photo (call ToString()) to make a fixed version.

ReadOnlySpan<char> — like a finger pointing at a portion of the sign. It doesn't copy anything; it just allows you to inspect a region of text efficiently.

Under the Hood

What actually happens inside the .NET runtime when you work with strings?

INTERNAL VIEW
1. STRING OBJECT

A System.String object contains:

2. IMMUTABLE
3. STRING INTERNING
4. STRINGBUILDER BUFFER
5. MODERN LOW-ALLOCATION APIS (.NET 8+)

C# 14 and .NET 10 do not change the fundamental string model. The underlying object is still an immutable UTF-16 sequence. However, modern C# (11+) introduced raw string literals, UTF-8 string literals, and performance-oriented APIs like spans continue to be the recommended way to handle high-performance text processing.

Common Confusion

1. string vs StringBuilder

Many beginners assume string can be modified because code like name += "!" appears to change the variable. It doesn't — it creates a new string and reassigns the reference.

FeaturestringStringBuilder
MutabilityImmutableMutable
Performance for loopsPoor (allocations per change)Excellent (single buffer)
Thread safetyInherently thread-safeNot thread-safe by default
Typical useShort-lived, simple textBuilding complex text iteratively

2. == vs .Equals()

For most reference types, == compares references. But string overloads == to compare values. So both a == b and a.Equals(b) return true if the content is identical, even if the objects are different references.

string a = "test";
string b = "te" + "st";
Console.WriteLine(a == b);       // True
Console.WriteLine(a.Equals(b));   // True

3. null vs empty vs whitespace

These are different states:

string? nullable = null;
string empty = "";
string whitespace = "   ";

Console.WriteLine(string.IsNullOrEmpty(nullable));        // True
Console.WriteLine(string.IsNullOrEmpty(empty));           // True
Console.WriteLine(string.IsNullOrWhiteSpace(whitespace)); // True
Console.WriteLine(string.IsNullOrEmpty(whitespace));      // False

4. char vs string

A char uses single quotes and represents one UTF-16 code unit. A string uses double quotes and represents a sequence.

char letter = 'A';      // one character
string text = "A";      // a sequence of one character

Common Mistakes

Mistake 1 — Using += inside a loop

Wrong:

string result = "";
for (int i = 0; i < 10000; i++)
{
    result += i.ToString();   // new string allocated every iteration!
}

Correct:

var builder = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
    builder.Append(i);
}
string result = builder.ToString();

This can be orders of magnitude faster.

Mistake 2 — Forgetting culture when casing/comparing

Using ToLower() or ToUpper() for internal identifiers can create culture-specific surprises (e.g., Turkish İ problem).

Use ToLowerInvariant(), ToUpperInvariant(), or specify StringComparison.Ordinal for identifiers and keys. Use culture-aware methods only for user-facing text.

Mistake 3 — Not handling null before calling methods

Calling .Length or .ToUpper() on a null string throws NullReferenceException.

Check with string.IsNullOrEmpty or string.IsNullOrWhiteSpace first, or use null-conditional access name?.Length.

Mistake 4 — Using string methods for high-performance scanning

Calling string.Contains, Split, or repeated IndexOf in a hot loop can allocate unnecessarily.

In .NET 8+, use SearchValues<char> and ReadOnlySpan<char> for zero-allocation parsing and scanning.

using System.Buffers;

ReadOnlySpan<char> text = "Hello, World!".AsSpan();
SearchValues<char> vowels = SearchValues.Create("aeiouAEIOU");
int count = 0;
foreach (var ch in text)
{
    if (vowels.Contains(ch)) count++;
}
Console.WriteLine(count); // 3

When Should I Use It?

Use plain string when:

Use StringBuilder when:

Use ReadOnlySpan<char> / SearchValues when:

When string handling might be overkill:

Mental Model

Text = a sequence of characters
string = an immutable, fixed sequence
StringBuilder = a mutable text builder
ReadOnlySpan<char> = a zero-copy view over text

Remember:
· Modifying a string creates a new one
· Use StringBuilder for loops
· Use spans when allocations matter
· Always consider encoding and culture

Key Takeaway


Check Your Understanding

You've seen how strings work, why immutability matters, and how to choose the right text tool for the job. Let's see if you can apply this knowledge.

1. You need to build a large CSV file by appending 100,000 rows inside a loop. Which type should you use for the best performance?

Show answer

Correct: B

Why B is correct: StringBuilder maintains a mutable internal buffer. Each Append writes into that buffer without allocating a new string. Only ToString() at the end creates one final string. This is dramatically more efficient than repeated string concatenation in a loop.

Why A is incorrect: string += in a loop creates a new string on every iteration because strings are immutable. This leads to O(n²) time and excessive garbage collection.

Why C is incorrect: char[] is a fixed-size array. You would need to manage capacity and resizing manually, which is exactly what StringBuilder already does for you.

Why D is incorrect: ReadOnlySpan<char> is a view over existing text, not a builder. It cannot efficiently accumulate new characters into a growing string.

Reinforcement: Whenever you need to construct a large string from many pieces, especially inside a loop, StringBuilder is your go-to mutable builder.

2. Why does immutability make strings safer and easier to use in .NET?

Show answer

Correct: B

Why B is correct: Immutability means once a string is created, its contents cannot change. Therefore multiple threads can safely share the same string without locks. The runtime can also cache hash codes and intern literal strings for efficiency, knowing they will never mutate.

Why A is incorrect: Immutability does not prevent all memory leaks. If you keep references to strings unnecessarily, they will still occupy memory.

Why C is incorrect: Immutability often makes operations slower if many modifications are needed, because each modification creates a new string. That's why StringBuilder exists.

Why D is incorrect: Strings are managed objects and are garbage collected when no longer referenced. Immutability does not exempt them from GC.

Reinforcement: Thread safety, hash code caching, and string interning are three major benefits that flow directly from immutability.

3. What does the following code print?

string a = "hello";
string b = a;
b = b + " world";
Console.WriteLine(a);
Show answer

Correct: A

Why A is correct: Strings are immutable. b = b + " world" creates a new string and assigns it to b. The original string "hello" remains unchanged, so a still references "hello".

Why B is incorrect: a was never modified. Only b was reassigned to a new string.

Why C is incorrect: The concatenation added " world", not removed "hello".

Why D is incorrect: This is perfectly valid C# and will not throw.

Reinforcement: When you assign a string to another variable, both initially point to the same immutable object. Modifying one via reassignment does not affect the other.

4. A method receives a string parameter that may be null. Which check correctly handles both null and strings containing only spaces?

Show answer

Correct: C

Why C is correct: string.IsNullOrWhiteSpace returns true if the string is null, empty, or consists only of white-space characters. It is the safest single check for "no meaningful text."

Why A is incorrect: This only checks for null. It misses empty strings and whitespace-only strings.

Why B is incorrect: Calling .Length on a null string throws NullReferenceException. This check is incomplete and unsafe.

Why D is incorrect: This only checks for an empty string, not null or whitespace.

Reinforcement: Use IsNullOrWhiteSpace for most user-input validation. Use IsNullOrEmpty only when whitespace is considered valid data.

5. In .NET 8+, which API would you use to efficiently count occurrences of multiple characters (e.g., vowels) in a large string without allocating intermediate strings?

Show answer

Correct: C

Why C is correct: SearchValues<char> is designed for fast, allocation-free scanning of text for a set of characters. Combined with ReadOnlySpan<char>, you can iterate without creating substrings or intermediate strings.

Why A is incorrect: Split creates multiple string allocations and is not efficient for counting characters.

Why B is incorrect: Repeated Contains calls allocate or scan inefficiently; they are not optimized for set-based character searches.

Why D is incorrect: StringBuilder.Replace is for replacing text and will allocate even if used for counting. It is not appropriate here.

Reinforcement: For hot-path text processing in modern .NET, prefer SearchValues, ReadOnlySpan<char>, and similar low-allocation APIs to keep performance predictable.

You now have a clear mental model of strings and text in .NET — from immutable strings to high-performance span-based scanning!


dotnetmadeeasy.com — Learn C# and .NET, the right way.