Strings and Immutability
A string is text that never changes: every operation builds a new string, so the result has to be assigned to be kept.
string text = "hello"; int n = text.Length; char first = text[0];A string is a piece of text, such as "hello". In C# the type is written string, which is a second name for the type System.String. The two words mean exactly the same type, and string is the one used in everyday code.
A string in .NET is immutable. Once it exists, its characters never change. Every method that looks like it edits a string actually builds a new string and hands that back. The string you started with is still there, untouched. This is not a restriction the language could lift later; the whole base class library is built on the promise that a string you hold cannot be altered behind your back.
Reading a string
Length gives the number of characters. Square brackets read one character by position, and positions start at 0, so the first character is text[0] and the last is text[text.Length - 1]. A single character is its own type, char, written with single quotes: 'h'. Asking for a position that does not exist throws IndexOutOfRangeException.
What trips people up
Two things. First, people call a method and drop the result. name.Trim(); on a line by itself changes nothing; you need name = name.Trim();. Second, immutability costs something in a loop. total += word; is shorthand for total = total + word;, and each round builds a whole new string and copies everything collected so far. For a few pieces that is fine. For thousands it is not, which is what StringBuilder exists for.
Example
string greeting = "hello";
// Every string operation returns a NEW string.
// ToUpperInvariant uppercases without using the machine's language settings.
string loud = greeting.ToUpperInvariant();
Console.WriteLine(greeting); // the original is untouched
Console.WriteLine(loud);
Console.WriteLine(greeting.Length); // how many characters the string holds
Console.WriteLine(greeting[0]); // first character; counting starts at 0
Console.WriteLine(greeting[greeting.Length - 1]); // last character
// Replace returns a new string too. Throwing that result away does nothing.
greeting.Replace("hello", "goodbye");
Console.WriteLine(greeting);
// Keep the result instead.
string changed = greeting.Replace("hello", "goodbye");
Console.WriteLine(changed);
// Output:
// hello
// HELLO
// 5
// h
// o
// hello
// goodbyeWhen to use it
- Check that a user-supplied order reference is exactly eight characters before sending it to a payment provider.
- Pull the first character of a surname to build an initial for a display name.
- Uppercase a product code for display while keeping the original value as stored.
- Reject an empty form field by testing Length before any other work.
More examples
A char is not a string
Indexing a string gives a char, written with single quotes, not a one-character string. Mixing the two is the most common first compile error with strings: 'A' and "A" are different types and will not compare to each other. Reach for Substring when you need the piece as text, and indexing when you need the character itself.
string code = "A7X";
char first = code[0]; // a single character, type char
string firstAsText = code.Substring(0, 1); // the same character as a string
Console.WriteLine(first);
Console.WriteLine(firstAsText);
Console.WriteLine(first == 'A'); // char compared with a char literal
Console.WriteLine(firstAsText == "A"); // string compared with a string literal
// Length is a count, so the last valid position is Length - 1.
Console.WriteLine(code.Length);
Console.WriteLine(code[code.Length - 1]);
// Output:
// A
// A
// True
// True
// 3
// XComparing text, not identity
A string is a reference type, so a variable holds the address of the text rather than the text itself. For almost every other reference type == compares addresses. String overrides that and compares the characters, which is why == is the right tool for comparing text. ReferenceEquals is shown only to make the distinction visible; production code has no reason to call it on strings.
string typed = "net";
// Built when the program runs, so this is a different object in memory.
string built = string.Concat("n", "et");
// == on strings compares the characters, which is almost always what you want.
Console.WriteLine(typed == built);
// ReferenceEquals asks whether they are the same object in memory.
Console.WriteLine(ReferenceEquals(typed, built));
// Equals does the same character comparison as ==.
Console.WriteLine(typed.Equals(built));
// Output:
// True
// False
// TrueWhy += in a loop is expensive
Three strings were created to produce one answer, and each copied everything already collected. The cost grows with the square of the number of additions, so a thousand appends copy around half a million characters. This is the measurement behind the StringBuilder lesson; it is not a reason to avoid += for two or three pieces.
// A loop runs the same lines again and again. Here i is 1, then 2, then 3.
string joined = "";
for (int i = 1; i <= 3; i++)
{
// Shorthand for: joined = joined + i + "-";
// Each round builds a brand new string and copies the old one into it.
joined += i + "-";
Console.WriteLine($"after {i}: {joined} (length {joined.Length})");
}
// Output:
// after 1: 1- (length 2)
// after 2: 1-2- (length 4)
// after 3: 1-2-3- (length 6)
Discussion