35

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".

Community
  • 1
  • 1
cdiggins
  • 17,602
  • 7
  • 105
  • 102
  • Check http://stackoverflow.com/questions/23081402/named-indexed-property-in-c/23081403#23081403 – Spook Apr 15 '14 at 10:50
  • See https://github.com/dotnet/csharplang/issues/471 for the request to add this functionality and all arguments for and against it. Until today, the developers refuse to add it because they don't see sufficient benefit for the language. – Tobias Knauss Mar 27 '20 at 21:30
  • @OP your first link is not valid anymore – serge Oct 28 '20 at 12:31

6 Answers6

20

EDIT FOR 2022: This continues to get votes, but it probably isn't something I would use today primarily because it does push garbage collection in a way that would not be ideal at scale, if the property was being hit a lot. I remember this being a complicated topic, and I do not want to go deep on researching it right now, but I wonder if indexers could solve this problem today. See: https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/indexers/


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 IndexedProperty<int, T> ExampleProperty
    {
        get
        {
            return new IndexedProperty<int, T>(GetIndex, SetIndex);
        }
    }

    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 ReadOnlyIndexedProperty<int, T> ExampleProperty
    {
        get
        {
            return new ReadOnlyIndexedProperty<int, T>(GetIndex);
        }
    }

Or:

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

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

Brian MacKay
  • 31,133
  • 17
  • 86
  • 125
  • 1
    You're needlessly pushing the GC with the repeated `new IndexedProperty` in the property getter. Also the use of delegates probably prevents a bunch of optimizations. Both may be acceptable in certain use cases, but in general I wouldn't recommend code like that. Writing specialized indexer classes surely is more work, but will perform much better. – Paul Groke Mar 27 '15 at 16:48
  • You example is confusing because you access a collection, whose items already could be accessed without the custom indexer class. Better use a `class CExample { private string[] m_Texts; private string GetText(int index) { return m_Texts[index];}` to demonstrate the common usage. – Tobias Knauss Jul 11 '17 at 11:47
  • You just repeat what's in the question, you make peoples loose their time. Also, you say that it is not the proper way to use Stack Overflow but you don't say why. You comment is totally useless and I would like to understand why you say so? – Eric Ouellet Sep 06 '19 at 19:26
  • I voted too quickly (sorry). I can't change my vote if you don't change your answer. I wonder if you can modify it (very slight modif) in order for me to vote up instead or vote down ? I can see that it could be useful... Please note that a comment about wrong usage of stack overflow without explanation is really chocking me because it does not bring anything useful to anybody... – Eric Ouellet Sep 06 '19 at 19:37
  • @EricOuellet Sure, edited. I put that comment in because technically what I wrote here is not an answer to the question, and on StackOverflow, an answer should typically be an answer. I felt this code might help someone, so I posted it anyway, with a disclaimer. It's a bit of a gray area. That was also 6 years ago so this is ancient... I am not sure I would take this route today. – Brian MacKay Sep 06 '19 at 21:12
  • 1
    I hope you will continue to take that route although it is not perfectly in exact stackoverflow rules... because it helps peoples. Although I did not use your code now, I kept a copy in my general library for future use. Thanks! – Eric Ouellet Sep 06 '19 at 21:47
  • @EricOuellet And thanks for helping me make the answer better. :) I have a feeling there are a number of other ways this could be improved today. – Brian MacKay Sep 06 '19 at 21:50
  • This is wrong ! Now ExampleProperty is readable, although it is not. You break reflection ! PropertyInfo.CanWrite, PropertyInfo.CanRead – Stefan Steiger Dec 01 '20 at 16:15
  • How is this different to having a readonly array? – Toxid Nov 23 '21 at 08:51
9

Well, the simpliest is to have the property return an object which implements IList.

Remember that just because it implements IList doesn't mean it's a collection itself, just that it implements certain methods.

James Curran
  • 101,701
  • 37
  • 181
  • 258
  • Hmmmm... I like this line of thinking. However the problem I have with IList is the members like "Add", "Remove", etc. It would be kind of dishonest to expose the property as an IList just ignore them. Perhaps there is another interface that would make more sense? – cdiggins Jul 27 '10 at 14:35
  • 3
    You can have a read-only `IList`. `Array` and `ReadOnlyCollection` both implement `IList`; both these classes throw exceptions when you call `Add`, `Remove`, etc. – Tim Robinson Jul 27 '10 at 14:38
  • But its not "a read-only" IList. You can set elements at particular indexes. It is distinctly not a list, but neither is Array. However, I did not know that "Array" implemented "IList"! That seems pretty obviously a bad design decision. Of course, it would now classify as "idiomatic C#". I'm on the fence. – cdiggins Jul 27 '10 at 14:43
  • 2
    I've always thought that there should be an interface in the framework specifically for this use case that doesn't imply collection semantics - `IIndexable`, maybe? Even though `IList` specifically gives you an out (via the `IsReadOnly` property), it does imply collection semantics simply by virtue of inheriting from `ICollection`. – Dathan Jul 27 '10 at 14:44
  • If the collection should not be changed by the outside world, use IEnumerable. If the collection will be changed by the outside world, use ICollection. If indexed access is required, use IList. – Gage Jul 27 '10 at 14:49
  • There needs to be a term to distinguish between a collection for which items cannot be added or removed and a collection for which the items it contains cannot be modified. Unfortunately, we use the term "read-only" to describe both. – Dr. Wily's Apprentice Jul 27 '10 at 16:32
7

I think the design you've posted is the way to go, with the one difference that I would define an interface:

public interface IIndexed<IndexT, ValueT>
{
    ValueT this[IndexT i] { get; set; }
}

And for common cases, I would use the class you put in the original question (which would implement this interface).

It would be nice if the base class library provided a suitable interface for us, but it doesn't. Returning an IList here would be a perversion.

quentin-starin
  • 26,121
  • 7
  • 68
  • 86
6

This doesn't answer your question, but it's interesting to note that CIL supports making properties like you've described - some languages (For example, F#) will allow you to define them in such a way too.

The this[] indexer in C# is just a specific instance of one of these which is renamed to Item when you build your app. The C# compiler only knows how to read this one, so if you write a "named indexer" called Target in an F# library, and try to use it in a C#, the only way you could access the property is via the ... get_Target(int) and void set_Target(int, ...) methods. Sucks.

Mark H
  • 13,797
  • 4
  • 31
  • 45
3

Why not have your class inherit IList then you can just use the index and add your own properties to it. Although you will still have the Add and Remove functions its not dishonest not to use them. Plus you may find it useful to have them furthur down the road.

For more information about Lists and Arrays check out: Which is better to use array or List<>?

EDIT:

MSDN has an article on index properties you may want to take a look at. Doesn't seem to complicated just tedious.

http://msdn.microsoft.com/en-us/library/aa288464(VS.71).aspx

There is another option where you can create an alternative Add method but depending on the type of object your add method may not always be called. Explained here:

How do I override List<T>'s Add method in C#?

EDIT 2: Similar to the first post

Why don't you have a hidden list object in your class and then just create your own methods for obtaining the data. That way Add and Remove aren't seen and the list is already indexed.

Also what do you mean by "named indexer" are you looking for the equivalent of the row["My_Column_Name"]. Theres an MSDN article I found that may be useful as it seems to show the basic way to implement that property.

http://msdn.microsoft.com/en-us/library/146h6tk5.aspx

class Test
    {
        private List<T> index;
        public T this[string name]{ get; set; }
        public T this[int i]
        {
            get
            {
                return index[i];
            }
            set
            {
                index[i] = value;
            }
        }
    }
Community
  • 1
  • 1
Gage
  • 7,365
  • 9
  • 47
  • 77
  • I want my types to indicate the intended usage. The problem with using an IList, is that I need to add documentation for any consumers of my code, saying yes I implement IList, but you shouldn't use "Add" or "Remove". I prefer self-documenting code. – cdiggins Jul 27 '10 at 14:48
  • 1
    @cdiggins I'm of two minds about this. I agree with you in principle, but for `IList` and `ICollection`, the framework documentation specifically notes that implementing classes should throw `NotSupportedException` upon a call to the `Add`, `Clear`, `Insert`, `Remove`, or `RemoveAt` methods if the collection is read-only (i.e., if the `IsReadOnly` property returns true). It's reasonable to expect your users to use your code in a way that conforms with the documentation, so I think using `IList` is reasonable - and is actually the recommended way to do what you want. – Dathan Jul 27 '10 at 14:57
  • Hey Gage, I had already linked to that MSDN article in my post. – cdiggins Jul 27 '10 at 17:28
2

After some research, I came up with a slightly different solution that better fitted my needs. The example is a little concocted, but it does suit what I need it to adapt it to.

Usage:

MyClass MC = new MyClass();
int x = MC.IntProperty[5];

And the code to make it work:

public class MyClass
{
    public readonly IntIndexing IntProperty;

    public MyClass()
    {
        IntProperty = new IntIndexing(this);
    }

    private int GetInt(int index)
    {
        switch (index)
        {
            case 1:
                return 56;
            case 2:
                return 47;
            case 3:
                return 88;
            case 4:
                return 12;
            case 5:
                return 32;
            default:
                return -1;
        }
    }

    public class IntIndexing
    {
        private MyClass MC;

        internal IntIndexing(MyClass mc)
        {
            MC = mc;
        }

        public int this[int index]
        {
            get { return MC.GetInt(index); }
        }
    }
}
damichab
  • 162
  • 10