代码之家  ›  专栏  ›  技术社区  ›  Thomas Levesque

为什么C不实现索引属性?

  •  79
  • Thomas Levesque  · 技术社区  · 14 年前

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

    不过,我还是希望有更好的解释…我在读书 this blog post about new C# 4 features 在有关COM互操作的部分中,以下部分引起了我的注意:

    顺便说一下,这段代码还使用了一个新特性:索引属性(仔细看看范围后面的方括号)。 但此功能仅适用于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中看到这个功能吗?如果不是,为什么?

    9 回复  |  直到 14 年前
        1
  •  108
  •   Eric Lippert    14 年前

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

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

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

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

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

    索引属性从未出现在任何位置 近的 “必须拥有”列表的顶部。他们在“好”名单上的排名很低,还和“坏主意”名单调情。

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

        2
  •  19
  •   Pavel Minaev    14 年前

    C语言索引器 索引属性。它被命名 Item 默认情况下(您可以从例如vb中引用它),并且可以使用 IndexerNameAttribute 如果你愿意的话。

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

        3
  •  14
  •   Ion Todirel    14 年前

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

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

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

        4
  •  7
  •   Joshua A. Schaeffer    12 年前

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

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

        5
  •  5
  •   James Higgins    10 年前

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

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

    如果索引属性在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;
        }
    }
    
        6
  •  2
  •   Community Keith    7 年前

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

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

        7
  •  1
  •   Ron Warholic    14 年前

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

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

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

    public void SetFoo(int index, Foo toSet) {...}
    public Foo GetFoo(int index) {...}
    
        8
  •  1
  •   bradgonesurfing    10 年前

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

    用于只读索引

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

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

    MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
    MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];
    
        9
  •  0
  •   Community Keith    7 年前

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

    推荐文章