Easy creation of properties that support indexing

2019-01-16 16:54发布

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

7条回答
女痞
2楼-- · 2019-01-16 17:02

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.

查看更多
走好不送
3楼-- · 2019-01-16 17:06

Try explicitly implemented interfaces, as shown at the 2nd way proposed in a reply here: Named indexed property in C#?

查看更多
我命由我不由天
4楼-- · 2019-01-16 17:09

This is not technically the correct way to use StackOverflow, but I found your idea so useful that I extended it. I thought it would save people time if I posted what I came up with and an example of how to use it.

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 exactly the way you would expect a get-only/set-only property to behave.

查看更多
地球回转人心会变
5楼-- · 2019-01-16 17:10

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.

查看更多
Bombasti
6楼-- · 2019-01-16 17:10

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); }
        }
    }
}
查看更多
叛逆
7楼-- · 2019-01-16 17:12

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.

查看更多
登录 后发表回答