indexes - indexadores c#
¿Por qué C#no implementa propiedades indexadas? (9)
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 deindex
), 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 dinámico)
- 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 sintaxis 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 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 realmente devolvería un IIndexer<int, string>
, 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.
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) {...}
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.
Considero que la falta de propiedades indexadas es muy frustrante cuando intento 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 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
}
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 objetivo 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
/// <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;
}
}
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.
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<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 indizació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 fábrica
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 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)
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 sintaxis 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.
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.