Back to Blog
C#

Find the Last Occurrence with C# LastIndexOf

c# lastindexof: Learn C# LastIndexOf syntax, overloads, and practical use cases to find the last occurrence of a character or substring in a string.

C# string manipulationIndexOfString search.NETSubstringLastIndexOf
C# code snippet showing LastIndexOf used to find the last dot in a file name.

When you need to locate the last occurrence of a character or substring in a string, string.LastIndexOf is the direct method in .NET. It searches backward from the end of the string (or from a specified index) and returns the zero-based index of the match, or -1 if no match is found. Understanding c# lastindexof is essential for tasks like parsing file paths, trimming known suffixes, or extracting the final segment of a delimited string.

Basic Syntax and Overloads

The method is available on the string type and is heavily overloaded. The most common forms are:

int LastIndexOf(char value); int LastIndexOf(string value); int LastIndexOf(char value, int startIndex); int LastIndexOf(string value, int startIndex); int LastIndexOf(string value, int startIndex, int count);

The parameterless versions search the entire string. The overloads that accept startIndex begin the search at that index and move backward. The overload with count limits the number of character positions to examine. It's important to note that startIndex is inclusive — it is the first character to check.

Validating the Search Input

LastIndexOf throws ArgumentNullException if the string parameter is null, and ArgumentOutOfRangeException if startIndex or count are outside the valid range. The valid startIndex range is from 0 to the string length. For count, it must be non-negative and startIndex + count cannot exceed the length. If you are constructing these values dynamically, guard them before calling.

Case Sensitivity and Culture

By default, LastIndexOf performs a case-sensitive, culture-sensitive comparison. This means that for most cultures, "a" and "A" are treated as different, and the comparison uses the current culture's linguistic rules. For example, in Turkish, the uppercase 'I' has different casing rules than English, which can yield unexpected results. To control this, use an overload that accepts a StringComparison enumeration. The recommended approach for ordinal, culture-independent matching is to pass StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase. An ordinal comparison is fast and treats characters purely by their numeric Unicode value, making it suitable for most programmatic parsing where linguistic rules are not required.

Practical Example: Extracting the File Extension

A common real-world use is extracting the extension from a file name. Consider the following code:

string fileName = @"C:\reports\quarterly\Q4-2025.txt"; int lastDot = fileName.LastIndexOf('.'); string extension = lastDot >= 0 ? fileName.Substring(lastDot + 1) : ""; Console.WriteLine(extension); // Outputs: txt

Here, LastIndexOf('.') finds the position of the final dot in the full path. The Substring call then takes everything after it. Without LastIndexOf, you would need to use IndexOf and then repeat the search or write a manual loop, which is more error-prone. This pattern is useful for any scenario where you need to ignore earlier occurrences inside directory names.

Working with Substrings

The overload that takes a string searches for a sequence of characters. This is useful for isolating the last folder name in a path:

string path = @"/home/user/documents/projects"; int lastSlash = path.LastIndexOf('/'); string lastFolder = lastSlash >= 0 ? path.Substring(lastSlash + 1) : path; Console.WriteLine(lastFolder); // Outputs: projects

Note that this uses a forward slash as the separator, which is common on Unix-like systems. On Windows, paths might use backslashes. In that case, you could pass a backslash character, but remember that a backslash is an escape character in C# – you would write '\\' for a character literal. For cross-platform code, consider using Path.DirectorySeparatorChar or Path.GetFileName instead.

Behavior When No Match Is Found

If the specified character or substring is not present, LastIndexOf returns -1. This is a sentinel value, so always check it before using the result as an index. For example:

string data = "1234567890"; int commaIndex = data.LastIndexOf(','); if (commaIndex >= 0) { // Safe to use commaIndex } else { // handle missing delimiter }

Ignoring the -1 case and directly using the returned index in Substring or indexing will cause an ArgumentOutOfRangeException. A common defensive pattern is to combine the check and extraction in one line, as shown earlier.

Performance and Memory Considerations

LastIndexOf is an O(n) operation in the worst case, where n is the length of the string being searched. For a search that uses an ordinal comparison, the implementation is efficient and does not allocate memory (aside from the case-insensitive ordinal variant, which might create temporary lowercase representations internally). For culture-sensitive comparisons, the algorithm may be more complex and potentially slower, so if you are performing many searches in a loop, prefer StringComparison.Ordinal. If you are searching for a fixed set of delimiters in a long text, keep in mind that every call scans from the start (or the specified start index) backward. If you need to find the last occurrence of any of several characters, consider using LastIndexOfAny which accepts an array of characters and returns the highest index at which any of them appears, potentially reducing the number of passes.

Common Mistakes and Edge Cases

One frequent mistake is confusing the return value with a boolean. LastIndexOf returns an index, not a bool, so if (path.LastIndexOf('\\') == -1) is a valid check, but bool found = path.LastIndexOf('\\'); will not compile. Another issue is using the overload with startIndex without understanding that the search is backward. For instance, "abcabc".LastIndexOf('a', 3) starts at index 3 (the second 'c') and searches backward, finding the 'a' at index 0. But "abcabc".LastIndexOf('a', 2) searches only the 'c' and 'b' and returns -1. When you pass a count, it affects the number of characters scanned, but the starting point remains inclusive. Always test with your exact inputs to avoid off-by-one errors.

Comparison with IndexOf and Alternatives

The direct counterpart is IndexOf, which searches forward from the start. The choice between them depends solely on which occurrence you need. For scenarios where you need to handle multiple delimiters, LastIndexOfAny is more efficient than multiple calls. For complex pattern matching, regular expressions with the Regex class can be used, but they are often heavier and slower for simple positional searches. If you are working with spans, .NET offers MemoryExtensions.LastIndexOf for ReadOnlySpan<char>, which can be used in high-performance paths to avoid allocations. For example:

ReadOnlySpan<char> span = stackalloc char[] { 'a', 'b', 'c', 'a' }; int idx = span.LastIndexOf('a');

This API is available in .NET Core 2.1+ and .NET 5+, but not in the older .NET Framework. Use it when you have already converted your string to a span for other operations and want to avoid extra method calls.

Where This Method Should Not Be Used

For simple existence checks, Contains is clearer and returns a boolean directly. If you need to find the last occurrence across multiple strings or in a list, you would normally use a loop that calls LastIndexOf on each, but if you need the last element that contains a substring, a LINQ query might be more readable. Also, for directory paths, always prefer Path.GetDirectoryName and Path.GetFileName over manual string manipulation to avoid platform-specific separator issues. LastIndexOf is a low-level tool; use it when you have a clear need for index-based extraction.

Extending the Pattern: Trimming a Known Suffix

A practical advanced pattern is to remove a trailing suffix only if it exists, using LastIndexOf with StringComparison.Ordinal to avoid culture issues. For instance, to remove a trailing "_draft" from a document name:

string name = "report_final_draft"; string suffix = "_draft"; int pos = name.LastIndexOf(suffix, StringComparison.Ordinal); if (pos >= 0 && pos + suffix.Length == name.Length) { name = name.Substring(0, pos); }

The extra condition pos + suffix.Length == name.Length ensures that the match is actually at the end of the string, not in the middle. This guards against accidentally removing a suffix that appears earlier, which would be wrong. This approach is preferable to using EndsWith and Substring in two separate calls because it does the lookup and validation in one pass.

Final Technical Note: Overload Resolution Pitfalls

When you pass a string literal that could be implicitly converted to either char or string, the compiler selects the exact overload. For example, LastIndexOf('a') calls the char overload, while LastIndexOf("a") calls the string overload. If you pass a variable of type object, you will get a compile-time error. Also, be aware that the StringComparison overloads only exist for string, not for char – to do a case-insensitive char search, you can convert the char to a string and use LastIndexOf(char.ToString(), StringComparison.OrdinalIgnoreCase). This is a niche but sometimes necessary workaround.

Understanding the nuances of LastIndexOf will prevent subtle bugs in your string-processing logic. By choosing the correct overload and comparison mode, you can write predictable, maintainable code that handles real-world input variations.

c# lastindexof: Practical Usage and Code Examples | RYUSLOG DEV