We have now come to the part in the C# in Simple Terms series where we can explore some cool but little-used C# features. Among these is the ability to access values in a class instance in the same way we access array values; we do this using a C# feature called indexers.
So, let's build some indexers!
The Sample Project
Indexers
Indexers allow C# class instances to be indexed like arrays. This means we can access a particular value without explicitly specifying the type of said value.
This is particularly useful when defining custom collection classes. Let's say we create a custom collection class called IntMathCollection
which keeps the elements of the collection in an array:
public class IntMathCollection
{
private int[] _values = new int[1000];
}
For simplicity, let's populate the collection of _values
with randomly-generated integers 1-10000.
public class IntMathCollection
{
private int[] _values = new int[1000];
public IntMathCollection()
{
Random rand = new Random(DateTime.Now.Millisecond);
for(int i = 0; i< 1000; i ++)
{
_values[i] = rand.Next(1,10000);
}
}
}
Setup
The reason we might have such a class is to find statistics about the collection of integers, such as the mean, median, and mode. Let's implement some methods to find these statistics.
The mean is a simple average:
public class IntMathCollection
{
//...Other methods and properties
public double Mean()
{
return _values.ToList().Average();
}
}
The median is the number that appears in the middle of an ordered list of values. Since this array will always have an even number of values, we return the average of the two numbers in the middle of the ordered array.
public class IntMathCollection
{
//...Other methods and properties
public int Median()
{
var orderedValues = _values.ToList().OrderBy(x => x).ToArray();
var value1 = orderedValues[499];
var value2 = orderedValues[500];
return (value1 + value2) / 2;
}
}
Finally, the mode is the single value that appears the most number of times in the collection. We can group the values and then order them using LINQ to find the mode.
public class IntMathCollection
{
//...Other methods and properties
public int Mode()
{
return _values.GroupBy(v => v)
.OrderByDescending(g => g.Count())
.First()
.Key;
}
}
The implementation of these methods provides a reason for this class to exist separately of another, more generic collection like List<T>
.
Creating an Indexer on a Class
If we wanted to access a specific value in the IntMathCollection
instance, we are currently unable to, since the class does not define a way to do this. We can create an indexer using the this
keyword, and access individual values using the value
keyword.
public class IntMathCollection
{
public int this[int i]
{
get { return _values[i]; }
set { _values[i] = value; }
}
//...Other methods and properties
}
Note that an indexer is really just a special property of the class, complete with get
and set
accessors.
We can use this new indexer like so:
IntMathCollection myInts = new IntMathCollection();
Console.WriteLine("Element at position 67 is " + myInts[67]);
Console.WriteLine("Element at position 813 is " + myInts[813]);
Console.WriteLine("Element at position 490 is " + myInts[490]);
Using Expression Body Members
Since C# 7.0, we have been able to implement set
accessors using the expression body member syntax (we could implement get
accessors using this syntax before C# 7.0). Our indexer can now be simplified to the following:
public class IntMathCollection
{
public int this[int i]
{
get => _values[i];
set => _values[i] = value;
}
//...Other methods and properties
}
Creating an Indexer on an Interface
We can also create an indexer on an interface using a slightly different syntax.
public interface IHasIndexer
{
string this[int index]
{
get;
set;
}
}
We discussed interfaces in a previous post in this series:
Classes which implement this interface must then provide the implementation for the indexer.
public class HasIndexer : IHasIndexer
{
private string[] _values = new string[10];
public string this[int index]
{
get => _values[index];
set => _values[index] = value;
}
}
Other Details
It is also possible to do the following:
- Create an Indexer with more than one index value, e.g. for accessing two-dimensional arrays.
- Use values other than
int
for the index (likestring
,double
, or other primitive types).
Check out the official Microsoft documentation for more.
New Keyword Usage
this
- Used in indexers to create an indexer.value
- Used inset
accessors to access a given value.
Summary
Indexers in C# allow class instances to be accessed like arrays. We create an indexer using the this
keyword and get the value being given with the value
keywords. We can implement indexers on both classes and interfaces, with interfaces needing a slightly different syntax.
Got questions about indexers? I'm here to help! Ask away in the comments below.
In the next part of this series, we will discuss another C# feature we missed the first time around but that you probably will use pretty darn often: iterators.
Happy Coding!