¿Por qué C # no implementa propiedades indexadas?

83

Lo sé, lo sé ... La respuesta de Eric Lippert a este tipo de preguntas suele ser algo así como " porque no valió 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 publicación de blog sobre las nuevas características de C # 4 , y en la sección sobre COM Interop, la siguiente parte me llamó la atención:

Por cierto, este código usa una característica nueva más: propiedades indexadas (observe más de cerca los corchetes después de Rango). Pero esta característica 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 fuera posible crear propiedades indexadas en C #, pero esta oración me hizo pensarlo nuevamente. Puedo ver varias buenas razones para implementarlo:

  • el CLR lo admite (por ejemplo, PropertyInfo.GetValuetiene un indexparámetro), por lo que es una pena que no podamos aprovecharlo en C #
  • es compatible con la interoperabilidad COM, como se muestra en el artículo (usando envío dinámico)
  • está implementado en VB.NET
  • ya es posible crear indexadores, es decir, aplicar un índice al objeto en sí, por lo que probablemente no sería gran cosa extender la idea a las propiedades, manteniendo la misma sintaxis y simplemente reemplazando thiscon 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 ( ValuesCollectionpor ejemplo) que implemente un indexador y cambiar la Valuespropiedad para que devuelva una instancia de esa clase interna.

Esto es muy fácil de hacer, pero molesto ... ¡Así que quizás el compilador 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<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;
        }
    }
}

Pero, por supuesto, en ese caso, la propiedad en realidad devolvería a IIndexer<int, string>, y no sería realmente una propiedad indexada ... Sería mejor generar una propiedad indexada CLR real.

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

Thomas Levesque
fuente
1
Tengo la sensación de que este es otro de esos problemas de "recibimos solicitudes para X pero no más que para Y" .
ChaosPandion
1
@ChaosPandion, sí, probablemente tengas razón ... Pero esta característica probablemente sería bastante fácil de implementar, y aunque ciertamente no es "imprescindible", definitivamente entra en la categoría "bueno tener"
Thomas Levesque
4
Los indexadores ya son un poco molestos desde el punto de vista de CLR. Agregan un nuevo caso de límite al código que quiere trabajar con propiedades, ya que ahora cualquier propiedad podría tener parámetros de indexación. Creo que la implementación de C # tiene sentido, ya que el concepto que típicamente representa un indexador no es una propiedad de un objeto, sino su 'contenido'. Si proporciona propiedades de indexación arbitrarias, está implicando que la clase puede tener diferentes grupos de contenido, lo que naturalmente conduce a encapsular el subcontenido complejo como una nueva clase. Mi pregunta es: ¿por qué CLR proporciona propiedades indexadas?
Dan Bryant
1
@tk_ gracias por tu comentario constructivo. ¿Está publicando comentarios similares a todas las publicaciones sobre idiomas que no son Free Pascal? Bueno, espero que te haga sentir bien contigo mismo ...
Thomas Levesque
3
Esta es una de las pocas situaciones en las que C ++ / CLI y VB.net son mejores que C #. He implementado muchas propiedades indexadas en mi código C ++ / CLI y ahora, al convertirlo a C #, tengo que encontrar soluciones para todas ellas. :-( CHUPA !!! // Su Permitiría a escribir ese tipo de cosas es lo que he hecho durante años.
Tobias Knauss

Respuestas:

122

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 agrupamos 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, analizamos cuánto presupuesto teníamos para diseñar, implementar, probar, documentar, enviar y mantener las funciones "imprescindibles" y descubrimos que estábamos 100% por encima del presupuesto.

Así que movimos un montón de cosas del segmento "tengo que tener" al segmento "bueno tener".

Las propiedades indexadas nunca estuvieron cerca de la parte superior de la lista "imprescindible". Están muy abajo en la lista de "buenos" y coquetean con la lista de "malas ideas".

Cada minuto que dedicamos a diseñar, implementar, probar, documentar o mantener una buena característica X es un minuto que no podemos dedicar a las increíbles características A, B, C, D, E, F y G. Tenemos que priorizar sin piedad para que solo hacer las mejores funciones posibles. Las propiedades indexadas serían buenas, pero las bondades no son ni siquiera lo suficientemente buenas como para ser implementadas.

Eric Lippert
fuente
20
¿Puedo agregar un voto para ponerlo en la lista incorrecta? Realmente no veo cómo la implementación actual es una gran limitación cuando se puede exponer un tipo anidado que implementa un indexador. Me imagino que empezarías a ver mucha piratería para intentar meter con calzador algo en la vinculación de datos y propiedades que deberían ser métodos.
Josh
11
Y, con suerte, INotifyPropertyChanged auto-implementado es mucho más alto en la lista que las propiedades indexadas. :)
Josh
3
@Eric, está bien, eso es lo que sospechaba ... ¡gracias por responder de todos modos! Supongo que puedo vivir sin propiedades indexadas, como lo he hecho durante años;)
Thomas Levesque
5
@Martin: No soy un experto en cómo se determinan los presupuestos de los grandes equipos de software. Su pregunta debe dirigirse a Soma, Jason Zander o Scott Wiltamuth, creo que todos escriben blogs ocasionalmente. Su comparación con Scala es una comparación de manzanas a naranjas; Scala no tiene la mayoría de los costos que tiene C #; solo por nombrar uno, no tiene millones de usuarios con requisitos de compatibilidad con versiones anteriores extremadamente importantes. Podría nombrarle muchos más factores que podrían causar enormes diferencias de costos entre C # y Scala.
Eric Lippert
12
+1: La gente podría aprender mucho sobre la gestión de proyectos de software leyendo esto. Y solo tiene unas pocas líneas.
Brian MacKay
22

El indexador AC # es una propiedad indexada. Tiene un nombre Itempredeterminado (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 Directrices de diseño del marco, que recomiendan el enfoque de una propiedad no indexada que devuelve un objeto indexable para las colecciones de miembros. Es decir, "ser indexable" es un rasgo de un tipo; si es indexable en más de una forma, entonces debería dividirse en varios tipos.

Pavel Minaev
fuente
1
¡Gracias! Repetidamente he luchado con errores al implementar interfaces de interoperabilidad COM que tienen un indxer predeterminado (DISPID 0) que se importa como este [int] pero cuyo nombre no era originalmente "Elemento" (a veces es "elemento" o "valor", o similar ). Esto se compila y se ejecuta de todos modos, pero conduce a advertencias de FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes, problemas de cumplimiento de CLS (identificadores que difieren solo en el caso), etc., ya que el nombre no encaja del todo. [IndexerName] es todo lo que se necesitaba, pero nunca logré encontrarlo.
puetzk
¡¡¡Gracias!!! El atributo IndexerName me permitió terminar de convertir un ensamblado de VB a C # sin romper las firmas de MSIL.
Jon Tirjan
15

Debido a que ya puede hacerlo, y se ve obligado a pensar en aspectos de OO, agregar propiedades indexadas solo 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 de hacer algo, en lugar de 10 formas. Pero, por supuesto, esta es una opinión subjetiva.


fuente
2
+1, esta es una forma mucho mejor de implementarlo que engullir el lenguaje con construcciones VB5.
Josh
1
+1 porque así es como lo haría yo. Y si fueras bueno, probablemente podrías hacer esto genérico.
Tony
10
Un problema con ese enfoque es que es posible que otro código haga una copia del indexador, y no está claro cuál debería ser la semántica si lo hace. Si el código dice "var userList = Foo.Users; Foo.RemoveSomeUsers (); someUser = userList [5];" ¿Debería ser eso lo que solía ser el elemento [5] de Foo (antes de RemoveSomeUsers), o después? Si una userList [] fuera una propiedad indexada, no tendría que exponerse directamente.
supercat
¿Le gusta asignar transitorios?
Joshua
9

Solía ​​favorecer la idea de propiedades indexadas, pero luego me di cuenta de que agregaría una ambigüedad horrible y en realidad desincentivaría la funcionalidad. Las propiedades indexadas significarían que no tiene una instancia de colección secundaria. Eso es bueno y malo. Es menos complicado de implementar y no necesita una referencia a la clase propietaria adjunta. Pero también significa que no puede pasar esa colección secundaria a nada; probablemente tendrías que enumerar cada vez. Tampoco puedes hacer un foreach sobre él. Lo peor de todo es que, al mirar una propiedad indexada, no se puede saber si es esa o una propiedad de colección.

La idea es racional, pero simplemente conduce a la inflexibilidad y la torpeza abrupta.

Joshua A. Schaeffer
fuente
Pensamientos interesantes, aunque la respuesta sea un poco tardía;). Estás haciendo muy buenos puntos, +1.
Thomas Levesque
En vb.net, una clase puede tener una propiedad indexada y no indexada del mismo nombre [por ejemplo Bar]. La expresión Thing.Bar(5)usaría la propiedad indexada Baren Thing, mientras (Thing.Bar)(5)que usaría la propiedad no indexada Bary luego usaría el indexador predeterminado del objeto resultante. En mi opinión, permitir Thing.Bar[5]que sea una propiedad de Thing, más que una propiedad de Thing.Bar, es bueno ya que, entre otras cosas, es posible que en algún momento, el significado de Thing.Bar[4]pueda ser claro, pero el efecto propio de ...
supercat
... algo como var temp=Thing.Bar; do_stuff_with_thing; var q=temp[4]puede no estar claro. Considere también la noción que Thingpodría contener los datos Baren un campo que podría ser un objeto inmutable compartido o un objeto mutable no compartido; un intento de escribir Barcuando el campo de respaldo es inmutable debería hacer una copia mutable, pero un intento de leer desde él no debería. Si Bares una propiedad indexada con nombre, el captador indexado podría dejar la colección de respaldo sola (ya sea mutable o no) mientras que el configurador podría hacer una nueva copia mutable si fuera necesario.
supercat
un indexador de una propiedad no es un enumerador, es una clave
George Birbilis
6

Encuentro la falta de propiedades indexadas muy frustrante cuando trato de escribir código limpio y conciso. Una propiedad indexada tiene una connotación muy diferente a proporcionar una referencia de clase que está indexada o proporcionar métodos individuales. Me parece un poco perturbador que proporcionar acceso a un objeto interno que implementa una propiedad indexada se considere aceptable, ya que a menudo se rompe uno de los componentes clave de la orientación a objetos: la encapsulación.

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

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

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
}

Desafortunadamente, las propiedades indexadas no están implementadas, así que implementé una clase para almacenarlas y proporcioné acceso a eso. Esta es una implementación indeseable porque el propósito de la clase de configuración en este modelo de dominio es encapsular todos los detalles. Los clientes de esta clase accederán a fragmentos de script específicos por su nombre y no tendrán ninguna razón para contarlos o enumerarlos.

Podría haber implementado esto como:

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

Lo que probablemente debería haberlo hecho, 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 largo, 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

    /// <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;
    }
}
James Higgins
fuente
2

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

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

cdiggins
fuente
2
En respuesta a su edición: no creo que cause hinchazón en el lenguaje; la sintaxis ya está disponible para los indexadores de clases ( this[]), solo necesitan permitir un identificador en lugar de this. Pero dudo que alguna vez se incluya en el idioma, por las razones explicadas por Eric en su respuesta
Thomas Levesque
1

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

Dejando de lado las bromas, probablemente se deba a que las soluciones son simples y la función nunca reduce el tiempo frente a los beneficios. Sin embargo, no me sorprendería ver que esto aparezca como un cambio en el futuro.

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

public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}
Ron Warholic
fuente
Muy cierto. Si la sintaxis de la propiedad es absolutamente vital, entonces puede usar la solución alternativa de Ion (quizás con algunos genéricos para permitir una variedad de tipos de devolución). Independientemente, creo que esto habla de la relativa facilidad de lograr el mismo trabajo sin funciones de idioma adicionales.
Ron Warholic
1

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

Para indexación de solo lectura

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

Para indexación mutable

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

y una fabrica

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

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<MoineauPartEnum, MoineauFlankContour> _RoIndexer;

    public RoIndexer<MoineauPartEnum, MoineauFlankContour> FlankFor
    {
        get
        {
            return _RoIndexer;
        }
    }

}

y con una instancia de MoineauFlankContours puedo hacer

MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];
bradgonesurf
fuente
Inteligente, pero probablemente sería mejor almacenar en caché el indexador en lugar de crearlo cada vez;)
Thomas Levesque
0

También descubrí que puede usar interfaces implementadas explícitamente para lograr esto, como se muestra aquí: ¿ Propiedad indexada nombrada en C #? (vea la segunda forma mostrada en esa respuesta)

George Birbilis
fuente