AlgoMaster Logo

String Methods

Last Updated: May 22, 2026

High Priority
10 min read

Most real work with strings is not about typing characters into a literal, it's about reading, searching, slicing, and reshaping text that came from somewhere else. A customer typed an email with extra spaces, a product name needs the category stripped off, a CSV row needs to be split into fields, a coupon code needs to be checked for a prefix. The string type has a long list of instance methods and a few static helpers that cover these jobs. This chapter walks through the ones used most often, grouped by category, with e-commerce examples for each.

A Note on Immutability Before We Start

Every method in this lesson that "modifies" a string actually returns a brand new string. The original string is never touched, because strings in C# are immutable. "hello".Replace('h', 'H') does not change "hello"; it builds a new string "Hello" and hands it back. If you ignore the return value, the work is wasted. The _String Immutability & Interning_ lesson covers why strings are designed this way and what that means for memory; for now, the rule to remember is: assign the result.

The first Trim() produced a trimmed string and threw it away. The second one captured the result, which is what you almost always want.

With that out of the way, here's a quick map of the categories before we go deep on each.

The diagram shows where we're going. Searching answers "is this in the string and where?" Extracting pulls out a piece. Modifying builds a reshaped copy. Splitting and joining move between a single string and an array of strings. Helpers cover the small cases that come up everywhere.

Searching: Where Is It?

The first family of methods answers questions about whether some text is in a string, and if so, where. Five methods cover most of what you need.

Contains

Contains returns true if the argument appears anywhere in the string, false otherwise. It's the simplest of the five, useful when only presence matters, not position.

The third call returns False because the default comparison is case-sensitive. There's an overload that takes a StringComparison value to control case and culture, and we'll touch on it briefly later in this lesson, but the full rules live in the _String Comparison_ lesson.

StartsWith and EndsWith

StartsWith and EndsWith check the prefix or suffix of a string. They're often the right answer when you'd otherwise be tempted to write s.Substring(0, n) == "...", which is uglier and allocates a new string for nothing.

Both methods also have StringComparison overloads, same caveat as Contains.

IndexOf and LastIndexOf

When you need to know not just whether a substring is there, but where, use IndexOf. It returns the zero-based index of the first occurrence, or -1 if the substring is not found.

LastIndexOf does the same thing but scans from the end. That's the right pick when you want the deepest segment of a path or the final separator in something.

Both methods take either a char or a string. The char overload is slightly faster because it doesn't need to compare a multi-character pattern, but the difference rarely matters unless you're scanning huge strings in a tight loop.

Contains, StartsWith, EndsWith, IndexOf, and LastIndexOf are all O(n) in the length of the string being searched (roughly, the runtime uses faster algorithms in some cases, but worst case is linear). They don't allocate. The -1 return from IndexOf is the standard "not found" sentinel; check for it before passing the result to Substring.

Extracting: Pull a Piece Out

Once you know where something is, you usually want to grab a piece of the string around it. Two methods and one indexer syntax handle this.

Substring(start) and Substring(start, length)

Substring(start) returns everything from start to the end of the string. Substring(start, length) returns length characters starting at start. Both build a new string.

A small flow diagram of what Substring does internally:

The runtime reads four characters starting at index 0 and copies them into a new string object. The source string is untouched.

Two ways to go wrong with Substring. First, if start is past the end, you get an ArgumentOutOfRangeException. Second, if start + length runs past the end, same exception. Guard against this when you compute the arguments from IndexOf:

Range Indexer: s[start..end]

C# 8 added the range operator .. for strings, the same syntax you've seen for arrays. s[a..b] returns a substring containing characters from index a up to (but not including) index b. Combined with the from-end operator ^, it covers most slicing needs in a way that reads more naturally than Substring.

The half-open behavior is the same as with arrays: the start is inclusive, the end is exclusive. productName[..4] is characters at indices 0, 1, 2, 3. Skipping either side defaults to the start or end of the string. s[..] is a full copy.

s[a..b] translates to s.Substring(a, b - a). They produce the same result; the range syntax is more readable for simple slices.

Both Substring and the range indexer allocate a new string and copy the characters. To look at a slice without keeping it, ReadOnlySpan<char> provides a view of part of a string without allocating. For everyday code, Substring is fine.

Modifying: Build a Reshaped Copy

Methods in this group return a new string with some change applied. The original is never modified, because strings are immutable. Most of these accept a char or a string and return a string.

Replace

Replace swaps every occurrence of one substring or character for another. It comes in two flavors: Replace(char, char) and Replace(string, string).

The example shows method chaining. Each Replace returns a fresh string, and the next method runs on that result. Chaining reads cleanly, but every step allocates.

Each Replace call allocates a new string. Three chained Replace calls allocate three intermediate strings (plus the final one), even though only the last value is visible. For one-off transformations this doesn't matter. Inside a hot loop, switch to StringBuilder.

Insert and Remove

Insert(index, text) returns a new string with text spliced in at the given position. Remove(start) removes everything from start to the end; Remove(start, count) removes count characters starting at start.

(Insert indices count against the string that exists at the moment of the call, which is why the second and third positions look off if you only think about the original. The example above shows the mechanic; for real phone formatting, regex or a dedicated formatter is the better tool.)

Trim, TrimStart, TrimEnd

User input is usually messy: extra spaces at the start or end, an accidental tab, a stray newline. Trim() returns a new string with whitespace stripped from both ends. TrimStart() and TrimEnd() strip from one side only.

The no-argument form strips whitespace (' ', '\t', '\n', '\r', and a few other Unicode whitespace characters). The overload that takes a params char[] strips any of those specific characters from the ends. The second example uses Trim('*') to strip asterisks from a banner.

PadLeft and PadRight

These pad a string out to a target width by adding characters on the left or the right. The default pad character is space; pass a char as the second argument to use something else. Useful for aligning columns in a printed report.

PadRight(15) adds spaces to the right of the name until the total length is 15 characters. PadLeft(8) right-aligns the price in an 8-character field. If the string is already at or above the target width, padding does nothing.

A quick reference table for the modifying methods:

MethodWhat it doesReturns
Replace(old, new)Swap every occurrence of old with newNew string
Insert(i, text)Splice text in at index iNew string
Remove(start)Drop everything from start onwardNew string
Remove(start, count)Drop count chars starting at startNew string
Trim()Strip whitespace from both endsNew string
TrimStart() / TrimEnd()Strip whitespace from one endNew string
PadLeft(n) / PadRight(n)Pad to width n (default space)New string

Case: Upper and Lower

Four methods cover case conversion. ToUpper() and ToLower() use the current culture for casing rules. ToUpperInvariant() and ToLowerInvariant() use a fixed, culture-independent set of rules.

The short version of when to use which: for anything where the result will be compared, stored, or used as a key (like normalizing an email address for lookup), prefer the invariant variants. The culture-aware versions can produce surprising results in certain locales (the classic example is the Turkish dotted/dotless i). For now, default to ToLowerInvariant() when normalizing identifiers.

Each case conversion allocates a new string. There's no way around that for the standard methods, since the source string is immutable. For case-normalizing inside a comparison, string.Equals(a, b, StringComparison.OrdinalIgnoreCase) avoids the allocation entirely.

Splitting and Joining

These move between a single string and an array of strings. They're the core operations for any code that parses a CSV row, a category path, a list of tags, or anything else with separators.

Split

Split breaks a string into pieces around a separator. The most common overloads take a char or a string[].

A string separator is useful when the divider is more than one character:

Splitting around " || " (with the spaces) keeps the tag values clean. Splitting around just "||" leaves leading and trailing spaces on each piece, requiring a separate trim.

StringSplitOptions.RemoveEmptyEntries

Consecutive separators or trailing separators produce empty strings in the result. The RemoveEmptyEntries option drops them.

StringSplitOptions.TrimEntries (added in .NET 5) also exists and trims whitespace from each piece. Combine them with | to do both at once: messy.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).

Split allocates an array plus a new string for each piece. For a row with 10 columns that's 11 allocations. In a parsing loop over a large file, this adds up; for occasional parsing in business logic, it's fine.

string.Join

string.Join is the inverse of Split. It takes a separator and a collection of strings and stitches them together. It's a static method, called on the string type, not on an instance.

The second example uses the params overload, where the items are passed as separate arguments. Both forms produce a single string with the separator inserted between every pair of items, but never at the start or end.

Building a CSV row from a cart with multiple lines:

string.Concat

string.Concat is Join with no separator. It's another static helper that takes any number of strings (or any collection) and glues them together. In most code you'd use string interpolation or the + operator, but Concat shows up when you have an existing collection and want a single string out of it.

Use Concat when you have a small fixed number of strings and want to combine them without the overhead of an interpolation or format string. For three or more concatenations in a row, StringBuilder wins on performance.

Helpers: Null and Whitespace Checks

Two static methods on the string type are used so often they deserve their own section.

string.IsNullOrEmpty

string.IsNullOrEmpty(s) returns true if s is either null or the empty string "". It's the safe one-shot check before you treat a string as having content.

The reason this method exists at all is that without it you'd write if (s == null || s == "") everywhere, and it's easy to forget the null half. IsNullOrEmpty packages both checks behind one call.

string.IsNullOrWhiteSpace

string.IsNullOrWhiteSpace(s) is stricter. It returns true if s is null, empty, or contains only whitespace characters. Use this when "the user typed nothing useful" matters, not just "the string is empty."

For user-facing input validation (a customer name, a shipping address line, a search box), IsNullOrWhiteSpace is almost always the right pick. A name field with only spaces is not a real name.

Converting Between string and char[]

Two helpers move between a string and an array of characters. They're occasional, not daily, but they come up when you need to manipulate characters one at a time or reverse a string.

ToCharArray

string.ToCharArray() returns a new char[] holding every character of the string in order.

The array is fully detached from the string, so writes to the array don't change the string (which couldn't change anyway, since it's immutable).

new string(char[])

The string constructor that takes a char[] is the inverse. Build or modify a character array, then wrap it in a string. The most common use case is reversing a string:

Array.Reverse flips the array in place, and new string(chars) constructs a fresh string from it. (For ASCII text this gives the expected reversed output. Strings with multi-byte characters need more care, which the regex and culture chapters touch on.)

ToCharArray allocates a new char[] and copies every character. new string(char[]) allocates a new string and copies the characters back. For a one-off reversal this is fine. For repeated character-level work in a loop, StringBuilder and Span<char> are better fits.

Everything Returns a New String

Every method that "modifies" a string returns a new string. The original is untouched. The _String Immutability & Interning_ lesson explains why C# was designed this way (it makes strings safe to share across threads, predictable to hash, and easy to reason about) and the _StringBuilder_ lesson introduces StringBuilder, the mutable counterpart for building a string piece by piece without allocating on every step.

The reference table below summarizes the methods covered in this lesson.

MethodCategoryWhat it doesReturns
Contains(value)SearchingIs value anywhere in the string?bool
StartsWith(value) / EndsWith(value)SearchingPrefix or suffix checkbool
IndexOf(value)SearchingFirst index, or -1int
LastIndexOf(value)SearchingLast index, or -1int
Substring(start) / Substring(start, length)ExtractingPull out a piecestring
s[a..b]ExtractingRange slice (C# 8+)string
Replace(old, new)ModifyingSwap occurrencesstring
Insert(i, text)ModifyingSplice text instring
Remove(start) / Remove(start, count)ModifyingDrop a chunkstring
Trim / TrimStart / TrimEndModifyingStrip endsstring
PadLeft(n) / PadRight(n)ModifyingPad to widthstring
ToUpper / ToLowerCaseCulture-aware casestring
ToUpperInvariant / ToLowerInvariantCaseCulture-independent casestring
Split(separator)SplittingBreak into piecesstring[]
string.Join(sep, parts)JoiningStitch pieces togetherstring
string.Concat(parts)JoiningJoin with no separatorstring
string.IsNullOrEmpty(s)HelperNull or ""?bool
string.IsNullOrWhiteSpace(s)HelperNull, empty, or whitespace only?bool
ToCharArray()ConvertString to char[]char[]
new string(char[])Convertchar[] to stringstring