1. A Few Words About Collections
So far, we've been working with arrays — that's the simplest way to store a bunch of elements of the same type. But in C#, there are tons of more convenient and powerful collections that solve all sorts of tasks.
Collection — that's just an object that can store a group of other objects. Unlike arrays, collections can usually change their size dynamically, give you handy methods for working with data, and are optimized for different use cases.
Main Types of Collections (Quick Overview):
List<T> — a dynamic array that can grow and shrink:
List<string> names = new List<string>();
names.Add("Alex");
names.Add("Maria");
Console.WriteLine(names[0]); // Alex
Console.WriteLine(names.Count); // 2
Dictionary<TKey, TValue> — a collection of "key-value" pairs, where each key matches up with a single value:
Dictionary<string, int> ages = new Dictionary<string, int>();
ages["Alex"] = 25;
ages["Maria"] = 30;
Console.WriteLine(ages["Alex"]); // 25
Heads up: in the examples above, we're using square brackets [] to access collection elements — just like with arrays! That's possible thanks to indexers, which is exactly what we're talking about today.
This is just your first taste of collections — we'll dive deeper into their features, differences, and uses in future lectures. For now, just get that collections let you access their elements with square brackets, and that's not magic — it's a special language feature.
2. Indexers
Normal C# objects work through properties and methods. But what if your object is kind of a mini-collection? For example, imagine:
- You're writing a Week class that should return the name of the day by its number: week[0] → "Monday".
- Or a Library class, where you can get a book by its number: library[3] → "Swann's Way".
Sounds handy, right? It'd be weird to write library.GetBookByIndex(3) every time — you wanna treat your object like an array!
That's where indexers come in.
Indexer — that's a special class member that lets you use objects of that class with square bracket syntax, just like arrays: obj[0], obj["key"], and so on.On the outside, an indexer looks like a property, but instead of a name — it takes parameters inside square brackets. It's kinda like writing .Name, but instead you do [i].
3. Simple Collection with an Indexer
Let's put together a mini-class that stores favorite colors. We'll use a string array for storage. Without an indexer, you'd have to make a method like GetColor(int i). But with an indexer:
using System;
public class FavoriteColors
{
// Private field for storing colors
private string[] colors = new string[5];
// Indexer:
public string this[int index]
{
get
{
// Array bounds check (encapsulation in action!)
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Invalid color index!");
return colors[index];
}
set
{
if (index < 0 || index >= colors.Length)
throw new IndexOutOfRangeException("Invalid color index!");
colors[index] = value ?? throw new ArgumentNullException(nameof(value));
}
}
}
class Program
{
static void Main()
{
FavoriteColors favorites = new FavoriteColors();
favorites[0] = "Green";
favorites[1] = "Blue";
favorites[2] = "Red";
favorites[10] = "Violet"; // Throws exception!
Console.WriteLine(favorites[1]); // Blue
}
}
What's going on here?
- We're making a private array so nobody can mess with it directly from outside.
- The indexer is defined as public string this[int index], where this means the indexer belongs to the object.
- Inside get and set we check the range, don't let you go out of bounds or write null.
- In the end, we can do favorites[0], just like with a regular array.
4. Indexer Syntax: Details
The syntax is like a property, but instead of a name (like Age) you use the keyword this with parameters:
// Indexer signature (general template)
[modifier] ResultType this[IndexType indexName]
{
get { ... }
set { ... }
}
Example: Classic
public class MyCollection
{
private int[] data = new int[10];
// Indexer for reading and writing
public int this[int index]
{
get { return data[index]; }
set { data[index] = value; }
}
}
Indexers Aren't Just for int
The cool thing: an indexer doesn't have to just take int. You can use any type (as long as accessing by key makes sense):
public string this[string colorName]
{
get { /* ... */ }
set { /* ... */ }
}
For example, in a phone book class, it makes sense to look up by name:
public class PhoneBook
{
private Dictionary<string, int> entries = new Dictionary<string, int>();
public int this[string name]
{
get
{
if (entries.ContainsKey(name))
return entries[name];
return null;
}
set
{
entries[name] = value;
}
}
}
I'll talk more about collections and how Dictionary<string, string> works in future lectures :P
5. Practical Example: Word Counter in Text
Let's keep building our app. Say now we've got a class that counts how many times each word appears in a text. It's super handy if the user can access the object with square brackets to get the count by word:
using System.Collections.Generic;
public class WordCounter
{
private Dictionary<string, int> counter = new Dictionary<string, int>();
// Indexer by string (word)
public int this[string word]
{
get
{
if (counter.ContainsKey(word))
return counter[word];
return 0; // If the word isn't there, return 0.
}
set
{
counter[word] = value;
}
}
// Method to count words from a string
public void AddWords(string text)
{
foreach (var word in text.Split(' ', System.StringSplitOptions.RemoveEmptyEntries))
{
if (counter.ContainsKey(word))
counter[word]++;
else
counter[word] = 1;
}
}
}
// In Main:
var wc = new WordCounter();
wc.AddWords("mama washed the frame washed mama papa");
Console.WriteLine($"'mama' appears {wc["mama"]} time(s)");
Console.WriteLine($"'frame' appears {wc["frame"]} time(s)");
Console.WriteLine($"'cat' appears {wc["cat"]} time(s)"); // 0
Why does this matter in real life? This approach is often used for making your own collections, memory libraries, mappings (dictionaries and indexes), and even DSLs (specialized languages inside C#).
6. Limitations and Gotchas
Indexers are powerful, but there are a few rules and gotchas (because of course there are).
Indexers Don't Have Names
Unlike a property, an indexer doesn't have a name, just a signature like this[type parameter]. If you try to write public int MyIndexer[int i] — the compiler will freak out. Only use this.
No Static Indexers
Indexers are always for an instance of a class, not for static members. So you can't declare static int this[int i] — the logic is, this always points to a specific object instance.
You Can Overload by Type/Number of Parameters
You can make several indexers in one class if their parameters differ by type or count. For example:
public string this[int i] { get { ... } set { ... } }
public string this[string key] { get { ... } set { ... } }
That's totally legal, the compiler won't get confused — and it'll let you know if the parameters overlap.
Only Supported via Properties
You can't declare an indexer without get or set accessors. If you want read-only — just leave out set, write-only — leave out get. Usually, you use both.
7. Practical Value and Why You Should Care
- Indexers are used all over data collections. Tons of .NET classes have them: for example, List<T>, Dictionary<TKey,TValue>. When you write list[2], you're using an indexer!
- Indexers let you hide the internal implementation (encapsulation!), but give a convenient and familiar interface. The user of your class doesn't care how you store the data, they just use the familiar [index].
- Your code gets concise and intuitive — which is a big plus in interviews (and your future teammates will love it).
Properties vs Indexers: Comparison
| Property | Indexer | |
|---|---|---|
| Name | Yes (like Name) | No (instead — this[parameter]) |
| Access | By name | By index (or other key) |
| Static | Can be static | Instance only |
| Multiple per class | Yes, any number | Yes, but with different parameter signatures |
| Usage | Storing/accessing data | Mini-collections, associative data |
8. Typical Mistakes and Tips
Mistake #1: trying to make an indexer static.
Can't do that — this[...] only works with an object.
Mistake #2: forgot to check the index.
If you don't check array bounds in get or set, your program might crash.
Mistake #3: mixed up parameter types.
If you make two indexers with the same parameters, the compiler will throw an error.
Mistake #4: forgot to implement get or set.
If you want both read and write access — you gotta implement both.
Tip: if your class wraps an array — just pass the access through the indexer. It'll speed things up and make your code super intuitive.
9. Why Bother Learning This
Indexers make your object's interface simple, clear, and convenient. They let you hide the internal implementation, but give the developer an easy way to access data.
That's exactly how built-in collections work: string, List<T>, Dictionary<TKey, TValue>, Span<T> and many more. When you write array[2] or text[0], you're already using an indexer.
And the best part — now you can write your own classes that work just as flexibly and cleanly. That means you're one step closer to writing professional, readable code.
GO TO FULL VERSION