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

Tho*_*que 80 c# language-features indexed-properties

我知道,我知道...... 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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

目前我知道的唯一解决方法是创建一个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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,当然,在这种情况下,属性实际上会返回一个IIndexer<int, string>,而不是真正的索引属性......生成一个真正的CLR索引属性会更好.

你怎么看 ?你想在C#中看到这个功能吗?如果没有,为什么?

Eri*_*ert 113

以下是我们设计C#4的方法.

首先,我们列出了我们可以考虑添加到语言中的每个可能的功能.

然后我们将这些功能分解为"这很糟糕,我们绝不能这样做","这太棒了,我们必须这样做","这很好,但这次不要这样做".

然后我们看看我们有多少预算来设计,实施,测试,记录,发布和维护"必备"功能,并发现我们超出预算100%.

因此,我们将一堆东西从"得到的"桶中移到了"好有"的桶中.

索引属性从未接近 "必须拥有"列表的顶部.他们在"好"的名单上非常低,并且与"坏主意"列表调情.

我们花费在设计,实现,测试,记录或维护好的特征X上的每一分钟都是我们不能花费在令人敬畏的特征A,B,C,D,E,F和G上的一分钟.我们必须无情地优先考虑,以便我们只尽可能做到最好的功能.索引属性会很好,但是好的并不是任何地方甚至接近足够好以实际实现.

  • 我可以加入投票将其列入坏名单吗?当你只是暴露一个实现索引器的嵌套类型时,我真的没有看到当前实现是如何受到限制的.我想你会开始看到很多hackery试图将一些东西变成数据绑定和应该是方法的属性. (18认同)
  • +1:通过阅读本文,人们可以学到很多关于管理软件项目的知识.它只有几行. (12认同)
  • 并且希望自动实现的INotifyPropertyChanged在列表中比索引属性更高.:) (10认同)
  • @Martin:我不是大型软件团队如何确定预算的专家.你的问题应该发给Soma,Jason Zander或Scott Wiltamuth,我相信所有人偶尔会写博客.你与Scala的比较是对苹果到橙子的比较; Scala没有C#的大部分成本; 仅举一例,它没有数百万用户具有极其重要的向后兼容性要求.我可以举出更多可能导致C#和Scala之间巨大成本差异的因素. (5认同)
  • @Eric,好的,这就是我怀疑的......谢谢你回答!我想我可以在没有索引属性的情况下生活,因为我已经做了多年;) (3认同)
  • Eric,为什么C#不会获得更多人和更多预算?Scala进展非常快.. (3认同)
  • @Martin:此外,假设我们的预算增加了50%.这并没有改变这样一个事实,即我们仍然拥有比预算更多的潜在功能*数百倍*来实现它们.它并没有改变我们仍然需要优先考虑的事实.它会对我们最大的成本产生负面影响:语言增长的成本太大,以至于设计与现有功能平滑交互的新功能变得困难或不可能.增加更多功能的预算越多,问题就越严重*,而不是*更好*. (2认同)
  • @Eric Lippert:谢谢您的解释。您对Scala比较是正确的。预算可以帮助推迟现在的“必须具备”功能。我了解这是其他人的问题。谢谢 (2认同)

Pav*_*aev 19

AC#indexer 一个索引属性.它Item默认命名(您可以从VB中引用它),如果需要,可以使用IndexerNameAttribute更改它.

我不确定为什么,特别是它的设计方式,但它似乎是一个故意的限制.但是,它与框架设计指南一致,它建议使用非索引属性的方法返回成员集合的可索引对象.即"可索引"是一种类型的特征; 如果它可以多种方式转换,那么它应该分成几种类型.


Ion*_*rel 14

因为你已经可以做到这一点了,并且它迫使你在OO方面进行思考,添加索引属性只会给语言增加更多噪音.而另一种方法是做另一件事.

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

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

foo.Values[0]
Run Code Online (Sandbox Code Playgroud)

我个人更愿意只看到一种做事的方式,而不是10种方式.但这当然是一种主观意见.

  • 这种方法的一个问题是,其他代码可能会制作索引器的副本,而且如果确实如此,则不清楚语义应该是什么.如果代码说"var userList = Foo.Users; Foo.RemoveSomeUsers(); someUser = userList [5];" 应该是以前的Foo元素[5](在RemoveSomeUsers之前),还是之后?如果一个userList []是索引属性,则不必直接公开. (10认同)
  • +1,这是实现它的一种更好的方式,而不是使用VB5构造语言. (2认同)

小智 8

我过去喜欢索引属性的想法,但后来意识到它会增加可怕的模糊性,实际上会阻碍功能.索引属性意味着您没有子集合实例.这既好又坏.它实现起来不那么麻烦,您不需要引用回到封闭的所有者类.但这也意味着你无法将那个子集合传递给任何东西; 你可能每次都要列举一次.你也不能做一个foreach.最糟糕的是,您无法通过查看索引属性来判断它是否是集合属性.

这个想法是理性的,但它只会导致不灵活和突然的尴尬.


小智 5

我发现在编写干净,简洁的代码时,缺少索引属性非常令人沮丧.索引属性与提供索引或提供单个方法的类引用具有非常不同的含义.我觉得有点令人不安的是,提供对实现索引属性的内部对象的访问甚至被认为是可接受的,因为它经常会破坏面向对象的关键组件之一:封装.

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

如果索引属性在C#中可用,我可以实现以下代码(语法是将此[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
}
Run Code Online (Sandbox Code Playgroud)

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

我可以实现这个:

public string ScriptGet(string name)
public void ScriptSet(string name, string value)
Run Code Online (Sandbox Code Playgroud)

我可能应该这样做,但这是一个有用的例子,说明为什么使用索引类作为这个缺失特征的替代品往往不是一个合理的替代品.

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

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;
    }
}
Run Code Online (Sandbox Code Playgroud)