¿Por qué C # no implementa propiedades indexadas?

Lo sé, lo sé … La respuesta de Eric Lippert a este tipo de preguntas suele ser algo así como ” porque no valía la pena el costo de diseñarlo, implementarlo, probarlo y documentarlo “.

Pero aún así, me gustaría una mejor explicación … Estaba leyendo esta entrada de blog sobre las nuevas características de C # 4 , y en la sección sobre COM Interop, la siguiente parte llamó mi atención:

Por cierto, este código usa una característica más: propiedades indexadas (mire más de cerca esos corchetes después de Rango). Pero esta función solo está disponible para interoperabilidad COM; no puede crear sus propias propiedades indexadas en C # 4.0 .

Está bien, pero por qué ? Ya sabía y lamenté que no era posible crear propiedades indexadas en C #, pero esta frase me hizo pensar nuevamente sobre eso. Puedo ver varias buenas razones para implementarlo:

  • CLR lo admite (por ejemplo, PropertyInfo.GetValue tiene un parámetro de index ), por lo que es una lástima que no podamos aprovecharlo en C #
  • es compatible con la interoperabilidad COM, como se muestra en el artículo (mediante el envío dynamic)
  • está implementado en VB.NET
  • ya es posible crear indizadores, es decir, aplicar un índice al objeto en sí mismo, por lo que probablemente no sea gran cosa extender la idea a las propiedades, manteniendo la misma syntax y simplemente reemplazando this con un nombre de propiedad

Permitiría escribir ese tipo de cosas:

 public class Foo { private string[] _values = new string[3]; public string Values[int index] { get { return _values[index]; } set { _values[index] = value; } } } 

Actualmente, la única solución que conozco es crear una clase interna ( ValuesCollection por ejemplo) que implemente un indexador y cambie la propiedad Values para que devuelva una instancia de esa clase interna.

Esto es muy fácil de hacer, pero molesto … ¡Entonces quizás el comstackdor podría hacerlo por nosotros! Una opción sería generar una clase interna que implemente el indexador y exponerlo a través de una interfaz genérica pública:

 // interface defined in the namespace System public interface IIndexer { TValue this[TIndex index] { get; set; } } public class Foo { private string[] _values = new string[3]; private class c__DisplayClass1 : IIndexer { 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 f__valuesIndexer; public IIndexer Values { get { if (f__valuesIndexer == null) f__valuesIndexer = new c__DisplayClass1(this); return f__valuesIndexer; } } } 

Pero, por supuesto, en ese caso la propiedad realmente devolvería un IIndexer , y realmente no sería una propiedad indexada … Sería mejor generar una propiedad real CLR indexada.

Qué piensas ? ¿Le gustaría ver esta característica en C #? Si no, ¿por qué?

Así es como diseñamos C # 4.

Primero hicimos una lista de todas las características posibles que podríamos pensar en agregar al lenguaje.

Luego incorporamos las características en “esto es malo, nunca debemos hacerlo”, “esto es increíble, tenemos que hacerlo” y “esto es bueno, pero no lo hagamos esta vez”.

Luego, observamos cuánto presupuesto teníamos que diseñar, implementar, probar, documentar, enviar y mantener las funciones “tengo que tener” y descubrí que estábamos al 100% por encima del presupuesto.

Así que movimos un montón de cosas del cubo “tengo que tener” al cubo “bueno tener”.

Las propiedades indexadas nunca estuvieron en la parte superior de la lista “tengo que tener”. Están muy abajo en la lista “buena” y coquetean con la lista de “mala idea”.

Cada minuto que pasamos diseñando, implementando, probando, documentando o manteniendo una función agradable X es un minuto que no podemos gastar en las increíbles funciones A, B, C, D, E, F y G. Tenemos que priorizar despiadadamente para que solo podamos hacer las mejores funciones posibles. Las propiedades indizadas serían agradables, pero bueno no está ni siquiera cerca de lo suficientemente bueno como para realmente ser implementado.

El indexador de AC # es una propiedad indexada. Se denomina Item por defecto (y puede referirse a él como tal desde, por ejemplo, VB), y puede cambiarlo con IndexerNameAttribute si lo desea.

No estoy seguro de por qué, específicamente, fue diseñado de esa manera, pero parece ser una limitación intencional. Sin embargo, es coherente con las Pautas de diseño del marco, que recomiendan el enfoque de una propiedad no indexada que devuelve un objeto indexable para colecciones de miembros. Es decir, “ser indexable” es un rasgo de un tipo; si es indexable en más de una forma, entonces realmente debería dividirse en varios tipos.

Como ya puede hacerlo, y le obliga a pensar en aspectos OO, agregar propiedades indexadas simplemente agregaría más ruido al lenguaje. Y solo otra forma de hacer otra cosa.

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

Personalmente, preferiría ver solo una forma única de hacer algo, en lugar de 10 formas. Pero, por supuesto, esta es una opinión subjetiva.

Solía ​​favorecer la idea de las propiedades indexadas, pero luego me di cuenta de que agregaría una ambigüedad horrible y desincentivaría la funcionalidad. Las propiedades indexadas significan que no tienes una instancia de colección hijo. Eso es bueno y malo. Es menos problemático implementarlo y no necesita una referencia de regreso a la clase propietaria adjunta. Pero también significa que no puede pasar esa colección de niños a nada; es probable que tengas que enumerar todas las veces. Tampoco puedes hacer un foreach en eso. Lo peor de todo es que no se puede decir al mirar una propiedad indexada si se trata de eso o de una propiedad de colección.

La idea es racional, pero solo lleva a la inflexibilidad y la incomodidad abrupta.

Considero que la falta de propiedades indexadas es muy frustrante cuando bash escribir un código limpio y conciso. Una propiedad indexada tiene una connotación muy diferente que proporcionar una referencia de clase indexada o proporcionar métodos individuales. Me resulta un poco molesto que incluso el acceso a un objeto interno que implementa una propiedad indexada sea considerado aceptable, ya que a menudo rompe uno de los componentes clave de la orientación del objeto: encapsulación.

Me encuentro con este problema con frecuencia, pero lo encontré de nuevo hoy, así que proporcionaré un ejemplo de código de mundo real. La interfaz y la clase que se escribe almacena la configuración de la aplicación, que es una colección de información poco relacionada. Necesitaba agregar fragmentos de script con nombre y el uso del indexador de clase sin nombre habría implicado un contexto muy incorrecto ya que los fragmentos de script son solo parte de la configuración.

Si las propiedades indexadas estuvieran disponibles en C #, podría haber implementado el siguiente código (la syntax es esta [clave] cambiada a PropertyName [clave]).

 public interface IConfig { // Other configuration properties removed for examp[le ///  /// Script fragments ///  string Scripts[string name] { get; set; } } ///  /// Class to handle loading and saving the application's configuration. ///  internal class Config : IConfig, IXmlConfig { #region Application Configuraiton Settings // Other configuration properties removed for examp[le ///  /// Script fragments ///  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 _scripts = new Dictionary(); #endregion ///  /// Clears configuration settings, but does not clear internal configuration meta-data. ///  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 } 

Lamentablemente, las propiedades indexadas no se implementan, así que implementé una clase para almacenarlas y proporcioné acceso a eso. Esta es una implementación no deseable porque el objective de la clase de configuración en este modelo de dominio es encapsular todos los detalles. Los clientes de esta clase tendrán acceso a fragmentos de scripts específicos por nombre y no tendrán motivos para contarlos o enumerarlos.

Pude haber implementado esto como:

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

Lo cual probablemente debería tener, pero esta es una ilustración útil de por qué el uso de clases indexadas como reemplazo de esta característica faltante a menudo no es un sustituto razonable.

Para implementar una capacidad similar a una propiedad indexada, tuve que escribir el siguiente código, que notará que es considerablemente más extenso, más complejo y, por lo tanto, más difícil de leer, comprender y mantener.

 public interface IConfig { // Other configuration properties removed for examp[le ///  /// Script fragments ///  ScriptsCollection Scripts { get; } } ///  /// Class to handle loading and saving the application's configuration. ///  internal class Config : IConfig, IXmlConfig { public Config() { _scripts = new ScriptsCollection(); _scripts.ScriptChanged += ScriptChanged; } #region Application Configuraiton Settings // Other configuration properties removed for examp[le ///  /// Script fragments ///  public ScriptsCollection Scripts { get { return _scripts; } } private readonly ScriptsCollection _scripts; private void ScriptChanged(object sender, ScriptChangedEventArgs e) { OnAppConfigChanged(); } #endregion ///  /// Clears configuration settings, but does not clear internal configuration meta-data. ///  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> { private readonly Dictionary Scripts = new Dictionary(); 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 ScriptChanged; protected void OnScriptChanged(string name) { if (ScriptChanged != null) { var script = this[name]; ScriptChanged.Invoke(this, new ScriptChangedEventArgs(name, script)); } } #region IEnumerable public IEnumerator> 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; } } 

Otra solución se enumera en Creación fácil de propiedades que admiten la indexación en C # , que requiere menos trabajo.

EDITAR : También debo agregar que en respuesta a la pregunta original, que si podemos lograr la syntax deseada, con el soporte de la biblioteca, entonces creo que debe haber un caso muy sólido para agregarlo directamente al lenguaje, para poder minimizar la hinchazón del lenguaje.

Bueno, yo diría que no lo han agregado porque no valía la pena el costo de diseñarlo, implementarlo, probarlo y documentarlo.

Bromas aparte, es probablemente porque las soluciones son simples y la función nunca hace el corte de tiempo versus beneficio. Sin embargo, no me sorprendería ver que esto parece ser un cambio en la línea.

También olvidó mencionar que una solución más fácil es simplemente hacer un método regular:

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

Hay una solución general simple que usa lambdas para proxy la funcionalidad de indexación

Para indexación de solo lectura

 public class RoIndexer { private readonly Func _Fn; public RoIndexer(Func fn) { _Fn = fn; } public TValue this[TIndex i] { get { return _Fn(i); } } } 

Para indización mutable

 public class RwIndexer { private readonly Func _Getter; private readonly Action _Setter; public RwIndexer(Func getter, Action setter) { _Getter = getter; _Setter = setter; } public TValue this[TIndex i] { get { return _Getter(i); } set { _Setter(i, value); } } } 

y una fábrica

 public static class Indexer { public static RwIndexer Create(Func getter, Action setter) { return new RwIndexer(getter, setter); } public static RoIndexer Create(Func getter) { return new RoIndexer(getter); } } 

en mi propio código lo uso como

 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 _RoIndexer; public RoIndexer FlankFor { get { return _RoIndexer; } } } 

y con una instancia de MoineauFlankContours que puedo hacer

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

Me acabo de enterar también que puede usar interfaces implementadas explícitamente para lograr esto, como se muestra aquí: Propiedad indexada con nombre en C #? (ver la segunda forma que se muestra en esa respuesta)