为什么 C # 不实现索引属性?

我知道,我知道... 埃里克 · 利伯特对这类问题的回答通常是“ 因为它不值得设计、实现、测试和记录它的成本”。

但是,我仍然想要一个更好的解释... ... 我正在阅读 这篇关于 C # 4新特性的博文,在 COM 互操作部分,以下部分引起了我的注意:

顺便说一下,这段代码使用了另一个新特性: 索引属性(仔细查看 Range 后面的那些方括号)但是这个特性只适用于 COM 互操作; 您不能在 C # 4.0中创建自己的索引属性.

好吧,但是为什么?我已经知道并且很遗憾不能在 C # 中创建索引属性,但是这个句子让我重新思考它。我可以找到几个很好的理由来实施它:

  • CLR 支持它(例如,PropertyInfo.GetValue有一个 index参数) ,所以很遗憾我们不能在 C # 中利用它
  • 如本文所示(使用克劳斯·福尔曼) ,它支持 COM 互操作
  • 它是在 VB.NET 中实现的
  • 现在已经可以创建索引器,也就是对对象本身应用索引,所以将这个想法扩展到属性,保持相同的语法,只用属性名替换 this,可能没什么大不了的

它允许写这样的东西:

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

目前我所知道的唯一解决办法是创建一个实现索引器的内部类(例如 ValuesCollection) ,并更改 Values属性,使其返回该内部类的一个实例。

这是非常容易做到的,但恼人... 所以也许编译器可以为我们做到这一点!一个选项是生成一个实现索引器的内部类,并通过一个公共泛型接口公开它:

// 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;
}
}
}

但是当然,在这种情况下,属性 事实上将返回一个 IIndexer<int, string>,并且不会真正成为一个索引属性... ... 最好生成一个真正的 CLR 索引属性。

您认为如何? 您想在 C # 中看到这个特性吗? 如果不想,为什么?

29960 次浏览

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.

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) {...}

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.

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.

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.

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.

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;
}
}

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];

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)