Back to Blog
C#

C# Property vs Indexer: Key Differences and Usage

c# property vs indexer: Understand the differences between C# properties and indexers, including syntax, use cases, and when to choose each for clean, maintainable code.

C# propertiesC# indexersC# syntaxobject-oriented programmingC# collections
Diagram comparing C# property and indexer syntax with a class showing both access patterns.

When designing a C# class, you often need to expose state or provide access to elements in a collection. Two language features serve this purpose: properties and indexers. Understanding the difference between c# property vs indexer is essential for writing clean, intuitive APIs. While both use accessors, they serve different roles: a property represents a single named value, while an indexer represents a collection of values accessed by an index.

Properties: A Named Accessor for a Single Value

A property is a member that provides a flexible mechanism to read, write, or compute the value of a private field. Properties are accessed by name, without any parameters, and they behave like public fields from the caller's perspective. The syntax is straightforward:

public class Temperature { private double _celsius; public double Celsius { get => _celsius; set => _celsius = value; } }

The get accessor returns the backing field or a computed value, and the set accessor assigns the incoming value to the backing field. You can also use auto-implemented properties when no additional logic is needed:

public double Celsius { get; set; }

Properties are ideal for exposing a single logical value, such as a name, a count, or a calculated result. They are discoverable through IntelliSense and clearly communicate what they represent.

Indexers: Accessing Elements by an Index

An indexer allows an instance of a class or struct to be indexed like an array. The syntax uses the this keyword followed by a parameter list in square brackets. The parameter can be an integer, a string, or any other type, and you can define multiple parameters for multi-dimensional access.

public class StringCollection { private string[] _items = new string[10]; public string this[int index] { get => _items[index]; set => _items[index] = value; } }

With this indexer, you can write:

var collection = new StringCollection(); collection[0] = "Hello"; string first = collection[0];

Indexers are particularly useful for classes that wrap collections, such as custom lists, dictionaries, or lookup tables. They provide a natural, array-like access pattern that feels familiar to users of the class.

Key Differences Between Properties and Indexers

While both properties and indexers use accessors, they differ in several fundamental ways. The table below summarizes the main distinctions:

AspectPropertyIndexer
Access syntaxobj.Propertyobj[index]
ParametersNoneOne or more (the index)
NameNamed identifierUses this keyword
PurposeSingle valueCollection of values
OverloadingNot possible (same name)Can overload by index type

A property is always accessed by its name, which makes it self-documenting. An indexer has no name; it uses the this keyword, so the class itself acts as the collection. This means an indexer is appropriate when the class represents a collection or a container, while a property is appropriate for a single attribute.

Another subtle difference is that indexers can be overloaded based on the type and number of index parameters. For example, you can have both this[int] and this[string] in the same class. Properties, on the other hand, cannot be overloaded; you would need different property names.

When to Use an Indexer Instead of a Property

Choosing between an indexer and a property depends on the nature of the data you are exposing. Use an indexer when your class is conceptually a collection, a list, a dictionary, or a mapping. For instance, a ShoppingCart class that holds a list of items could expose an indexer to allow direct item access by position or product ID.

public class ShoppingCart { private List<CartItem> _items = new(); public CartItem this[int index] { get => _items[index]; set => _items[index] = value; } public CartItem this[string productId] { get => _items.FirstOrDefault(i => i.ProductId == productId); } }

Here, the indexer provides a natural way to retrieve an item by its position or by a key. A property would not be suitable because you would need a separate property for each possible key, which is impractical.

Use a property when the class exposes a single, well-defined value. For example, a Customer class might have a Name property, an Email property, and an IsActive property. Each of these is a distinct attribute, not a collection. Trying to use an indexer for these would make the API confusing and less readable.

Performance and Maintainability Considerations

From a runtime perspective, properties and indexers compile to methods (get_ and set_), but indexers also include parameters. The JIT compiler can often inline simple property accessors, eliminating the method call overhead. Indexers, especially those with bounds checking or validation logic, may not be inlined as aggressively. However, the actual performance difference is usually negligible unless the accessor is called in a tight loop with heavy logic. The key is to keep accessors simple and avoid unnecessary work.

Maintainability is a more significant concern. Properties are more discoverable and self-documenting because they have names. An indexer hides the fact that a class is a collection, which can be useful but also can lead to misuse if the index has unclear semantics. For example, an indexer that accepts a string key but does not throw on missing keys might silently return null, causing subtle bugs. It is important to document the indexer's behavior and validate inputs appropriately.

Another maintainability aspect is that indexers can be overloaded, which adds flexibility but also complexity. If you overload an indexer with multiple types, ensure that the meaning of each index is clear and consistent. Overloading with an int and a string might be intuitive for a dictionary-like class, but overloading with two different numeric types could be confusing.

Common Mistakes and How to Avoid Them

One common mistake is using an indexer for a single value that would be better represented as a property. For example, exposing a Name field via obj[0] is poor design because it obscures the meaning. Always prefer a named property for scalar values.

Another mistake is forgetting to validate the index. An indexer that directly accesses an array without checking bounds will throw an IndexOutOfRangeException. While that exception is acceptable in some cases, a well-designed indexer should throw a more descriptive exception, such as ArgumentOutOfRangeException or KeyNotFoundException, depending on the context.

public string this[int index] { get { if (index < 0 || index >= _items.Length) throw new ArgumentOutOfRangeException(nameof(index)); return _items[index]; } }

This makes the failure mode explicit and helps callers understand what went wrong. Similarly, if the indexer is used for a lookup, consider returning null or a default value only if that is the intended behavior; otherwise, throw an exception to signal the missing key.

Compatibility and Version Considerations

Indexers have been part of C# since version 1.0, and their behavior has remained stable across all subsequent versions. This means code that uses indexers today will work in older and newer .NET environments without modification. Properties have also been stable, but modern C# introduced enhancements like expression-bodied members and init-only setters, which can be used with both properties and indexers.

One version-related nuance is that indexers in interfaces were not allowed until C# 8.0. If you are targeting an older C# version, you cannot declare an indexer in an interface. However, you can still implement an indexer in a class that implements an interface that defines a method like GetItem or SetItem. This is a rare limitation, but worth knowing if you work with legacy codebases.

Advanced Usage: Overloading Indexers and Using Multiple Parameters

Indexers support multiple parameters, which is useful for multi-dimensional collections. For example, a matrix class can expose a two-dimensional indexer:

public class Matrix { private int[,] _values; public Matrix(int rows, int columns) { _values = new int[rows, columns]; } public int this[int row, int col] { get => _values[row, col]; set => _values[row, col] = value; } }

You can also overload indexers to accept different types. A common pattern is to allow both integer and string indices, as seen in dictionary-like classes:

public class LookupTable { private Dictionary<string, int> _map = new(); public int this[string key] { get => _map[key]; set => _map[key] = value; } public int this[int index] { get => _map.Values.ElementAt(index); set => _map[_map.Keys.ElementAt(index)] = value; } }

This flexibility allows callers to access data in the way that is most natural for their use case. However, overloading indexers should be done with care. The semantics of each index must be clearly documented, and the behavior should be consistent. For instance, the integer index in the example above assumes a stable ordering of the dictionary keys, which is not guaranteed in .NET. A better design would use an ordered collection or expose a method instead.

Designing a Class with Both Properties and Indexers

A well-designed class often uses both properties and indexers together. Consider a BookCollection class that stores a list of books and also exposes metadata about the collection:

public class BookCollection { private List<Book> _books = new(); public int Count => _books.Count; public string CollectionName { get; set; } public Book this[int index] { get => _books[index]; set => _books[index] = value; } public Book this[string isbn] { get => _books.FirstOrDefault(b => b.ISBN == isbn); } }

Here, Count and CollectionName are properties that describe the collection as a whole, while the indexers provide access to individual books. This combination gives a clear API: callers can read the collection's name and count, and they can retrieve books by position or by ISBN. The indexer with a string key returns null if no match is found, which is a deliberate design choice; if you prefer an exception, you can throw KeyNotFoundException instead.

When you design your own classes, ask yourself: does this member represent a single attribute or a way to access elements within a collection? The answer will guide you toward the right choice. Using properties for scalar values and indexers for collection access keeps your code intuitive and maintainable.

c# property vs indexer: Practical Usage and Code Examples | RYUSLOG DEV