Bird
Raised Fist0
C Sharp (C#)programming~15 mins

Indexer with custom types in C Sharp (C#) - Deep Dive

Choose your learning style10 modes available
Overview - Indexer with custom types
What is it?
An indexer in C# lets you access objects like arrays but using custom types as keys instead of just numbers. It works like a property but uses square brackets to get or set values. This means you can use your own classes or structs to look up data inside an object. It makes your code cleaner and more intuitive when working with complex data.
Why it matters
Without indexers using custom types, you would have to write many methods to get or set values based on complex keys, making your code bulky and harder to read. Indexers simplify this by letting you use natural syntax, improving code clarity and reducing errors. This helps when building collections or data structures that need flexible and meaningful access patterns.
Where it fits
Before learning this, you should understand basic classes, properties, and arrays in C#. After mastering indexers with custom types, you can explore advanced collections, operator overloading, and designing APIs that feel natural to use.
Mental Model
Core Idea
An indexer with custom types lets you use your own objects as keys to access data inside another object, just like using an array but smarter.
Think of it like...
It's like having a special locker where instead of just numbers, you use your own shaped keys to open specific compartments inside.
ObjectWithIndexer
┌─────────────────────────┐
│  [CustomKeyType key]    │
│  ┌───────────────────┐  │
│  │ get { return val; }│  │
│  │ set { store val; } │  │
│  └───────────────────┘  │
└─────────────────────────┘

Usage:
obj[customKey] -> accesses value associated with customKey
Build-Up - 7 Steps
1
FoundationUnderstanding Basic Indexers
🤔
Concept: Learn what an indexer is and how it works with simple integer keys.
In C#, an indexer lets you access elements in an object using square brackets like an array. For example: public class Sample { private int[] data = new int[5]; public int this[int index] { get { return data[index]; } set { data[index] = value; } } } You can then do: Sample s = new Sample(); s[0] = 10; int x = s[0];
Result
You can get and set values using s[0], just like an array.
Understanding basic indexers builds the foundation to use more complex keys later.
2
FoundationCreating Custom Key Types
🤔
Concept: Define your own class or struct to use as a key for indexing.
You can create a class or struct to represent a key. For example: public struct Coordinate { public int X, Y; public Coordinate(int x, int y) { X = x; Y = y; } } This custom type can hold two numbers and be used as a key.
Result
You have a new type Coordinate that can represent positions.
Custom types let you represent complex keys beyond simple numbers.
3
IntermediateImplementing Indexer with Custom Type
🤔Before reading on: do you think you can use a custom class as an indexer key without extra code? Commit to your answer.
Concept: Use your custom type as the indexer parameter and handle key equality properly.
To use a custom type as a key, define an indexer like: public class Grid { private Dictionary cells = new Dictionary(); public string this[Coordinate coord] { get => cells.ContainsKey(coord) ? cells[coord] : null; set => cells[coord] = value; } } Coordinate must implement equality correctly for Dictionary to work well.
Result
You can now do: Grid g = new Grid(); g[new Coordinate(1,2)] = "Tree"; string val = g[new Coordinate(1,2)]; // "Tree"
Knowing that indexers can use any type as key unlocks flexible data access patterns.
4
IntermediateEnsuring Correct Key Equality
🤔Before reading on: do you think two different instances with same data are equal by default? Commit to your answer.
Concept: Override Equals and GetHashCode in your custom key type to ensure keys behave correctly in collections.
By default, two objects are equal only if they are the same instance. For keys, you want two objects with same data to be equal: public struct Coordinate { public int X, Y; public Coordinate(int x, int y) { X = x; Y = y; } public override bool Equals(object obj) { if (!(obj is Coordinate)) return false; Coordinate other = (Coordinate)obj; return X == other.X && Y == other.Y; } public override int GetHashCode() { return HashCode.Combine(X, Y); } } This ensures Dictionary keys work as expected.
Result
Two Coordinate instances with same X and Y are treated as equal keys.
Correct equality is crucial for indexers using custom types to avoid subtle bugs.
5
IntermediateUsing Indexers with Reference Type Keys
🤔
Concept: You can also use classes as keys, but must implement equality carefully.
If your key is a class, override Equals and GetHashCode similarly: public class Person { public string Name; public Person(string name) { Name = name; } public override bool Equals(object obj) { if (!(obj is Person)) return false; return Name == ((Person)obj).Name; } public override int GetHashCode() { return Name.GetHashCode(); } } Then use Person as indexer key in a Dictionary.
Result
You can index by Person objects, and keys with same Name are equal.
Reference types need explicit equality to behave as keys in indexers.
6
AdvancedHandling Missing Keys Gracefully
🤔Before reading on: do you think accessing a missing key throws an error or returns null? Commit to your answer.
Concept: Design your indexer to handle keys that don't exist without crashing the program.
In the indexer get accessor, check if the key exists before returning: public string this[Coordinate coord] { get { if (cells.TryGetValue(coord, out var value)) return value; else return ""; // or null or throw custom exception } set { cells[coord] = value; } } This prevents exceptions when keys are missing.
Result
Accessing a missing key returns empty string instead of error.
Handling missing keys prevents runtime crashes and improves robustness.
7
ExpertPerformance Considerations with Custom Keys
🤔Before reading on: do you think complex keys always slow down indexers significantly? Commit to your answer.
Concept: Understand how key complexity and equality implementation affect lookup speed and memory.
Custom keys with many fields or expensive equality checks can slow down Dictionary lookups. To optimize: - Keep keys small and immutable. - Cache hash codes if keys are immutable. - Use structs for small keys to avoid heap allocations. - Avoid complex logic in Equals. Example: public struct Coordinate { private readonly int hashCode; public int X, Y; public Coordinate(int x, int y) { X = x; Y = y; hashCode = HashCode.Combine(X, Y); } public override int GetHashCode() => hashCode; public override bool Equals(object obj) => obj is Coordinate c && c.X == X && c.Y == Y; } This reduces overhead in lookups.
Result
Faster and more memory-efficient indexer access with optimized keys.
Knowing performance trade-offs helps design scalable indexers with custom keys.
Under the Hood
When you use an indexer with a custom type key, C# internally calls the get or set accessor methods with that key. If the key is used in a Dictionary, the Dictionary uses the key's GetHashCode method to find the storage bucket and then uses Equals to find the exact entry. This means the key's equality and hash code implementations directly affect how the data is stored and retrieved. The indexer syntax is just a convenient way to call these methods without writing explicit function calls.
Why designed this way?
C# indexers were designed to provide array-like syntax for objects to improve readability and usability. Allowing custom types as keys leverages the power of user-defined types to create flexible and expressive APIs. Using Dictionary internally is a natural choice for fast lookups, but it requires proper equality semantics. This design balances ease of use with performance and flexibility.
Caller Code
   │
   ▼
ObjectWithIndexer[this[customKey]]
   │
   ▼
Indexer get/set method
   │
   ▼
Dictionary Lookup
 ┌───────────────┐
 │ GetHashCode() │
 └──────┬────────┘
        │
        ▼
 ┌───────────────┐
 │   Equals()    │
 └──────┬────────┘
        │
        ▼
   Stored Value
Myth Busters - 4 Common Misconceptions
Quick: Do two different instances with identical data automatically behave as equal keys? Commit to yes or no.
Common Belief:If two objects have the same data, they are automatically equal keys in indexers.
Tap to reveal reality
Reality:By default, objects are only equal if they are the same instance unless Equals and GetHashCode are overridden.
Why it matters:Without overriding equality, your indexer may fail to find existing keys, causing bugs and unexpected behavior.
Quick: Does using a class as a key always work the same as using a struct? Commit to yes or no.
Common Belief:Classes and structs behave the same as keys in indexers without extra work.
Tap to reveal reality
Reality:Classes are reference types and require explicit equality overrides; structs are value types and have default field-wise equality but often need overrides for performance.
Why it matters:Misunderstanding this leads to subtle bugs where keys don't match or cause performance issues.
Quick: Does accessing a missing key in an indexer always throw an exception? Commit to yes or no.
Common Belief:Trying to get a value for a missing key always throws an error.
Tap to reveal reality
Reality:It depends on the implementation; you can design the indexer to return null, default values, or throw exceptions.
Why it matters:Assuming exceptions always happen can cause unnecessary try-catch blocks or crashes if not handled properly.
Quick: Does adding complex logic inside Equals affect indexer performance? Commit to yes or no.
Common Belief:Equals method complexity does not impact indexer performance noticeably.
Tap to reveal reality
Reality:Complex Equals methods slow down lookups because they are called frequently during key comparisons.
Why it matters:Ignoring this can cause performance bottlenecks in large collections using custom keys.
Expert Zone
1
Immutable custom keys improve safety and performance by preventing accidental changes that break dictionary lookups.
2
Caching hash codes in immutable structs avoids recalculating expensive hashes on every lookup.
3
Using structs as keys avoids heap allocations but requires careful design to avoid copying overhead.
When NOT to use
Avoid using complex mutable objects as indexer keys because changes can corrupt the internal data structure. Instead, use immutable structs or simple types. For very large or complex keys, consider alternative data structures like tries or databases for efficient lookup.
Production Patterns
In production, indexers with custom keys are common in spatial data structures (e.g., grids with coordinate keys), caching systems keyed by composite objects, and domain models where entities are accessed by complex identifiers. Developers often combine indexers with validation and error handling to ensure robustness.
Connections
Hash Functions
Indexers with custom keys rely on hash functions to quickly locate data in collections.
Understanding how hash functions work helps you design better keys that minimize collisions and improve lookup speed.
Immutable Data Structures
Custom key types used in indexers are often immutable to ensure consistent behavior.
Knowing immutability principles helps prevent bugs caused by changing keys after insertion.
Database Indexing
Using custom keys in indexers is similar to how databases use composite keys to find records efficiently.
Recognizing this connection helps appreciate the importance of key design and equality in software and data storage.
Common Pitfalls
#1Using mutable objects as keys and then changing their fields after insertion.
Wrong approach:Coordinate coord = new Coordinate(1, 2); grid[coord] = "Tree"; coord.X = 3; // Changed key after insertion string val = grid[new Coordinate(1, 2)]; // Fails to find value
Correct approach:Make Coordinate immutable: public struct Coordinate { public readonly int X, Y; public Coordinate(int x, int y) { X = x; Y = y; } } Use without changing fields after insertion.
Root cause:Changing key data after insertion breaks dictionary's internal hashing and lookup.
#2Not overriding Equals and GetHashCode in custom key types.
Wrong approach:public class Person { public string Name; } // Used as key without overrides var dict = new Dictionary(); var p1 = new Person { Name = "Alice" }; var p2 = new Person { Name = "Alice" }; dict[p1] = "Data"; string val = dict[p2]; // Returns null, keys not equal
Correct approach:Override Equals and GetHashCode: public class Person { public string Name; public override bool Equals(object obj) => obj is Person p && p.Name == Name; public override int GetHashCode() => Name.GetHashCode(); }
Root cause:Default reference equality causes logically equal keys to be treated as different.
#3Assuming indexer access always throws on missing keys.
Wrong approach:public string this[Coordinate coord] { get { return cells[coord]; } // Throws if key missing set { cells[coord] = value; } }
Correct approach:public string this[Coordinate coord] { get { return cells.TryGetValue(coord, out var val) ? val : null; } set { cells[coord] = value; } }
Root cause:Not handling missing keys causes runtime exceptions.
Key Takeaways
Indexers let you access data inside objects using square brackets, just like arrays.
You can use your own classes or structs as keys in indexers to create flexible and readable APIs.
Custom key types must correctly implement equality and hashing to work reliably in indexers.
Handling missing keys gracefully prevents runtime errors and improves program stability.
Performance depends on key design; immutable, simple keys with cached hashes are best for fast lookups.

Practice

(1/5)
1.

What is the main purpose of an indexer in a C# class?

Choose the best description.

easy
A. To create a constructor for initializing objects.
B. To define a method that returns the size of a collection.
C. To allow objects to be accessed using array-like bracket syntax with custom keys.
D. To overload the + operator for custom types.

Solution

  1. Step 1: Understand what an indexer does

    An indexer lets you use square brackets [] to access elements inside an object, similar to arrays.
  2. Step 2: Identify the correct description

    Only To allow objects to be accessed using array-like bracket syntax with custom keys. describes this behavior correctly by mentioning bracket syntax and custom keys.
  3. Final Answer:

    To allow objects to be accessed using array-like bracket syntax with custom keys. -> Option C
  4. Quick Check:

    Indexer = bracket access [OK]
Hint: Indexers let you use [] on objects like arrays [OK]
Common Mistakes:
  • Confusing indexers with constructors
  • Thinking indexers define collection size
  • Mixing indexers with operator overloading
2.

Which of the following is the correct syntax to declare an indexer in a C# class using a custom type Key as the index?

public class MyCollection {
    // Indexer declaration here
}
easy
A. public string this[Key key] { get; set; }
B. public string this(Key key) { get; set; }
C. public string this[Key] { get; set; }
D. public string this[Key key] => get; set;

Solution

  1. Step 1: Recall indexer syntax

    Indexers use the keyword this followed by square brackets with the parameter type and name inside.
  2. Step 2: Check each option

    public string this[Key key] { get; set; } correctly uses this[Key key] with get and set accessors. Others have syntax errors or wrong brackets.
  3. Final Answer:

    public string this[Key key] { get; set; } -> Option A
  4. Quick Check:

    Indexer syntax = this[Type name] { get; set; } [OK]
Hint: Indexer uses this[Type name] with get/set inside braces [OK]
Common Mistakes:
  • Using parentheses instead of brackets
  • Omitting parameter name in brackets
  • Using expression-bodied syntax incorrectly
3.

Given the following code, what will be the output?

public record Key(int Id);

public class Store {
    private Dictionary<Key, string> data = new();
    public string this[Key key] {
        get => data.TryGetValue(key, out var val) ? val : "Not found";
        set => data[key] = value;
    }
}

var store = new Store();
store[new Key(1)] = "Apple";
store[new Key(2)] = "Banana";
Console.WriteLine(store[new Key(1)]);
Console.WriteLine(store[new Key(3)]);
medium
A. Not found\nNot found
B. Apple\nBanana
C. Compilation error
D. Apple\nNot found

Solution

  1. Step 1: Understand how the indexer accesses data

    The indexer uses a Dictionary with Key as key type. It returns the value if found, else "Not found".
  2. Step 2: Check the keys used

    Keys with Id 1 and 2 are added. Accessing new Key(1) finds "Apple". Accessing new Key(3) is missing, so returns "Not found".
  3. Final Answer:

    Apple Not found -> Option D
  4. Quick Check:

    Key(1) found, Key(3) missing = Apple, Not found [OK]
Hint: Indexer uses dictionary lookup; missing keys return default [OK]
Common Mistakes:
  • Assuming new Key(1) is different from stored Key(1)
  • Expecting Banana for Key(3)
  • Ignoring TryGetValue fallback
4.

Identify the error in the following code snippet that implements an indexer with a custom type Key:

public class DataStore {
    private Dictionary<Key, int> dict = new();
    public int this[Key key] {
        get { return dict[key]; }
        set { dict.Add(key, value); }
    }
}
medium
A. Using dict.Add in set causes exception if key exists; should use dict[key] = value.
B. Indexer must return string, not int.
C. Key type cannot be used as indexer parameter.
D. Missing constructor for DataStore class.

Solution

  1. Step 1: Analyze the set accessor

    The set accessor uses dict.Add(key, value), which throws an exception if the key already exists.
  2. Step 2: Suggest correct usage

    To update or add, use dict[key] = value; which overwrites existing or adds new key.
  3. Final Answer:

    Using dict.Add in set causes exception if key exists; should use dict[key] = value. -> Option A
  4. Quick Check:

    dict.Add throws if key exists; use dict[key] = value [OK]
Hint: Use dict[key] = value to avoid exceptions on set [OK]
Common Mistakes:
  • Using Add instead of index assignment in set
  • Thinking Key cannot be indexer parameter
  • Confusing return types in indexer
5.

You want to create a class Matrix that uses a custom struct Position as the indexer key to store integer values. The Position struct has two int fields: Row and Col. Which approach correctly implements the indexer to allow getting and setting values by Position?

public struct Position {
    public int Row, Col;
    public Position(int r, int c) { Row = r; Col = c; }
}

public class Matrix {
    private Dictionary<Position, int> values = new();
    // Implement indexer here
}
hard
A. public int this[Position pos] { get => values[pos]; set => values[pos] = value; }
B. public int this[Position pos] { get => values.TryGetValue(pos, out var v) ? v : 0; set => values[pos] = value; }
C. public int this[Position pos] { get { return values[pos]; } set { values.Add(pos, value); } }
D. public int this[int row, int col] { get => values[new Position(row, col)]; set => values[new Position(row, col)] = value; }

Solution

  1. Step 1: Consider safe get behavior

    Using TryGetValue with fallback 0 avoids exceptions if the position key is missing.
  2. Step 2: Check set accessor correctness

    Using values[pos] = value; safely adds or updates the dictionary entry.
  3. Step 3: Evaluate other options

    public int this[int row, int col] { get => values[new Position(row, col)]; set => values[new Position(row, col)] = value; } uses two parameters, not matching the question's single Position key. public int this[Position pos] { get { return values[pos]; } set { values.Add(pos, value); } } uses Add in set, which throws if key exists. public int this[Position pos] { get => values[pos]; set => values[pos] = value; }'s get accessor throws if key missing.
  4. Final Answer:

    public int this[Position pos] { get => values.TryGetValue(pos, out var v) ? v : 0; set => values[pos] = value; } -> Option B
  5. Quick Check:

    Safe get with TryGetValue + index assignment set [OK]
Hint: Use TryGetValue in get to avoid exceptions on missing keys [OK]
Common Mistakes:
  • Using Add in set causing exceptions
  • Not handling missing keys in get
  • Using multiple parameters instead of custom type
Recommended

Ready to prove it?

Pass the test and this pattern moves unknown strong while earning +1 Grip toward your certificate.

10 questions~5 mineasy hard