Search code examples
c#propertiesindexed-properties

Easy creation of properties that support indexing in C#


In C# I find indexed properties extremely useful. For example:

var myObj = new MyClass();
myObj[42] = "hello"; 
Console.WriteLine(myObj[42]);

However as far as I know there is no syntactic sugar to support fields that themselves support indexing (please correct me if I am wrong). For example:

var myObj = new MyClass();
myObj.field[42] = "hello"; 
Console.WriteLine(myObj.field[42]);

The reason I need this is that I am already using the index property on my class, but I have GetNumX(), GetX(), and SetX() functions as follows:

public int NumTargetSlots {  
    get { return _Maker.NumRefs; }  
}
public ReferenceTarget GetTarget(int n) {
    return ReferenceTarget.Create(_Maker.GetReference(n));
}
public void SetTarget(int n, ReferenceTarget rt) {
    _Maker.ReplaceReference(n, rt._Target, true);
}

As you can probably see exposing these as one indexable field property would make more sense. I could write a custom class to achieve this every time I want the syntactic sugar but all of the boilerplate code just seem unnecessary.

So I wrote a custom class to encapsulate the boilerplate and to make it easy to create properties that can be indexed . This way I can add a new property as follows:

public IndexedProperty<ReferenceTarget> TargetArray  {
    get { 
       return new IndexedProperty<int, ReferenceTarget>(
           (int n) => GetTarget(n), 
           (int n, ReferenceTarget rt) => SetTarget(n, rt));
       }
}

The code for this new IndexedProperty class looks like:

public class IndexedProperty<IndexT, ValueT>
{
    Action<IndexT, ValueT> setAction;
    Func<IndexT, ValueT> getFunc;

    public IndexedProperty(Func<IndexT, ValueT> getFunc, Action<IndexT, ValueT> setAction)
    {
        this.getFunc = getFunc;
        this.setAction = setAction;
    }

    public ValueT this[IndexT i]
    {
        get {
            return getFunc(i);
        }
        set {
            setAction(i, value);
        }
    }
}

So my question is: is there a better way to do all of this?

Well to be specific, is there a more idiomatic way in C# to create an indexable field property, and if not how could I improve my IndexedProperty class?

EDIT: After further research, Jon Skeet calls this a "named indexer".


Solution

  • I found your idea useful, so I extended it. This may not technically be a proper answer since I'm not sure it squarely answers your question, but I thought it might be useful to people who came here looking for property indexers.

    First, I needed to be able to support get-only and set-only properties, so I made a slight variation of your code for these scenarios:

    Get and Set (very minor changes):

    public class IndexedProperty<TIndex, TValue>
    {
        readonly Action<TIndex, TValue> SetAction;
        readonly Func<TIndex, TValue> GetFunc;
    
        public IndexedProperty(Func<TIndex, TValue> getFunc, Action<TIndex, TValue> setAction)
        {
            this.GetFunc = getFunc;
            this.SetAction = setAction;
        }
    
        public TValue this[TIndex i]
        {
            get
            {
                return GetFunc(i);
            }
            set
            {
                SetAction(i, value);
            }
        }
    }
    

    Get Only:

    public class ReadOnlyIndexedProperty<TIndex, TValue>
    {
        readonly Func<TIndex, TValue> GetFunc;
    
        public ReadOnlyIndexedProperty(Func<TIndex, TValue> getFunc)
        {
            this.GetFunc = getFunc;
        }
    
        public TValue this[TIndex i]
        {
            get
            {
                return GetFunc(i);
            }
        }
    }
    

    Set Only:

    public class WriteOnlyIndexedProperty<TIndex, TValue>
    {
        readonly Action<TIndex, TValue> SetAction;
    
        public WriteOnlyIndexedProperty(Action<TIndex, TValue> setAction)
        {
            this.SetAction = setAction;
        }
    
        public TValue this[TIndex i]
        {
            set 
            {
                SetAction(i, value);
            }
        }
    }
    

    Example

    Here's a simple usage example. I inherit from Collection and create a named indexer, as Jon Skeet called it. This example is intended to be simple, not practical:

    public class ExampleCollection<T> : Collection<T>
    {
        public ExampleCollection()
        {
            ExampleProperty = new IndexedProperty<int, T>(GetIndex, SetIndex);
        }
    
        public IndexedProperty<int, T> ExampleProperty { get; }
    
        private T GetIndex(int index)
        {
            return this[index];
        }
        private void SetIndex(int index, T value)
        {
            this[index] = value;
        }
    }
    

    ExampleCollection in the Wild

    This hastily constructed unit test shows how it looks when you ExampleCollection in a project:

    [TestClass]
    public class IndexPropertyTests
    {
        [TestMethod]
        public void IndexPropertyTest()
        {
            var MyExample = new ExampleCollection<string>();
            MyExample.Add("a");
            MyExample.Add("b");
    
            Assert.IsTrue(MyExample.ExampleProperty[0] == "a");
            Assert.IsTrue(MyExample.ExampleProperty[1] == "b");
    
            MyExample.ExampleProperty[0] = "c";
    
            Assert.IsTrue(MyExample.ExampleProperty[0] == "c");
    
        }
    }
    

    Finally, if you want to use the get-only and set-only versions, that looks like this:

        public ExampleCollection()
        {
            ExampleProperty = new ReadOnlyIndexedProperty<int, T>(GetIndex);
        }
    
        public ReadOnlyIndexedProperty<int, T> ExampleProperty { get; }
    

    Or:

        public ExampleCollection()
        {
            ExampleProperty = new WriteOnlyIndexedProperty<int, T>(SetIndex);
        }
    
        public WriteOnlyIndexedProperty<int, T> ExampleProperty { get; }
    

    In both cases, the result works the way you would expect a get-only/set-only property to behave.