Back to Blog
C#

Using C# IndexOf Effectively

c# indexof: Learn how to use the IndexOf method in C# on strings and collections, including overloads, comparison rules, error handling, and practical tradeoffs.

string searchIndexOfC#substringdata structures
Diagram showing a magnifying glass over a string with highlighted index positions, representing the IndexOf method in C#.

When you need to locate a character or substring within a string, c# indexof is the method you will reach for most often. string.IndexOf returns the zero-based index of the first occurrence of a specified value, or -1 if it isn't found. This simple behavior hides a number of important details about overloads, comparison rules, and performance that can affect the correctness of your code.

Basic String.IndexOf Overloads

The most common forms of IndexOf on string are:

  • IndexOf(char) finds the first occurrence of a character.
  • IndexOf(string) finds the first occurrence of a substring.
  • IndexOf(char, int) and IndexOf(string, int) start searching from a given index.
  • IndexOf(char, int, int) and IndexOf(string, int, int) also limit the search to a specified count of characters.

Here's a minimal example:

string path = "/home/user/documents/report.txt"; int lastSlash = path.LastIndexOf('/'); // LastIndexOf is the reverse of IndexOf int extensionStart = path.IndexOf(".txt"); // returns 25 in this example

The example shows both IndexOf and LastIndexOf. LastIndexOf is the companion method that searches from the end of the string toward the beginning. It's equally part of the IndexOf family and often needed when parsing file paths or URLs.

Using StringComparison to Control Matching

The most common source of bugs with IndexOf is the default comparison behavior. For string.IndexOf(string), the default is culture-sensitive, case-sensitive comparison using the current culture. For IndexOf(char), the comparison is always ordinal and case-sensitive.

To make the behavior explicit and avoid surprises, use the overload that accepts a StringComparison enumeration:

string marker = "version:"; string line = "VERSION: 2.1"; int index = line.IndexOf(marker, StringComparison.OrdinalIgnoreCase);

Without StringComparison.OrdinalIgnoreCase, this search would fail because the case doesn't match. The rule of thumb is: when comparing strings that are internal program identifiers, file names, or protocol tokens, use StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase. For user-facing text where linguistic rules apply, use StringComparison.CurrentCulture or StringComparison.InvariantCulture intentionally.

Keep in mind that culture-sensitive comparison can produce unexpected results. For example, in some cultures certain characters may compare as equal even though they are different. Ordinal comparison, on the other hand, compares the numeric code points of the characters, which is fast and deterministic. If you need to find a literal substring regardless of culture, ordinal is the safe choice.

Practical Use Cases for IndexOf

IndexOf is frequently used for parsing and extracting data from structured strings. A typical pattern is to locate a delimiter and then take a substring:

string data = "name=John;age=30;city=Boston"; int start = data.IndexOf("name=") + "name=".Length; int end = data.IndexOf(';', start); string name = data.Substring(start, end - start);

This works because IndexOf gives you the exact position of the next delimiter. Note that the + "name=".Length is necessary to skip past the key itself.

Another scenario is checking whether a string ends with a particular marker. Instead of EndsWith (which does a similar job), you might use LastIndexOf to find the last occurrence before the end:

string file = "archive.tar.gz"; if (file.LastIndexOf(".tar.gz") == file.Length - 7) { // file ends with .tar.gz }

While EndsWith is more readable, LastIndexOf gives you the index if you also need to remove the extension.

Using IndexOf on Other Types

The phrase c# indexof often refers to more than just string. Many collection types, such as List<T>, Array, and IList<T>, expose an IndexOf method as well.

For List<T>:

List<string> colors = new List<string> { "red", "green", "blue" }; int position = colors.IndexOf("green"); // returns 1

List<T>.IndexOf(T) uses the default equality comparer, which for reference types is reference equality unless the type overrides Equals. For string, it uses ordinal string equality. If you need a custom comparison, you can use FindIndex with a predicate:

int pos = colors.FindIndex(c => c.StartsWith("bl"));

For arrays, the Array.IndexOf static method works similarly:

int[] numbers = { 10, 20, 30, 40 }; int idx = Array.IndexOf(numbers, 30); // returns 2

Keep in mind that Array.IndexOf is a static method, not an instance method, so you pass the array as the first argument.

Handling the -1 Return Value

Every overload of IndexOf returns -1 when the value is not found. This is different from many other languages that return null or throw an exception. You must always check the result before using it as an index:

string text = "hello world"; int pos = text.IndexOf("xyz"); if (pos >= 0) { // safe to use pos } else { // handle not found }

Failing to check the return value leads to subtle bugs, especially if you immediately use the result as an argument to Substring. Substring(-1) throws an ArgumentOutOfRangeException. Also, be aware that -1 can be a valid index in some pathological cases only if you're using the optional startIndex parameter with a negative value, but that is not allowed—the startIndex must be within the string length, so -1 is always a sentinel for "not found."

Performance Characteristics

string.IndexOf performs a linear scan in the worst case. For short strings, this is negligible. For long strings or repeated searches, the cost can add up.

The StringComparison overload affects performance. Ordinal comparisons are faster because they avoid culture-specific rules. Culture-sensitive comparisons involve extra logic, especially for composite characters. If you're doing many searches in a loop, prefer ordinal comparisons when correctness allows.

Another performance consideration is the overload you choose. The char overload is extremely fast because it compares single characters. The string overload has to compare sequences. If you only need to find a punctuation character such as ':', using IndexOf(':') is more efficient than IndexOf(":"). The difference is small but can matter in high-throughput parsing scenarios.

For List<T>.IndexOf, the implementation is a linear search that uses the default equality comparer. If you need frequent lookups by key, consider using a Dictionary<TKey, TValue> instead. The Dictionary provides O(1) lookup, but it requires that keys are unique. So if you can change your data structure, a Dictionary beats a linear IndexOf call when the collection is large.

Common Pitfalls with Start Index and Count

When you use an overload with startIndex, be aware that the index is zero-based, and the search begins at that index. The overload with count limits the number of characters to inspect. For example:

string s = "abcdefabc"; int first = s.IndexOf("abc"); // 0 int second = s.IndexOf("abc", 1); // 6 int limited = s.IndexOf("abc", 0, 5); // -1 because "abc" is not within the first 5 characters

The count parameter is the number of characters to search through, not the end index. This is a common source of off-by-one errors when you try to search within a specific window.

A related mistake is using IndexOf with startIndex that exceeds the string's length. That throws an ArgumentOutOfRangeException. Similarly, a negative startIndex or count is invalid. Always validate input against s.Length before making the call.

IndexOf in a Loop: Avoiding Repeated Searches

A frequent pattern is to find all occurrences of a substring. You might be tempted to call IndexOf repeatedly, but you need to advance the start position to continue after each found index.

Here's a correct way:

string source = "the quick brown fox jumps over the lazy dog"; int searchFrom = 0; while (searchFrom < source.Length) { int idx = source.IndexOf("the", searchFrom, StringComparison.OrdinalIgnoreCase); if (idx < 0) break; Console.WriteLine($"Found at {idx}"); searchFrom = idx + 1; }

If you forget to update searchFrom, you'll end up in an infinite loop because the same index keeps being returned. The + 1 moves past the current match. For zero-length patterns, this approach would loop forever, but IndexOf with an empty string returns the start index immediately on all .NET versions that follow the spec, so be aware of that edge case.

This pattern is useful in lexers, parsers, or any code that needs to split on multiple custom delimiters that are not simple separators.

Choosing Between IndexOf, Contains, StartsWith, and Regular Expressions

IndexOf and Contains may seem similar, but they serve different purposes. Contains returns a boolean, while IndexOf returns the position. If you only need to know if a substring exists, Contains is more readable. However, if you need the position to extract data, IndexOf is necessary.

StartsWith and EndsWith also perform prefix/suffix checks. They are implemented more efficiently for that purpose than IndexOf combined with a length check.

Regular expressions are overkill for simple literals, but they become necessary for pattern matching with wildcards or alternations. For a fixed substring, IndexOf is substantially faster and cleaner.

Here's a quick decision table:

TaskRecommended method
Check existence onlyContains
Get position of first occurrenceIndexOf
Get position of last occurrenceLastIndexOf
Prefix checkStartsWith
Suffix checkEndsWith
Complex pattern matchingRegex

This table captures the common guidance. When you need the index, IndexOf is the direct answer; the others are about booleans.

IndexOf with StringComparison and Culture: A Deeper Look

The choice of StringComparison is not just about case sensitivity. It also alters how the method interprets the characters. For example, in Turkish, the uppercase of 'i' is 'İ' (with a dot). The default culture-sensitive comparison would handle this correctly, but StringComparison.OrdinalIgnoreCase would not. This can cause bugs in internationalized applications.

Another subtlety: StringComparison.InvariantCulture and StringComparison.CurrentCulture may yield different results if the current culture changes. If you want consistent behavior across environments, choose InvariantCulture or ordinal, not the default current culture.

The recommendation is to make the comparison explicit on every IndexOf call that involves strings with mixed casing or user input. Avoid relying on the default because it's easy to forget that the default is culture-sensitive, and later someone runs the app on a different locale and sees a different result.

For character searches, there is no StringComparison because character comparison is always ordinal. If you need case-insensitive character search, convert both sides to lower or upper case before searching:

char target = 'A'; string word = "banana"; int idx = word.IndexOf(char.ToLowerInvariant(target), StringComparison.Ordinal); // incorrect overload

Actually, IndexOf(char) does not accept a StringComparison. The correct way is to use the string overload with a one-character string:

int idx = word.IndexOf("a", StringComparison.CurrentCultureIgnoreCase);

This is a nuance worth remembering.

Troubleshooting: Why IndexOf Returns Unexpected Results

The most common reason is a mismatch between the casing you expect and the actual casing in the string. For example, IndexOf("error") on "Error" returns -1 by default. Another common reason is that the substring you're looking for is actually part of a larger pattern, and you're not accounting for surrounding characters. Consider a file name "report_final.txt" - searching for "report" gives index 0, which is correct, but if you then take the substring from that index, you'll get the whole name if you don't limit the length.

A more subtle issue is when the string contains multi-byte characters or surrogate pairs. IndexOf operates on UTF-16 code units, so an emoji, which is represented by a surrogate pair, occupies two indices. If you search for a character that is part of a surrogate pair, you might find a half-index. Always work on strings using StringInfo for text elements if you need to handle full Unicode characters.

When you combine IndexOf with Substring, double-check that the length you calculate doesn't go beyond the end of the string. It is safer to use the last occurrence of a delimiter and subtract indices rather than hardcoding lengths.

Final Code Example: Parsing Key-Value Pairs with IndexOf

To bring together many of the points, here's a robust example of parsing a query string into a dictionary, handling case-insensitivity and missing values:

using System; using System.Collections.Generic; Dictionary<string, string> ParseQuery(string query) { var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); if (string.IsNullOrEmpty(query)) return result; string[] pairs = query.Split('&'); foreach (var pair in pairs) { int eq = pair.IndexOf('='); if (eq < 0) { // No '=', treat entire string as key with empty value result[pair] = ""; } else { string key = pair.Substring(0, eq); string value = pair.Substring(eq + 1); result[key] = Uri.UnescapeDataString(value); // decode percent-encoding } } return result; }

This uses IndexOf(char) to find the '=' quickly, handles the not-found case, and uses a case-insensitive dictionary so that "Name" and "name" are treated the same. It also respects the ordinal nature of character searches. This pattern is common in routing, form handling, or configuration string parsing.

One thing to note is that Split('&') is less efficient than using IndexOf in a loop for very large strings because it allocates substrings for each pair. But for typical query strings, it is sufficient. If you need maximum performance, you would use IndexOf repeatedly to avoid that allocation, but that adds complexity.

Remember that IndexOf is a fundamental tool, but the surrounding logic—comparison rules, boundary checks, and Unicode handling—determines whether your parsing code is reliable. Keep those details in mind and you'll avoid many of the classic indexing bugs.