84

I know, I know... Eric Lippert's answer to this kind of question is usually something like "because it wasn't worth the cost of designing, implementing, testing and documenting it".

But still, I'd like a better explanation... I was reading this blog post about new C# 4 features, and in the section about COM Interop, the following part caught my attention :

By the way, this code uses one more new feature: indexed properties (take a closer look at those square brackets after Range.) But this feature is available only for COM interop; you cannot create your own indexed properties in C# 4.0.

OK, but why ? I already knew and regretted that it wasn't possible to create indexed properties in C#, but this sentence made me think again about it. I can see several good reasons to implement it :

  • the CLR supports it (for instance, PropertyInfo.GetValue has an index parameter), so it's a pity we can't take advantage of it in C#
  • it is supported for COM interop, as shown in the article (using dynamic dispatch)
  • it is implemented in VB.NET
  • it is already possible to create indexers, i.e. to apply an index to the object itself, so it would probably be no big deal to extend the idea to properties, keeping the same syntax and just replacing this with a property name

It would allow to write that kind of things :

public class Foo
{
    private string[] _values = new string[3];
    public string Values[int index]
    {
        get { return _values[index]; }
        set { _values[index] = value; }
    }
}

Currently the only workaround that I know is to create an inner class (ValuesCollection for instance) that implements an indexer, and change the Values property so that it returns an instance of that inner class.

This is very easy to do, but annoying... So perhaps the compiler could do it for us ! An option would be to generate an inner class that implements the indexer, and expose it through a public generic interface :

// interface defined in the namespace System
public interface IIndexer<TIndex, TValue>
{
    TValue this[TIndex index]  { get; set; }
}

public class Foo
{
    private string[] _values = new string[3];

    private class <>c__DisplayClass1 : IIndexer<int, string>
    {
        private Foo _foo;
        public <>c__DisplayClass1(Foo foo)
        {
            _foo = foo;
        }

        public string this[int index]
        {
            get { return _foo._values[index]; }
            set { _foo._values[index] = value; }
        }
    }

    private IIndexer<int, string> <>f__valuesIndexer;
    public IIndexer<int, string> Values
    {
        get
        {
            if (<>f__valuesIndexer == null)
                <>f__valuesIndexer = new <>c__DisplayClass1(this);
            return <>f__valuesIndexer;
        }
    }
}

But of course, in that case the property would actually return a IIndexer<int, string>, and wouldn't really be an indexed property... It would be better to generate a real CLR indexed property.

What do you think ? Would you like to see this feature in C# ? If not, why ?

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
  • 1
    I get the feeling this is another one of those *"we get requests for X but not more than for Y"* issues. – ChaosPandion May 10 '10 at 22:32
  • 1
    @ChaosPandion, yes, you're probably right... But this feature would probably be pretty easy to implement, and although it's certainly not a "must have", it definitely falls into the "nice to have" category – Thomas Levesque May 10 '10 at 22:38
  • 4
    Indexers are already a bit annoying from a CLR point of view. They add a new boundary case to code that wants to work with properties, as now any property could potentially have indexer parameters. I think the C# implementation makes sense, as the concept an indexer typically represents is not a property of an object, but rather its 'contents'. If you provide arbitrary indexer properties, you're implying the class can have different groups of content, which naturally leads to encapsulating the complex sub-content as a new class. My question is: why does the CLR provide indexed properties? – Dan Bryant May 11 '10 at 00:15
  • Like the design....+1 – GoldBishop Apr 27 '13 at 18:59
  • 1
    @tk_ thanks for your constructive comment. Are you posting similar comments to all posts about languages that are not Free Pascal? Well, I hope it makes you feel good about yourself... – Thomas Levesque Nov 26 '16 at 19:33
  • 3
    This is one of the few situations where C++/CLI and VB.net are better than C#. I have implemented lots of indexed properties in my C++/CLI code and now on converting it to C# I have to find workarounds for all of them. :-( SUCKS!!! // Your *It would allow to write that kind of things* is what I've done over years. – Tobias Knauss Jul 11 '17 at 09:56
  • 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:37
  • @TobiasKnauss, yes, I saw this issue. And 10 years after asking this question, I tend to agree. – Thomas Levesque Mar 28 '20 at 11:26

9 Answers9

125

Here's how we designed C# 4.

First we made a list of every possible feature we could think of adding to the language.

Then we bucketed the features into "this is bad, we must never do it", "this is awesome, we have to do it", and "this is good but let's not do it this time".

Then we looked at how much budget we had to design, implement, test, document, ship and maintain the "gotta have" features and discovered that we were 100% over budget.

So we moved a bunch of stuff from the "gotta have" bucket to the "nice to have" bucket.

Indexed properties were never anywhere near the top of the "gotta have" list. They are very low on the "nice" list and flirting with the "bad idea" list.

Every minute we spend designing, implementing, testing, documenting or maintaining nice feature X is a minute we can't spend on awesome features A, B, C, D, E, F and G. We have to ruthlessly prioritize so that we only do the best possible features. Indexed properties would be nice, but nice isn't anywhere even close to good enough to actually get implemented.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • 20
    Can I add a vote for putting it on the bad list? I don't really see how the current implementation is much of a limitation when you could just expose a nested type that implements an indexer. I would imagine you'd start seeing a lot of hackery to try to shoehorn something into databinding and properties that should be methods. – Josh May 10 '10 at 22:57
  • 12
    And hopefully auto-implemented INotifyPropertyChanged is way higher on the list than indexed properties. :) – Josh May 10 '10 at 23:00
  • 3
    @Eric, OK, that's what I suspected... thanks for answering anyway ! I guess I can live without indexed properties, as I've done for years ;) – Thomas Levesque May 10 '10 at 23:04
  • @Josh, see this discussion on UserVoice : http://dotnet.uservoice.com/forums/40583-wpf-feature-suggestions/suggestions/478802-modify-the-language-to-allow-for-observable-proper?ref=title – Thomas Levesque May 10 '10 at 23:08
  • I hope that "static indexers" (like Encoding["UTF8"]) will at some point make it to the good list :) – Michael Stum May 11 '10 at 18:34
  • 3
    Eric, why doesn't C# get more people and more budget? Scala progresses really fast.. – Martin Konicek Aug 15 '10 at 12:55
  • 5
    @Martin: I am not an expert on how large software teams are budgets are determined. Your question should be addressed to Soma, Jason Zander or Scott Wiltamuth, I believe all of whom write blogs occasionally. Your comparison to Scala is an apples-to-oranges comparison; Scala does not have most of the costs that C# does; just to name one, it does not have millions of users with extremely important backwards compatibility requirements. I could name you many more factors that could cause massive cost differences between C# and Scala. – Eric Lippert Aug 17 '10 at 06:52
  • 2
    @Martin: And besides, suppose magically our budget increased by 50%. That doesn't change the fact that we still have *hundreds* of times more potential features than budget to implement them. It doesn't change the fact that we still have to prioritize. And it negatively impacts our largest cost of all: the cost of growing the language too big such that it becomes difficult or impossible to design new features that interact smoothly with existing features. More budget to add more features makes that problem *worse*, not *better*. – Eric Lippert Aug 17 '10 at 16:32
  • 2
    @Eric Lippert: Thank you for explanation. You are right about the Scala comparison. The budget could help with the "must have" features that are being postponed now. I understand it is a question for someone else. Thank you – Martin Konicek Aug 19 '10 at 17:57
  • 12
    +1: People could learn a lot about managing software projects by reading this. And it's only a few lines long. – Brian MacKay May 14 '11 at 18:34
  • @Josh "expose a nested type that implements an indexer" doesn't work for structs unless you're willing to create garbage. –  Dec 13 '13 at 15:43
  • @josh or if you want to define an interface which your indexer implements (I think?) – Mr. Boy Jul 27 '16 at 14:28
  • How do you know that A,B,C,... are awesome? Based on the number of posts and workarounds I find on the internet, I am sure that X (indexed properties) has the same level of importance than A and others. It would be just nice to add the same functionality into C# that is already available in C++/CLI! – Tobias Knauss Jul 11 '17 at 11:17
  • 1
    @TobiasKnauss: Well if you would like to advocate for that feature, go join the discussion on github and say why you think that it's more important than other features. How do the designers know what features are awesome? (1) they have excellent taste, and (2) **they constantly talk to users**. – Eric Lippert Jul 11 '17 at 14:22
  • Thanks, I will do so. I just didn't know they are on GitHub. I visited visualstudio.uservoice.com in the past, but there seems to be little to none communication. Is it github.com/dotnet/csharplang ? Have found it there and added my comment. – Tobias Knauss Jul 11 '17 at 14:54
  • @TobiasKnauss: It turns out Github is where the people are! – Eric Lippert Jul 11 '17 at 14:56
23

A C# indexer is an indexed property. It is named Item by default (and you can refer to it as such from e.g. VB), and you can change it with IndexerNameAttribute if you want.

I'm not sure why, specifically, it was designed that way, but it does seem to be an intentional limitation. It is, however, consistent with Framework Design Guidelines, which do recommend the approach of a non-indexed property returning an indexable object for member collections. I.e. "being indexable" is a trait of a type; if it's indexable in more than one way, then it really should be split into several types.

Pavel Minaev
  • 99,783
  • 25
  • 219
  • 289
  • 1
    Thank you! I have repeatedly fought with errors when implementing COM interop interfaces that have a default indxer (DISPID 0) which imports as this[int] but whose name wasn't originally "Item" (sometimes it's "item" or "value", or similar). This compiles and runs anyway, but leads to FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes warnings, CLS-compliance issues (identifiers differing only in case), etc since the name doesn't quite fit. [IndexerName] is all that was needed, but I'd never managed to find it. – puetzk Sep 04 '15 at 19:17
  • Thank you!!! The IndexerName attribute allowed me to finish converting a VB assembly to C# without breaking MSIL signatures. – Jon Tirjan Aug 24 '16 at 02:25
16

Because you can already do it kind of, and it's forced you to think in OO aspects, adding indexed properties would just add more noise to the language. And just another way to do another thing.

class Foo
{
    public Values Values { ... }
}

class Values
{
    public string this[int index] { ... }    
}

foo.Values[0]

I personally would prefer to see only a single way of doing something, rather than 10 ways. But of course this is a subjective opinion.

  • 2
    +1, this is a much better way of implementing it than gumming up the language with VB5 constructs. – Josh May 10 '10 at 22:58
  • 1
    +1 because this is how I would do it. And if you were good you could probably make this generic. – Tony May 10 '10 at 23:48
  • 12
    One problem with that approach is that it's possible for other code to make a copy of the indexer, and it's unclear what the semantics should be if it does. If code says "var userList = Foo.Users; Foo.RemoveSomeUsers(); someUser = userList[5];" should that be what used to be element [5] of Foo (before the RemoveSomeUsers), or after? If one userList[] were an indexed property, it wouldn't have to be exposed directly. – supercat Dec 05 '11 at 23:31
  • Do you like allocating transients? – Joshua Jul 22 '16 at 21:59
9

I used to favor the idea of indexed properties but then realized it would add horrible ambiguity and actually disincentivize functionality. Indexed properties would mean you don't have a child collection instance. That's both good and bad. It's less trouble to implement and you don't need a reference back to the enclosing owner class. But it also means you can't pass that child collection to anything; you'd likely have to enumerate every single time. Nor can you do a foreach on it. Worst of all, you can't tell from looking at an indexed property whether it's that or a collection property.

The idea is rational but it just leads to inflexibility and abrupt awkwardness.

  • Interesting thoughts, even if the answer is a bit late ;). You're making very good points, +1. – Thomas Levesque Aug 26 '12 at 21:53
  • In vb.net, a class can have both an indexed and non-indexed property of the same name [e.g. `Bar`]. The expression `Thing.Bar(5)` would use indexed property `Bar` on `Thing`, while `(Thing.Bar)(5)` would use non-indexed property `Bar` and then use the default indexer of the resulting object. To my bind, allowing `Thing.Bar[5]` to be a property of `Thing`, rather than a property of `Thing.Bar`, is good since, among other things, it's possible that at some moment in time, the meaning of `Thing.Bar[4]` may be clear, but the proper effect of... – supercat Apr 02 '13 at 17:03
  • ...something like `var temp=Thing.Bar; do_stuff_with_thing; var q=temp[4]` may be unclear. Consider also the notion that `Thing` might hold the data `Bar` in a field which could either be a shared immutable object or an unshared mutable object; an attempt to write to `Bar` when the backing field is immutable should make a mutable copy, but an attempt to read from it should not. If `Bar` is a named indexed property, the indexed getter could leave the backing collection alone (whether mutable or not) while the setter could make a new mutable copy if needed. – supercat Apr 02 '13 at 17:07
  • an indexer of a property is not an enumerator - it is a key – George Birbilis May 29 '16 at 01:47
6

I find the lack of indexed properties very frustrating when trying to write clean, concise code. An indexed property has a very different connotation than providing a class reference that's indexed or providing individual methods. I find it a bit disturbing that providing access to an internal object that implements an indexed property is even considered acceptable since that often breaks one of the key components of object orientation: encapsulation.

I run into this problem often enough, but I just encountered it again today so I'll provide a real world code example. The interface and class being written stores application configuration which is a collection of loosely related information. I needed to add named script fragments and using the unnamed class indexer would have implied a very wrong context since script fragments are only part of the configuration.

If indexed properties were available in C# I could have implemented the below code (syntax is this[key] changed to PropertyName[key]).

public interface IConfig
{
    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    string Scripts[string name] { get; set; }
}

/// <summary>
/// Class to handle loading and saving the application's configuration.
/// </summary>
internal class Config : IConfig, IXmlConfig
{
  #region Application Configuraiton Settings

    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    public string Scripts[string name]
    {
        get
        {
            if (!string.IsNullOrWhiteSpace(name))
            {
                string script;
                if (_scripts.TryGetValue(name.Trim().ToLower(), out script))
                    return script;
            }
            return string.Empty;
        }
        set
        {
            if (!string.IsNullOrWhiteSpace(name))
            {
                _scripts[name.Trim().ToLower()] = value;
                OnAppConfigChanged();
            }
        }
    }
    private readonly Dictionary<string, string> _scripts = new Dictionary<string, string>();

  #endregion

    /// <summary>
    /// Clears configuration settings, but does not clear internal configuration meta-data.
    /// </summary>
    private void ClearConfig()
    {
        // Other properties removed for example
        _scripts.Clear();
    }

  #region IXmlConfig

    void IXmlConfig.XmlSaveTo(int configVersion, XElement appElement)
    {
        Debug.Assert(configVersion == 2);
        Debug.Assert(appElement != null);

        // Saving of other properties removed for example

        if (_scripts.Count > 0)
        {
            var scripts = new XElement("Scripts");
            foreach (var kvp in _scripts)
            {
                var scriptElement = new XElement(kvp.Key, kvp.Value);
                scripts.Add(scriptElement);
            }
            appElement.Add(scripts);
        }
    }

    void IXmlConfig.XmlLoadFrom(int configVersion, XElement appElement)
    {
        // Implementation simplified for example

        Debug.Assert(appElement != null);
        ClearConfig();
        if (configVersion == 2)
        {
            // Loading of other configuration properites removed for example

            var scripts = appElement.Element("Scripts");
            if (scripts != null)
                foreach (var script in scripts.Elements())
                    _scripts[script.Name.ToString()] = script.Value;
        }
        else
            throw new ApplicaitonException("Unknown configuration file version " + configVersion);
    }

  #endregion
}

Unfortunately indexed properties are not implemented so I implemented a class to store them and provided access to that. This is an undesirable implementation because the purpose of the configuration class in this domain model is to encapsulate all the details. Clients of this class will be accessing specific script fragments by name and have no reason to count or enumerate over them.

I could have implemented this as:

public string ScriptGet(string name)
public void ScriptSet(string name, string value)

Which I probably should have, but this is a useful illustration of why using indexed classes as a replacement for this missing feature is often not a reasonable substitute.

To implement similar capability as an indexed property I had to write the below code which you'll notice is considerably longer, more complex and thus harder to read, understand and maintain.

public interface IConfig
{
    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    ScriptsCollection Scripts { get; }
}

/// <summary>
/// Class to handle loading and saving the application's configuration.
/// </summary>
internal class Config : IConfig, IXmlConfig
{
    public Config()
    {
        _scripts = new ScriptsCollection();
        _scripts.ScriptChanged += ScriptChanged;
    }

  #region Application Configuraiton Settings

    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    public ScriptsCollection Scripts
    { get { return _scripts; } }
    private readonly ScriptsCollection _scripts;

    private void ScriptChanged(object sender, ScriptChangedEventArgs e)
    {
        OnAppConfigChanged();
    }

  #endregion

    /// <summary>
    /// Clears configuration settings, but does not clear internal configuration meta-data.
    /// </summary>
    private void ClearConfig()
    {
        // Other properties removed for example
        _scripts.Clear();
    }

  #region IXmlConfig

    void IXmlConfig.XmlSaveTo(int configVersion, XElement appElement)
    {
        Debug.Assert(configVersion == 2);
        Debug.Assert(appElement != null);

        // Saving of other properties removed for example

        if (_scripts.Count > 0)
        {
            var scripts = new XElement("Scripts");
            foreach (var kvp in _scripts)
            {
                var scriptElement = new XElement(kvp.Key, kvp.Value);
                scripts.Add(scriptElement);
            }
            appElement.Add(scripts);
        }
    }

    void IXmlConfig.XmlLoadFrom(int configVersion, XElement appElement)
    {
        // Implementation simplified for example

        Debug.Assert(appElement != null);
        ClearConfig();
        if (configVersion == 2)
        {
            // Loading of other configuration properites removed for example

            var scripts = appElement.Element("Scripts");
            if (scripts != null)
                foreach (var script in scripts.Elements())
                    _scripts[script.Name.ToString()] = script.Value;
        }
        else
            throw new ApplicaitonException("Unknown configuration file version " + configVersion);
    }

  #endregion
}

public class ScriptsCollection : IEnumerable<KeyValuePair<string, string>>
{
    private readonly Dictionary<string, string> Scripts = new Dictionary<string, string>();

    public string this[string name]
    {
        get
        {
            if (!string.IsNullOrWhiteSpace(name))
            {
                string script;
                if (Scripts.TryGetValue(name.Trim().ToLower(), out script))
                    return script;
            }
            return string.Empty;
        }
        set
        {
            if (!string.IsNullOrWhiteSpace(name))
                Scripts[name.Trim().ToLower()] = value;
        }
    }

    public void Clear()
    {
        Scripts.Clear();
    }

    public int Count
    {
        get { return Scripts.Count; }
    }

    public event EventHandler<ScriptChangedEventArgs> ScriptChanged;

    protected void OnScriptChanged(string name)
    {
        if (ScriptChanged != null)
        {
            var script = this[name];
            ScriptChanged.Invoke(this, new ScriptChangedEventArgs(name, script));
        }
    }

  #region IEnumerable

    public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
    {
        return Scripts.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

  #endregion
}

public class ScriptChangedEventArgs : EventArgs
{
    public string Name { get; set; }
    public string Script { get; set; }

    public ScriptChangedEventArgs(string name, string script)
    {
        Name = name;
        Script = script;
    }
}
James Higgins
  • 109
  • 1
  • 4
  • I don't really get it. The reason you are not leaking the Config class implementation is because you have an IConfig interface. The reason you *are* leaking the ScriptsCollection is because you did not make an interface... That aspect, being "abstraction leaky" has absolutely nothing to do with indexed properties per see. It *does* require more boilerplate though - I have no beef with that argument. – AnorZaken Oct 04 '21 at 16:53
2

Another workaround is listed at Easy creation of properties that support indexing in C#, that requires less work.

EDIT: I should also add that in response to the original question, that my if we can accomplish the desired syntax, with library support, then I think there needs to be a very strong case to add it directly to the language, in order to minimize language bloat.

Community
  • 1
  • 1
cdiggins
  • 17,602
  • 7
  • 105
  • 102
  • 2
    In answer to your edit: I don't think it would cause language bloat; the syntax is already there for class indexers (`this[]`), they just need to allow an identifier instead of `this`. But I doubt it will ever be included in the language, for the reasons explained by Eric in his answer – Thomas Levesque Jul 27 '10 at 17:31
1

There is a simple general solution using lambdas to proxy the indexing functionality

For read only indexing

public class RoIndexer<TIndex, TValue>
{
    private readonly Func<TIndex, TValue> _Fn;

    public RoIndexer(Func<TIndex, TValue> fn)
    {
        _Fn = fn;
    }

    public TValue this[TIndex i]
    {
        get
        {
            return _Fn(i);
        }
    }
}

For mutable indexing

public class RwIndexer<TIndex, TValue>
{
    private readonly Func<TIndex, TValue> _Getter;
    private readonly Action<TIndex, TValue> _Setter;

    public RwIndexer(Func<TIndex, TValue> getter, Action<TIndex, TValue> setter)
    {
        _Getter = getter;
        _Setter = setter;
    }

    public TValue this[TIndex i]
    {
        get
        {
            return _Getter(i);
        }
        set
        {
            _Setter(i, value);
        }
    }
}

and a factory

public static class Indexer
{
    public static RwIndexer<TIndex, TValue> Create<TIndex, TValue>(Func<TIndex, TValue> getter, Action<TIndex, TValue> setter)
    {
        return new RwIndexer<TIndex, TValue>(getter, setter);
    } 
    public static RoIndexer<TIndex, TValue> Create<TIndex, TValue>(Func<TIndex, TValue> getter)
    {
        return new RoIndexer<TIndex, TValue>(getter);
    } 
}

in my own code I use it like

public class MoineauFlankContours
{

    public MoineauFlankContour Rotor { get; private set; }

    public MoineauFlankContour Stator { get; private set; }

     public MoineauFlankContours()
    {
        _RoIndexer = Indexer.Create(( MoineauPartEnum p ) => 
            p == MoineauPartEnum.Rotor ? Rotor : Stator);
    }
    private RoIndexer<MoineauPartEnum, MoineauFlankContour> _RoIndexer;

    public RoIndexer<MoineauPartEnum, MoineauFlankContour> FlankFor
    {
        get
        {
            return _RoIndexer;
        }
    }

}

and with an instance of MoineauFlankContours I can do

MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];
bradgonesurfing
  • 30,949
  • 17
  • 114
  • 217
1

Well I would say that they haven't added it because it wasn't worth the cost of designing, implementing, testing and documenting it.

Joking aside, its probably because the workarounds are simple and the feature never makes the time versus benefit cut. I wouldn't be surprised to see this appear as a change down the line though.

You also forgot to mention that an easier workaround is just make a regular method:

public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}
Ron Warholic
  • 9,994
  • 31
  • 47
  • Very true. If the property syntax is absolutely vital then you can use Ion's workaround (perhaps with some generics to allow for a variety of return types). Regardless, I think this speaks to the relative ease of getting the same job accomplished without additional language features. – Ron Warholic May 10 '10 at 22:44
0

Just found out myself too that you can use explicitly implemented interfaces to achieve this, as shown here: Named indexed property in C#? (see the second way shown in that reply)

Community
  • 1
  • 1
George Birbilis
  • 2,782
  • 2
  • 33
  • 35