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


83

我知道,我知道...埃里克·利珀特(Eric Lippert)对此类问题的回答通常类似于“,因为它不值得设计,实施,测试和记录它的成本”。

但是,我仍然希望有一个更好的解释……我正在阅读有关C#4新功能的博客文章,并且在有关COM Interop的部分中,以下部分引起了我的注意:

顺便说一句,此代码使用了另一个新功能:索引属性(仔细研究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#中看到此功能吗?如果没有,为什么?


1
我感到这是“我们收到X要求但不超过Y要求”问题中的另一个问题。
ChaosPandion 2010年

1
@ChaosPandion,是的,您可能是对的...但是此功能可能很容易实现,尽管它当然不是“必须具备”,但它绝对属于“很好拥有”类别
Thomas Levesque

4
从CLR的角度来看,索引器已经有些烦人了。他们向要使用属性的代码添加了新的边界条件,因为现在任何属性都可能具有索引器参数。我认为C#实现是有意义的,因为索引器通常表示的概念不是对象的属性,而是对象的“内容”。如果提供任意的索引器属性,则意味着该类可以具有不同的内容组,这自然导致将复杂的子内容封装为新类。我的问题是:为什么CLR提供索引属性?
丹·布莱恩特

1
@tk_感谢您的建设性评论。您是否在有关非Free Pascal语言的所有帖子中发表了类似的评论?好吧,我希望这会让您对自己感觉良好...
Thomas Levesque

3
这是C ++ / CLI和VB.net比C#更好的少数情况之一。我已经在我的C ++ / CLI代码中实现了很多索引属性,现在将其转换为C#时,我必须找到所有这些的解决方法。:-( SUCKS!//你这将使写那种东西就是我多年来做了。
托比亚斯克瑙斯

Answers:


122

这是我们设计C#4的方法。

首先,我们列出了可以考虑添加到该语言的所有可能功能。

然后,我们将功能分类为“这是不好的,我们绝对不能这样做”,“这很棒,我们必须这样做”和“这很好,但这次我们不要这样做”。

然后,我们查看了设计,实施,测试,记录,运送和维护“必须拥有”功能所需的预算,发现我们超出了预算100%。

因此,我们将一堆东西从“必须拥有”存储桶移到了“不错拥有”存储桶。

索引属性永远不会靠近“必须拥有”列表的顶部。他们在“好”列表上的位置很低,并且在“坏主意”列表上调情。

我们花在设计,实施,测试,记录或维护良好功能X上的每一分钟都是我们无法花在A,B,C,D,E,F和G等出色功能上的每一分钟。尽力而为。索引属性会很不错,但是不错的地方还差得远不能实际实现。


20
我可以添加一票将其列入不良名单吗?当您仅公开实现索引器的嵌套类型时,我真的看不到当前的实现有多大限制。我想您会开始看到很多黑客试图将某些东西塞入数据绑定和应该是方法的属性中。
乔什(Josh)2010年

11
希望自动实现的INotifyPropertyChanged在列表上比索引属性高得多。:)
乔什(Josh)2010年

3
@Eric,好,那是我所怀疑的……谢谢您的回答!我想我可以像过去多年一样没有索引属性;)
Thomas Levesque 2010年

5
@马丁:我不是确定预算多少软件团队的专家。您应该向索马(Soma),杰森·赞德(Jason Zander)或斯科特·威尔塔姆(Scott Wiltamuth)提出问题,我相信所有这些人偶尔都会写博客。您与Scala的比较是从苹果到橘子的比较。Scala没有C#承担的大多数费用;仅举一个例子,它没有数百万个具有极其重要的向后兼容性要求的用户。我可以说出更多因素,这些因素可能导致C#和Scala之间的巨大成本差异。
埃里克·利珀特

12
+1:通过阅读本书,人们可以学到很多有关管理软件项目的知识。而且只有几行。
Brian MacKay

22

AC#索引器一个索引属性。它是Item默认命名的(您可以从VB中引用它的名称),并且可以根据需要使用IndexerNameAttribute对其进行更改。

我不确定为什么要特别设计这种方式,但这似乎确实是有意限制。但是,这与Framework Design Guidelines一致,后者确实建议使用非索引属性返回成员集合的可索引对象的方法。即“可索引”是一种类型的特征;如果它可以以多种方式进行索引,那么实际上应该将其分为几种类型。


1
谢谢!在实现具有默认indxer(DISPID 0)的COM互操作接口时,我反复犯过错误,该indxer导入为this [int],但其名称最初不是“ Item”(有时是“ item”或“ value”,或类似名称) )。无论如何,它都可以编译和运行,但是会导致FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes警告,CLS遵从性问题(标识符仅在大小写不同的情况下)等,因为名称不太适合。仅需要[IndexerName],但我从未设法找到它。
puetzk

谢谢!!!IndexerName属性使我可以完成VB程序集到C#的转换而不会破坏MSIL签名。
乔恩·提尔詹

15

因为您已经可以做到,并且迫使您在OO方面进行思考,所以添加索引属性只会给语言带来更多干扰。而这是做另一件事的另一种方式。

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

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

foo.Values[0]

我个人更希望看到做某事的单一方法,而不是十种方法。但这当然是主观意见。


2
+1,这是一种比使用VB5构造语言更强的实现方法。
乔什(Josh)2010年

1
+1,因为这就是我的做法。如果您表现出色,则可以使它通用。
托尼2010年

10
这种方法的一个问题是,其他代码可能会复制索引器,并且尚不清楚语义应该是什么。如果代码显示为“ var userList = Foo.Users; Foo.RemoveSomeUsers(); someUser = userList [5];” 应该是Foo的元素[5](在RemoveSomeUsers之前)还是之后?如果一个userList []是一个索引属性,则不必直接公开它。
supercat

您喜欢分配瞬变吗?
约书亚

9

我曾经赞成索引属性的概念,但后来意识到这会增加可怕的歧义并实际上消除了激励功能。索引属性意味着您没有子集合实例。既好又坏。实施起来比较麻烦,您不需要对封闭的所有者类的引用。但这也意味着您不能将该子集合传递给任何东西;您可能必须每次都枚举。您也无法在其上进行学习。最糟糕的是,您无法通过查看索引属性来判断是该属性还是集合属性。

这个想法是合理的,但只会导致僵化和突然的尴尬。


有趣的想法,即使答案有点晚;)。您的观点很不错,+ 1。
Thomas Levesque 2012年

在vb.net中,一个类可以同时具有相同名称的索引属性和非索引属性[例如Bar]。表达Thing.Bar(5)将使用索引属性BarThing,而(Thing.Bar)(5)将使用非索引属性Bar,然后使用所得到的对象的默认索引。出于我的考虑,允许Thing.Bar[5]成为的属性Thing,而不是的属性Thing.Bar,是好的,因为,除其他事项外,它可能是在某个时间的时刻,意思Thing.Bar[4]可能是清楚的,但适当的效果...
supercat

...var temp=Thing.Bar; do_stuff_with_thing; var q=temp[4]可能不清楚。还考虑一下Thing可能将数据保存Bar在一个字段中的概念,该字段可以是共享的不可变对象或非共享的可变对象;尝试Bar在后备字段不可变的情况下进行写操作应创建可变的副本,但尝试从中进行读取的操作则不应进行更改。如果Bar是命名的索引属性,则索引的getter可以保留后备集合(无论是否可变),而setter可以根据需要制作新的可变副本。
2013年

属性的索引器不是枚举器-它是关键
乔治·比比里斯

6

我发现在尝试编写简洁明了的代码时缺少索引属性非常令人沮丧。与提供被索引的类引用或提供单个方法相比,索引属性的含义截然不同。我发现令人不安的是,提供对实现索引属性的内部对象的访问甚至被认为是可接受的,因为这通常会破坏面向对象的关键组成部分之一:封装。

我经常遇到这个问题,但是今天我又遇到了这个问题,因此我将提供一个真实的代码示例。编写的接口和类存储应用程序配置,该配置是松散相关信息的集合。我需要添加命名脚本片段,并且使用未命名类索引器将隐含非常错误的上下文,因为脚本片段只是配置的一部分。

如果C#中提供了索引属性,则可以实现以下代码(语法是this [key]更改为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
}

不幸的是,没有实现索引属性,因此我实现了一个类来存储它们并提供对此的访问。这是不理想的实现,因为此域模型中配置类的目的是封装所有细节。此类的客户端将按名称访问特定的脚本片段,并且没有理由对其进行计数或枚举。

我可以实现为:

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

我可能应该拥有,但是这很好地说明了为什么使用索引类代替此缺少的功能通常不是合理的替代方法。

为了实现与索引属性类似的功能,我必须编写以下代码,您会注意到,这段代码更长,更复杂,因此更难于阅读,理解和维护。

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

2

轻松创建支持C#中索引的属性中列出了另一个解决方法,该方法需要较少的工作。

编辑:我还应该补充一点,以回应最初的问题,即如果我们能够在库支持下完成所需的语法,那么我认为需要一个非常有力的案例将其直接添加到语言中,以便尽量减少语言膨胀。


2
回答您的修改:我认为这不会引起语言膨胀;类索引器(this[])的语法已经存在,它们只需要允许使用标识符而不是即可this。但是我怀疑它是否会包含在该语言中,原因是埃里克(Eric)在回答中解释的原因
托马斯·列维斯克

1

好吧,我想说他们没有添加它,因为它不值得设计,实施,测试和记录它的成本。

开个玩笑,这可能是因为解决方法很简单,并且该功能永远不会减少时间和收益。看到这似乎是线下的变化,我不会感到惊讶。

您还忘了提到,更简单的解决方法是仅使用常规方法:

public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}

非常真实 如果属性语法绝对重要,那么您可以使用Ion的解决方法(也许使用一些泛型来允许各种返回类型)。无论如何,我认为这与在没有其他语言功能的情况下完成相同工作相对容易。
罗恩·沃霍里奇

1

有一个简单的通用解决方案,使用lambda代理索引功能

对于只读索引

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

聪明,但是最好是缓存索引器而不是每次都创建它;)
Thomas Levesque 2014年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.