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

时间:2010-05-10 22:30:01

标签: c# language-features indexed-properties

我知道,我知道...... Eric Lippert对这类问题的回答通常类似于“,因为它不值得设计,实施,测试和记录它的成本”。

但是,我还是想要一个更好的解释......我正在阅读this blog post about new C# 4 features,在关于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>

但是,当然,在这种情况下,属性实际返回{{1}},并且实际上不会是索引属性...生成真正的CLR会更好索引属性。

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

9 个答案:

答案 0 :(得分:110)

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

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

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

然后我们研究了设计,实施,测试,记录,发布和维护“必备”功能所需的预算,并发现我们超出预算100%。

所以我们把一堆东西从“得到的”桶中移到了“好有”的桶里。

索引属性永远不会接近“gotta have”列表的顶部。他们在“好”的名单上非常低,并且与“坏主意”列表调情。

我们花在设计,实现,测试,记录或维护好的功能X上的每一分钟都是我们无法在功能A,B,C,D,E,F和G上使用的一分钟。我们必须无情地优先考虑我们只做最好的功能。索引属性会很好,但很好,甚至不能接近足够好以实际实现。

答案 1 :(得分:19)

C#indexer 索引属性。默认情况下它名为Item(您可以从例如VB中引用它),如果需要,可以使用IndexerNameAttribute进行更改。

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

答案 2 :(得分:14)

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

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

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

foo.Values[0]

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

答案 3 :(得分:8)

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

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

答案 4 :(得分: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
}

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

我可以将其实现为:

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

答案 5 :(得分:2)

Easy creation of properties that support indexing in C#列出了另一种解决方法,需要的工作量更少。

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

答案 6 :(得分:1)

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

开玩笑说,这可能是因为解决方法很简单,而且这个功能永远不会让时间与利益减少有关。我不会感到惊讶的是,这看起来似乎是一种变化。

你也忘了提到一个更简单的解决方法就是制定常规方法:

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

答案 7 :(得分:1)

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

对于只读索引

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

答案 8 :(得分:0)

我也发现自己也可以使用明确实现的接口来实现这一点,如下所示: Named indexed property in C#?(参见该回复中显示的第二种方式)