关于语言特性:为什么C#不实现索引属性?

Why C# doesn't implement indexed properties?

我知道,我知道…埃里克·利珀特对这类问题的回答通常是"因为它不值得花费设计、实施、测试和记录的成本"。

不过,我还是希望有更好的解释…我在读这篇关于新的C 4特性的博客文章,在关于COM互操作的章节中,以下部分引起了我的注意:

By the way, this code uses one more new feature: indexed properties (take a closer look at those square brackets after Range.) But this feature is available only for COM interop; you cannot create your own indexed properties in C# 4.0.

好的,但是为什么?我已经知道并后悔不能在C中创建索引属性,但这句话让我重新思考了一下。我可以看到实现它的几个好理由:

  • clr支持它(例如,PropertyInfo.GetValue有一个index参数),所以很遗憾我们不能在c中利用它。#
  • 它支持COM互操作,如本文所示(使用动态调度)
  • 它是在vb.net中实现的
  • 已经有可能创建索引器,即对对象本身应用索引,因此将思想扩展到属性、保持相同的语法并用属性名称替换this,可能没有什么大不了的。

它将允许写这样的东西:

1
2
3
4
5
6
7
8
9
public class Foo
{
    private string[] _values = new string[3];
    public string Values[int index]
    {
        get { return _values[index]; }
        set { _values[index] = value; }
    }
}

目前我所知道的唯一解决方法是创建一个实现索引器的内部类(例如ValuesCollection),并更改Values属性,以便它返回该内部类的一个实例。

这很容易,但很烦人…所以也许编译器可以为我们做这件事!一个选项是生成实现索引器的内部类,并通过公共通用接口公开它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 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,而不是一个真正的索引属性…最好生成一个真正的clr索引属性。

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


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

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

然后,我们将这些特性分成"这是坏的,我们绝不能这样做"、"这是棒的,我们必须这样做"和"这是好的,但这次我们不要这样做"。

然后我们研究了我们必须设计、实现、测试、记录、发送和维护"必须拥有"特性的预算,发现我们超出了预算的100%。

所以我们把一堆东西从"必须拥有"桶移到了"美好拥有"桶。

索引属性从来没有接近"必须拥有"列表的顶部。他们在"好"名单上的排名很低,还和"坏主意"名单调情。

我们花在设计、实现、测试、记录或维护好的特性X上的每一分钟都是我们不能花在令人敬畏的特性A、B、C、D、E、F和G上的一分钟。我们必须无情地确定优先级,这样我们才能做到最好的特性。索引属性是不错的,但是nice还不足以真正实现。


索引器是索引属性。默认情况下,它名为Item(您可以从例如vb中这样引用它),如果需要,您可以使用indexernameattribute更改它。

我不知道为什么,具体来说,它是这样设计的,但它似乎是一个有意的限制。但是,它与框架设计准则一致,框架设计准则确实建议使用非索引属性为成员集合返回可索引对象的方法。也就是说,"可索引"是一种类型的特征;如果它以多种方式可索引,那么它真的应该分为几个类型。


因为您已经可以这样做了,而且它迫使您考虑OO方面的问题,添加索引属性只会给语言增加更多的噪声。还有另一种方法。

1
2
3
4
5
6
7
8
9
10
11
class Foo
{
    public Values Values { ... }
}

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

foo.Values[0]

我个人更希望看到的只是一种做事的方式,而不是10种方式。当然,这是一个主观的观点。


我以前喜欢索引属性的概念,但后来意识到它会增加可怕的模糊性,实际上会抑制功能。索引属性意味着您没有子集合实例。这是好是坏。实现起来比较容易,并且不需要返回到封闭的所有者类的引用。但这也意味着您不能将该子集合传递给任何对象;您可能需要每次枚举一次。你也不能在上面做前臂。最糟糕的是,从索引属性看不出它是索引属性还是集合属性。

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


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

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

如果索引属性在C中可用,我可以实现以下代码(语法是将此[key]更改为propertyname[key])。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
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
}

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

我本可以将其实现为:

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

这可能是我应该有的,但这是一个很有用的例子,说明为什么使用索引类来替换这个缺失的特性通常不是一个合理的替代品。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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;
    }
}

另一个解决方法是在C中轻松创建支持索引的属性,这需要较少的工作。

编辑:我还应该补充一点,作为对原始问题的回应,我认为如果我们能够在库支持下完成所需的语法,那么我认为需要有一个非常强大的案例来将它直接添加到语言中,以最小化语言膨胀。


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

用于只读索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
        }
    }
}

用于可变索引

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
        }
    }
}

还有一个工厂

1
2
3
4
5
6
7
8
9
10
11
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);
    }
}

在我自己的代码中,我使用它就像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
        }
    }

}

再举一个莫尼厄夫兰克特轮廓的例子

1
2
MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];


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

撇开开开开玩笑不说,这可能是因为解决方法很简单,而且该功能不会缩短时间和效益。不过,我不会惊讶地看到这似乎是一个改变。

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

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


我也发现,您可以使用显式实现的接口来实现这一点,如下所示:C中的命名索引属性(见回复中的第二种方式)