【问题标题】:Why C# doesn't implement indexed properties?为什么 C# 不实现索引属性?
【发布时间】:2011-02-17 21:52:47
【问题描述】:

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

但是,我还是想要一个更好的解释...我正在阅读this blog post about new C# 4 features,在关于 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&lt;int, string&gt;,并且不会真正成为索引属性...最好生成一个真正的 CLR 索引属性.

你怎么看?您想在 C# 中看到此功能吗?如果不是,为什么?

【问题讨论】:

  • 我感觉这是另一个“我们收到对 X 的请求,但不超过 Y”的问题
  • @ChaosPandion,是的,你可能是对的......但是这个功能可能很容易实现,虽然它肯定不是“必须拥有”,但它绝对属于“不错拥有”类别
  • 从 CLR 的角度来看,索引器已经有点烦人了。他们为想要使用属性的代码添加了一个新的边界情况,因为现在任何属性都可能具有索引器参数。我认为 C# 实现是有道理的,因为索引器通常表示的概念不是对象的属性,而是它的“内容”。如果您提供任意索引器属性,则意味着该类可以具有不同的内容组,这自然会导致将复杂的子内容封装为一个新类。我的问题是:为什么 CLR 提供索引属性?
  • @tk_ 感谢您的建设性意见。您是否在所有关于非 Free Pascal 语言的帖子中发布了类似的 cmets?好吧,我希望它能让你对自己感觉良好......
  • 这是 C++/CLI 和 VB.net 比 C# 更好的少数情况之一。我已经在我的 C++/CLI 代码中实现了许多索引属性,现在在将其转换为 C# 时,我必须为所有这些属性找到解决方法。 :-( 糟透了!!! // 你的 它允许写那种东西 是我多年来所做的。

标签: c# language-features indexed-properties


【解决方案1】:

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

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

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

foo.Values[0]

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

【讨论】:

  • +1,这比用 VB5 结构搞砸语言要好得多。
  • +1 因为我会这样做。如果你做得好,你可能会做这个通用的。
  • 这种方法的一个问题是其他代码可能会复制索引器,并且不清楚如果这样做的话语义应该是什么。如果代码显示“var userList = Foo.Users; Foo.RemoveSomeUsers(); someUser = userList[5];”那应该是 Foo 的元素 [5](在 RemoveSomeUsers 之前)还是之后?如果一个 userList[] 是索引属性,则不必直接公开。
  • 你喜欢分配瞬态吗?
【解决方案2】:

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

除了开玩笑,这可能是因为解决方法很简单,而且该功能永远不会减少时间与收益。不过,看到这似乎是一种变化,我不会感到惊讶。

您还忘了提到一个更简单的解决方法是制作一个常规方法:

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

【讨论】:

  • 非常正确。如果属性语法绝对重要,那么您可以使用 Ion 的解决方法(可能使用一些泛型以允许各种返回类型)。无论如何,我认为这说明在没有额外语言功能的情况下完成相同的工作相对容易。
【解决方案3】:

C# 索引器一个索引属性。它默认命名为Item(您可以从例如VB中引用它),如果需要,您可以使用IndexerNameAttribute更改它。

我不确定为什么,具体来说,它是这样设计的,但它似乎是一个故意的限制。然而,它与框架设计指南一致,它确实推荐了非索引属性为成员集合返回可索引对象的方法。 IE。 “可索引”是一种类型的特征;如果它可以以多种方式被索引,那么它真的应该分成几种类型。

【讨论】:

  • 谢谢!在实现具有默认索引器 (DISPID 0) 的 COM 互操作接口时,我反复与错误作斗争,该接口作为 this[int] 导入但其名称最初不是“项目”(有时是“项目”或“值”或类似名称)。这无论如何都会编译和运行,但会导致 FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes 警告、CLS 合规性问题(标识符仅在大小写不同)等,因为名称不太合适。 [IndexerName] 就是所需要的,但我从未设法找到它。
  • 谢谢!!! IndexerName 属性使我能够在不破坏 MSIL 签名的情况下完成将 VB 程序集转换为 C#。
【解决方案4】:

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

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

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

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

所以我们将一堆东西从“必须拥有”存储桶移到“很高兴拥有”存储桶。

索引属性永远不会在“必须拥有”列表的顶部接近。他们在“好主意”列表中的排名非常低,并且在“坏主意”列表中调情。

我们花在设计、实施、测试、记录或维护好的功能 X 上的每一分钟,都是我们不能花在很棒的功能 A、B、C、D、E、F 和 G 上的一分钟。我们必须毫不留情地确定优先级我们只做最好的功能。索引属性会很不错,但不错还远远不够好到无法实际实现。

【讨论】:

  • 我可以添加投票以将其列入不良名单吗?当您可以只公开实现索引器的嵌套类型时,我真的不明白当前的实现有多大限制。我想你会开始看到很多黑客尝试将一些东西硬塞到数据绑定和应该是方法的属性中。
  • 希望自动实现的 INotifyPropertyChanged 在列表中比索引属性高得多。 :)
  • @Eric,好吧,这就是我的怀疑......谢谢你的回答!我想我可以在没有索引属性的情况下生活,就像我多年来所做的那样;)
  • @Martin:我不是关于如何确定大型软件团队预算的专家。您的问题应该向 Soma、Jason Zander 或 Scott Wiltamuth 提出,我相信他们都偶尔会写博客。您与 Scala 的比较是苹果与橘子的比较; Scala 没有 C# 的大部分成本;仅举一个例子,它没有数百万具有极其重要的向后兼容性要求的用户。我可以举出更多可能导致 C# 和 Scala 之间的巨大成本差异的因素。
  • +1:通过阅读本文,人们可以学到很多关于管理软件项目的知识。而且只有几行。
【解决方案5】:

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

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

【讨论】:

  • 回答您的编辑:我认为这不会导致语言膨胀;类索引器 (this[]) 的语法已经存在,它们只需要允许标识符而不是 this。但我怀疑它是否会被包含在语言中,因为 Eric 在他的回答中解释了原因
【解决方案6】:

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

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

【讨论】:

  • 有趣的想法,即使答案有点晚了;)。你的观点非常好,+1。
  • 在 vb.net 中,一个类可以同时具有同名的索引和非索引属性 [e.g. Bar]。表达式Thing.Bar(5) 将使用Thing 上的索引属性Bar,而(Thing.Bar)(5) 将使用非索引属性Bar,然后使用结果对象的默认索引器。在我看来,允许Thing.Bar[5] 成为Thing 的属性,而不是Thing.Bar 的属性,这很好,因为除其他外,有可能在某个时刻,Thing.Bar[4] 的含义可能要清楚,但...的适当效果...
  • ...var temp=Thing.Bar; do_stuff_with_thing; var q=temp[4] 之类的内容可能不清楚。还要考虑Thing 可能将数据Bar 保存在一个字段中的概念,该字段可以是共享的不可变对象或非共享的可变对象;当支持字段不可变时尝试写入Bar 应该生成可变副本,但尝试读取它不应该。如果Bar 是一个命名的索引属性,则索引的getter 可以单独保留支持集合(无论是否可变),而setter 可以根据需要制作一个新的可变副本。
  • 属性的索引器不是枚举器——它是一个键
【解决方案7】:

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

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

如果索引属性在 C# 中可用,我可以实现以下代码(语法 is 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;
    }
}

【讨论】:

  • 我不太明白。您没有泄漏 Config 类实现的原因是因为您有一个 IConfig 接口。您 泄漏 ScriptsCollection 的原因是因为您没有创建接口……在这方面,“抽象泄漏”与每次看到的索引属性完全无关。不过,它确实需要更多样板文件——我不同意这个论点。
【解决方案8】:

有一个使用 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];

【讨论】:

  • 聪明,但最好缓存索引器而不是每次都创建它;)
【解决方案9】:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-02-05
    • 1970-01-01
    • 2011-10-13
    • 1970-01-01
    • 2016-12-01
    • 2011-02-04
    • 2017-10-19
    相关资源
    最近更新 更多