84

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

但是,我仍然想要一个更好的解释......我正在阅读这篇关于 C# 4 新特性的博客文章,在关于 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;
        }
    }
}

但当然,在这种情况下,该属性实际上会返回 a IIndexer<int, string>,并且不会真正成为索引属性……最好生成一个真正的 CLR 索引属性。

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

4

9 回答 9

125

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

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

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

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

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

索引属性从未接近“必须拥有”列表的顶部。他们在“好”名单上的排名很低,并且在“坏主意”名单上调情。

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

于 2010-05-10T22:52:16.637 回答
23

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

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

于 2010-05-10T22:43:57.450 回答
16

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

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

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

foo.Values[0]

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

于 2010-05-10T22:38:24.367 回答
9

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

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

于 2012-08-24T18:01:13.757 回答
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;
    }
}
于 2014-04-05T18:45:05.180 回答
2

另一个解决方法列在Easy creation of properties that support indexing in C#中,需要较少的工作。

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

于 2010-07-27T14:45:14.863 回答
1

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

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

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

public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}
于 2010-05-10T22:38:25.047 回答
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];
于 2014-05-28T07:51:29.343 回答
0

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

于 2016-05-30T03:13:27.550 回答