Common String Operations

Split, Join, Trim, Replace and the search methods, plus the StringComparison argument that decides how two pieces of text are compared.

Syntaxtext.Contains(value, StringComparison.OrdinalIgnoreCase)

Most work with text is cutting it up, cleaning it, or asking whether it contains something. These are methods on the string itself, and every one of them returns a new string or a new array rather than changing the original.

The common ones

Split cuts a string wherever it finds a separator and returns an array of the pieces: a fixed row of values numbered from 0 and read with square brackets. string.Join does the reverse, putting a separator between the pieces. Trim removes whitespace from both ends, and TrimStart and TrimEnd do one end each. Replace swaps every occurrence of one piece of text for another. Substring(startIndex, length) takes a slice. IndexOf reports where something starts, or -1 when it is not there. Contains, StartsWith and EndsWith answer yes or no.

Ordinal or culture-aware

The search methods take an optional StringComparison value. Ordinal compares the raw character numbers: exact, fast, and identical on every machine. CurrentCulture applies the language rules of the region the machine is set to, where, for example, the single character œ counts as the pair "oe". Each has an IgnoreCase twin, and InvariantCulture offers language rules that never vary by machine.

What trips people up

The defaults are not consistent. Contains(string) is ordinal. StartsWith(string), EndsWith(string) and IndexOf(string) use the current culture. Identifiers, file names, URLs and anything compared for correctness want Ordinal or OrdinalIgnoreCase. Save culture rules for text a person reads.

Example

Example · csharp
string raw = "  Ada, Grace , Linus  ";

// Split cuts the string at every comma and returns an array of the pieces.
// TrimEntries also strips the spaces around each piece (added in .NET 5).
string[] names = raw.Split(',', StringSplitOptions.TrimEntries);

// Join does the opposite: one string from many, with a separator between them.
Console.WriteLine(string.Join(" | ", names));

Console.WriteLine(raw.Trim());                   // whitespace off both ends
Console.WriteLine(raw.Replace(",", ";").Trim()); // every comma replaced

// Contains(string) compares character by character, so case matters.
Console.WriteLine(names[0].Contains("ada"));

// Ask for the case-insensitive comparison explicitly.
Console.WriteLine(names[0].Contains("ada", StringComparison.OrdinalIgnoreCase));
Console.WriteLine(names[0].StartsWith("A", StringComparison.Ordinal));
Console.WriteLine(names[2].EndsWith("us", StringComparison.Ordinal));

// Output:
// Ada | Grace | Linus
// Ada, Grace , Linus
// Ada; Grace ; Linus
// False
// True
// True
// True

When to use it

  • Turn a comma-separated list of tags from a form field into separate values.
  • Strip stray whitespace from a pasted email address before saving it.
  • Check whether an uploaded file name ends with .csv regardless of capitalisation.
  • Rebuild a slug by joining cleaned words with hyphens.

More examples

Splitting a line of comma-separated values

Split with a single char argument uses StringSplitOptions.None, which keeps empty pieces, and that is usually right for data: an empty field in a CSV row is a real field, and dropping it shifts every column after it. Reach for RemoveEmptyEntries when you are splitting human input, such as a space-separated list of tags. Neither option understands quoted fields, so a real CSV file needs a proper parser.

Example · csharp
string line = "id,name,,city";

// None (the default) keeps the empty piece between the two commas.
string[] all = line.Split(',');

// RemoveEmptyEntries drops it.
string[] filled = line.Split(',', StringSplitOptions.RemoveEmptyEntries);

Console.WriteLine(all.Length);
Console.WriteLine(string.Join("/", all));
Console.WriteLine(filled.Length);
Console.WriteLine(string.Join("/", filled));

// Output:
// 4
// id/name//city
// 3
// id/name/city

Where Ordinal and culture-aware disagree

This is the clearest demonstration that the choice is not cosmetic. If you compare a routing key, a header name or a file extension with culture rules, two different byte sequences can be treated as equal, and in some languages two identical-looking ones can be treated as different. Ordinal and OrdinalIgnoreCase are the right default for anything a machine produced.

Example · csharp
// U+0153 is the single "oe" ligature character.
string word = "œil";

// Ordinal compares raw character numbers: one character is not two.
Console.WriteLine(word.StartsWith("oe", StringComparison.Ordinal));

// Culture-aware comparison uses language rules, where the ligature counts as oe.
Console.WriteLine(word.StartsWith("oe", StringComparison.InvariantCulture));

// Case-insensitive ordinal is the usual choice for identifiers and file names.
Console.WriteLine("README.MD".EndsWith(".md", StringComparison.OrdinalIgnoreCase));

// Output:
// False
// True
// True

Pulling one value out of a header line

IndexOf plus Substring is the pattern for splitting on only the first separator, which Split cannot do without extra arguments; here the value itself may contain spaces or colons and must stay intact. Always test the result against -1 first, because Substring with a negative start throws ArgumentOutOfRangeException.

Example · csharp
string header = "Authorization: Bearer abc123";

// IndexOf(char) is ordinal and returns -1 when the character is absent.
int colon = header.IndexOf(':');

if (colon >= 0)
{
    string key = header.Substring(0, colon);        // start at 0, take colon chars
    string value = header.Substring(colon + 1);     // from after the colon onwards
    Console.WriteLine($"{key} => {value.Trim()}");
}
else
{
    Console.WriteLine("malformed header");
}

// Output:
// Authorization => Bearer abc123

Discussion

  • Be the first to comment on this lesson.