C# List IndexOf: Locating Elements in a List
c# list indexof: Learn how to use List.IndexOf in C# to find element positions, handle missing items, and choose between IndexOf and FindIndex for your search needs.
If you have a List<string> and need to find the position of a specific value, List.IndexOf returns the zero-based index or -1 if the value is absent. The method is straightforward for value types and reference types, but its behavior depends on the equality comparison used. This article explains how to use c# list indexof effectively, including overloads, performance characteristics, and when FindIndex is a better fit.
The Basic IndexOf Call
The simplest overload takes a single argument: the item you are looking for. It returns the first occurrence of that item in the list.
List<string> fruits = new List<string> { "apple", "banana", "cherry" }; int index = fruits.IndexOf("banana"); Console.WriteLine(index); // Output: 1
The search starts at index 0 and moves forward. If the item appears multiple times, only the first match is returned. To find later occurrences, you need an overload that accepts a starting index.
List<string> names = new List<string> { "Anna", "Bob", "Anna", "Charlie" }; int first = names.IndexOf("Anna"); // 0 int second = names.IndexOf("Anna", 1); // 2
The second parameter specifies the starting position for the search. The method still returns the first match at or after that position. This overload is useful when you need to process repeated elements without removing them from the list.
Understanding the Overloads
List<T>.IndexOf has three overloads:
| Overload | Description |
|---|---|
IndexOf(T item) | Searches the entire list for the first occurrence. |
IndexOf(T item, int index) | Searches starting at the given index to the end. |
IndexOf(T item, int index, int count) | Searches a range of count elements starting at index. |
All overloads return the zero-based index of the first match, or -1 if no match is found. The count parameter limits how many elements are examined, which can be useful when you know the item cannot appear beyond a certain position.
List<int> numbers = new List<int> { 10, 20, 30, 40, 30, 50 }; int result = numbers.IndexOf(30, 2, 2); // Searches indices 2 and 3 only Console.WriteLine(result); // Output: -1 because 30 is at index 4, not in range
Here the search starts at index 2 and checks exactly two elements (indices 2 and 3). The value 30 appears at index 4, which is outside the range, so the method returns -1. This overload is rarely needed but can avoid scanning large lists when you have positional constraints.
What Happens When the Element Is Not Found
When IndexOf cannot find a match, it returns -1. This is a sentinel value that is impossible as a valid index because list indices are zero-based and non-negative. You must check for -1 before using the result as an index, otherwise you will get an ArgumentOutOfRangeException.
List<string> colors = new List<string> { "red", "green", "blue" }; int pos = colors.IndexOf("yellow"); if (pos >= 0) { Console.WriteLine($"Found at {pos}"); } else { Console.WriteLine("Not found"); }
This pattern is idiomatic in C#. Forgetting the check is a common source of bugs, especially when the list contents change at runtime. The method does not throw an exception when the item is missing; it returns -1, which is part of the contract.
Performance and Equality Semantics
IndexOf performs a linear scan from the starting index to the end of the range. Its time complexity is O(n) in the worst case, where n is the number of elements examined. For small lists this is negligible, but for large lists it can become a bottleneck if called repeatedly.
The equality comparison uses the default equality comparer for the type T. For reference types, this is object.Equals, which checks reference equality unless the type overrides Equals. For value types, it uses the overridden Equals if present, otherwise it falls back to value equality via reflection.
public class Product { public int Id { get; set; } public string Name { get; set; } } List<Product> products = new List<Product> { new Product { Id = 1, Name = "Laptop" }, new Product { Id = 2, Name = "Mouse" } }; Product target = new Product { Id = 1, Name = "Laptop" }; int idx = products.IndexOf(target); // Returns -1 because reference equality fails
If you want value-based equality, you need to override Equals and GetHashCode in the class, or use FindIndex with a predicate that compares specific properties. This is a frequent source of confusion when working with custom types.
Case-Insensitive Search Without Custom Comparer
IndexOf does not have an overload that accepts a StringComparer. If you need a case-insensitive search, you have two main options: convert the list to a different representation, or use FindIndex with a predicate.
List<string> words = new List<string> { "Alpha", "beta", "Gamma" }; int idx = words.FindIndex(w => w.Equals("BETA", StringComparison.OrdinalIgnoreCase)); Console.WriteLine(idx); // Output: 1
FindIndex accepts a Predicate<T> and returns the index of the first element that satisfies the condition. This is more flexible than IndexOf because you can define any comparison logic, including case-insensitive matching, culture-aware comparison, or comparisons on a property of the element.
If you must use IndexOf for case-insensitive search, you can temporarily normalize the list with List.ConvertAll or use LINQ, but those approaches add overhead and are less readable. For most scenarios, FindIndex is the better tool.
FindIndex vs IndexOf: Which One to Use
IndexOf is the right choice when you have an exact object or value to find and the default equality semantics are acceptable. It is concise and does not require a delegate.
FindIndex is the better choice when:
- You need to compare using a custom rule, such as a property value or case-insensitive string comparison.
- The element type does not override
Equalsand you want value-based matching. - You want to search for an element that satisfies a complex condition.
The tradeoff is that FindIndex requires a delegate, which can be slightly less efficient due to the delegate invocation overhead, but the difference is negligible for typical list sizes. The real cost is the same linear scan.
List<int> data = new List<int> { 5, 12, 8, 3, 17 }; int evenIndex = data.FindIndex(n => n % 2 == 0); // Returns 1 (12)
If you only need to check existence, consider List.Contains or Any instead of using IndexOf and checking for -1. Contains returns a boolean and is clearer when you do not need the index.
Handling Null and Value Type Elements
IndexOf handles null as a valid search item for reference types. If the list contains null, you can find its position directly.
List<string> items = new List<string> { "a", null, "b" }; int nullIndex = items.IndexOf(null); // Returns 1
For value types, null cannot be passed because T is a non-nullable value type. If you use Nullable<T> (e.g., List<int?>), then null is a valid value and IndexOf works as expected.
List<int?> numbers = new List<int?> { 1, null, 3 }; int idx = numbers.IndexOf(null); // Returns 1
Be careful when mixing nullable and non-nullable types. If you have a List<int> and try to call IndexOf(null), the compiler will reject it because int cannot be null. This is a compile-time check that prevents accidental misuse.
Using IndexOf with Custom Types
When you have a custom class and want IndexOf to find an element by value, you must override Equals and GetHashCode. Without these overrides, IndexOf uses reference equality, which almost always fails unless you pass the exact same object instance.
public class Person { public string Name { get; set; } public int Age { get; set; } public override bool Equals(object obj) { return obj is Person other && Name == other.Name && Age == other.Age; } public override int GetHashCode() { return HashCode.Combine(Name, Age); } }
With these overrides, IndexOf will compare objects by their property values, not by reference. This makes the method behave intuitively for domain objects. However, overriding Equals affects all equality comparisons, including Contains, Distinct, and Remove, which can be desirable or not depending on the context.
If you do not want to change the class's equality semantics globally, FindIndex with a predicate is the safer choice because it scopes the comparison to the search operation only.
When a Dictionary Is a Better Alternative
If you frequently need to find the index of an element by a key, a List<T> may not be the right structure. A Dictionary<TKey, TValue> provides O(1) lookup by key, but it does not preserve insertion order in older .NET versions. In .NET 6 and later, OrderedDictionary or Dictionary with KeyedByTypeCollection can help, but for simple index lookup, a List with IndexOf is acceptable only if the list is small or the search is infrequent.
For large lists where you repeatedly search by a property, consider building a separate Dictionary that maps the property value to the index. This trades memory for speed and avoids the linear scan.
List<Product> products = GetProducts(); var indexMap = products .Select((p, i) => new { p.Id, Index = i }) .ToDictionary(x => x.Id, x => x.Index); int productIndex = indexMap[42]; // O(1) lookup
This pattern is useful when the list is relatively static and you need many lookups. The initial construction cost is O(n), but subsequent lookups are O(1). If the list changes frequently, maintaining the dictionary becomes a burden, and a simple IndexOf might be simpler.
IndexOf remains a fundamental tool for working with lists. Its linear behavior is acceptable for many real-world scenarios, and its overloads give you control over the search range. When you need custom comparison logic, FindIndex is the more expressive alternative. Understanding the equality semantics and the -1 return value will help you avoid common bugs and write more predictable code.