为什么C#不实现索引属性?
我知道,我知道… Eric Lippert对这类问题的回答通常是“ 因为不值得花费devise,实施,testing和logging它 ”。
但是,我仍然想要一个更好的解释…我正在阅读关于新的C#4function的博客文章 ,以及有关COM Interop的部分,以下部分引起了我的注意:
顺便说一下,这段代码使用了一个新的特性:索引属性(仔细看看Range后面的方括号)。 但是这个特性只适用于COM interop; 您不能在C#4.0中创build自己的索引属性 。
好的,但是为什么? 我已经知道并且后悔不可能在C#中创build索引属性,但是这句话让我再次想到它。 我可以看到几个很好的理由来实现它:
- CLR支持它(例如,
PropertyInfo.GetValue
有一个index
参数),所以很遗憾我们不能在C#中利用它。 - 它支持COM interop,如文章中所示(使用dynamic分派)
- 它是在VB.NET中实现的
- 已经有可能创build索引器,也就是将索引应用到对象本身,所以把想法扩展到属性,保持相同的语法并用属性名称replace它可能没什么大不了的
这将允许写这样的事情:
public class Foo { private string[] _values = new string[3]; public string Values[int index] { get { return _values[index]; } set { _values[index] = value; } } }
目前我唯一的解决方法是创build一个实现索引器的内部类( 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#中看到这个function吗? 如果没有,为什么?
以下是我们如何deviseC#4。
首先,我们列出了我们可以考虑添加到语言中的所有可能的function。
然后,我们把这些特征分为“这是坏的,我们不能这样做”,“这很棒,我们必须这样做”,“这是好的,但不要这样做”。
然后,我们看看我们有多less预算来devise,实施,testing,logging,运送和维护“必须拥有”的function,并发现我们已经超出预算100%。
所以我们把一堆东西从“必须”的桶里搬到了“好的”桶里。
索引属性从来没有接近 “必须拥有”列表的顶部。 他们在“好的”名单上很低,并且与“坏主意”名单调情。
我们每一分钟都花费在devise,实施,testing,logging或维护好的function上X是一分钟,我们不能花在真棒functionA,B,C,D,E,F和G上。我们必须无情地排列优先顺序,做最好的function。 索引的属性会很好,但是好的不是任何地方,甚至接近足够好,实际上得到实施。
AC#索引器是一个索引属性。 它被默认命名为Item
(你可以像VB一样引用它),并且你可以用IndexerNameAttribute来改变它。
我不知道为什么,具体来说,它是这样devise的,但似乎是有意的限制。 但是,它与框架devise指南是一致的,它build议使用非索引属性返回成员集合的可索引对象。 即“可索引”是一种types的特征; 如果它可以以多种方式进行索引,那么它应该被分成几种types。
因为你已经可以做到这一点,而且迫使你在面向对象方面进行思考,添加索引属性只会给语言增加更多的噪音。 而另一种方式做另一件事。
class Foo { public Values Values { ... } } class Values { public string this[int index] { ... } } foo.Values[0]
我个人宁愿只看到一种做法,而不是10种方式。 但当然这是一个主观意见。
我曾经赞成索引属性的想法,但后来意识到它会添加可怕的含糊不清,并实际上阻碍function。 索引属性意味着你没有一个子集合实例。 这是好的和坏的。 实现起来不那么麻烦,而且不需要返回封闭所有者类的引用。 但是这也意味着你不能把这个孩子的collections传递给任何东西; 你可能不得不一一列举。 你也不能对此做任何事情。 最糟糕的是,你无法从索引属性中看出它是一个还是一个集合属性。
这个想法是合理的,但这只会导致僵化和突然的尴尬。
我发现在编写干净,简洁的代码时,缺乏索引属性非常令人沮丧。 索引属性与提供索引或提供单独方法的类引用的内涵非常不同。 我发现提供对实现索引属性的内部对象的访问甚至被认为是可以接受的,因为这经常破坏面向对象的关键组件之一:封装。
我经常遇到这个问题,但是我今天又遇到了这个问题,所以我将提供一个真实世界的代码示例。 正在编写的接口和类存储应用程序configuration,这是一个松散相关信息的集合。 我需要添加命名的脚本片段,使用未命名的类索引器将意味着一个非常错误的上下文,因为脚本片段只是configuration的一部分。
如果索引的属性在C#中可用,我可以实现下面的代码(语法是这个[键]改为PropertyName [键])。
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 }
不幸的是索引属性没有实现,所以我实现了一个类来存储它们,并提供了访问。 这是一个不可取的实现,因为在这个领域模型中configuration类的目的是封装所有的细节。 这个类的客户端将按名字访问特定的脚本片段,没有理由对它们进行计数或枚举。
我可以实现这一点:
public string ScriptGet(string name) public void ScriptSet(string name, string value)
我可能应该有,但是这是一个有用的例子,说明为什么使用索引类来替代这个缺失的特性往往不是一个合理的替代。
为了实现与索引属性类似的function,我必须编写下面的代码,您会注意到它相当长,更复杂,因此难以阅读,理解和维护。
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; } }
另一个解决方法是在易于创build支持C#索引的属性中列出,这需要较less的工作。
编辑 :我还应该补充说,在回答原来的问题,如果我们能够完成所需的语法,与图书馆的支持,那么我认为需要有一个非常强大的情况下直接添加到语言,以便尽量减less语言膨胀。
那么我会说,他们没有添加它,因为它不值得devise,实施,testing和logging它的成本。
开玩笑说,这可能是因为解决方法很简单,function从来没有使时间和利益削减。 我不会感到惊讶的是,这看起来像是一个变化。
你也忘了提到一个简单的解决方法就是制定一个常规的方法:
public void SetFoo(int index, Foo toSet) {...} public Foo GetFoo(int index) {...}
有一个简单的通用解决scheme使用lambda来代理索引function
用于只读索引
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); } } }
对于可变的索引
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); } } }
和一个工厂
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); } }
在我自己的代码中,我使用它
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; } } }
并与MoineauFlankContours我能做的一个例子
MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor]; MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];
刚刚发现我自己也可以使用明确实现的接口来实现这一点,如下所示: C#中的命名索引属性? (见答复中显示的第二种方式)