Pourquoi C# n'implémente pas les propriétés indexées?
je sais, je sais... La réponse d'Eric Lippert à ce genre de question est habituellement quelque chose comme parce que cela ne valait pas le coût de concevoir, mettre en œuvre, tester et documenter ".
mais j'aimerais quand même une meilleure explication... Je lisais ce billet de blog sur les nouvelles fonctionnalités C# 4 , et dans la section sur COM Interop, la partie suivante a attiré mon attention:
soit dit en passant, ce code utilise une nouvelle caractéristique: les propriétés indexées (regardez de plus près ces crochets après la gamme.) mais cette fonctionnalité n'est disponible que pour COM interop; vous ne pouvez pas créer vos propres propriétés indexées dans C# 4.0 .
OK, mais pourquoi ? J'ai déjà su et regretté qu'il n'était pas possible de créer des propriétés indexées dans C#, mais cette phrase m'a fait réfléchir à nouveau à ce sujet. Je peux voir plusieurs bonnes raisons de sa mise en œuvre:
- le CLR le supporte (par exemple,
PropertyInfo.GetValue
a un paramètreindex
), il est donc dommage que nous ne puissions pas en profiter dans C# - il est supporté pour COM interop, comme indiqué dans l'article (utilisation de Dynamic dispatch)
- il est mis en œuvre en VB.NET
- il est déjà possible de créer des indexeurs, c'est à dire d'appliquer un indice de l'objet lui-même, de sorte qu'il serait ce n'est probablement pas grand chose d'étendre l'idée aux propriétés, en gardant la même syntaxe et en remplaçant simplement
this
par un nom de propriété
Il permettrait d'écrire ce genre de choses :
public class Foo
{
private string[] _values = new string[3];
public string Values[int index]
{
get { return _values[index]; }
set { _values[index] = value; }
}
}
actuellement, la seule solution que je connaisse est de créer une classe interne ( ValuesCollection
par exemple) qui implémente un indexeur, et de changer la propriété Values
pour qu'elle renvoie une instance de cette classe interne.
C'est très facile à faire, mais ennuyeux... Alors peut-être que le compilateur pourrait le faire pour nous ! Une option serait de générer une classe interne qui implémente l'indexeur, et de l'exposer à travers une interface publique Générique:
// 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;
}
}
}
mais bien sûr, dans ce cas , le bien en fait retourner un IIndexer<int, string>
, et ne serait pas vraiment une propriété indexée... Il serait préférable de générer une propriété réelle indexée CLR.
Qu'en pensez-vous ? Voulez-vous voir cette fonctionnalité dans C# ? Si non, pourquoi ?
9 réponses
Voici comment nous avons conçu C# 4.
tout d'abord, nous avons fait une liste de toutes les caractéristiques possibles que nous pourrions ajouter à la langue.
puis nous avons mis les caractéristiques dans "c'est mauvais, nous ne devons jamais le faire", "c'est génial, nous devons le faire", et "c'est bon mais ne le faisons pas cette fois".
ensuite, nous avons examiné combien de budget nous avions pour concevoir, mettre en œuvre, tester, documenter, expédier et maintenir les caractéristiques "gotta have" et j'ai découvert que nous dépassions le budget à 100%.
alors on a déplacé un tas de trucs du seau "gotta have" au seau "nice to have".
les propriétés indexées n'ont jamais été nulle part près de le haut de la liste" gotta have". Ils sont très bas sur la liste des "Gentils" et flirtent avec la liste des "mauvaises idées".
chaque minute que nous passons à concevoir, mettre en œuvre, tester, documenter ou maintenir la fonctionnalité nice X est minute nous ne pouvons pas passer sur les traits impressionnants A, B, C, D, E, F et G. Nous devons impitoyablement prioriser de sorte que nous ne faisons que les traits les meilleurs possibles. Les propriétés indexées seraient bien, mais nice n'est même pas assez bien pour être implémenté.
C# indexeur est une propriété indexée. Il est nommé Item
par défaut (et vous pouvez vous y référer en tant que tel à partir de par exemple VB), et vous pouvez le changer avec IndexerNameAttribute si vous voulez.
Je ne sais pas pourquoi, spécifiquement, il a été conçu de cette façon, mais il semble que ce soit une limitation intentionnelle. Il est toutefois conforme aux lignes directrices sur la conception du cadre, qui recommandent l'approche d'un propriété retournant un objet indexable pour les collections des membres. C'est-à-dire que "être indexable" est un trait d'un type; s'il est indexable de plus d'une façon, alors il devrait vraiment être divisé en plusieurs types.
parce que vous pouvez déjà le faire en quelque sorte, et il est forcé de penser dans les aspects OO, l'ajout de propriétés indexées serait tout simplement ajouter plus de bruit à la langue. Et juste une autre façon de faire autre chose.
class Foo
{
public Values Values { ... }
}
class Values
{
public string this[int index] { ... }
}
foo.Values[0]
personnellement, je préfère voir une seule manière de faire quelque chose, plutôt que 10 façons. Mais bien sûr c'est un avis subjectif.
j'avais l'habitude de favoriser l'idée de propriétés indexées, mais j'ai alors réalisé qu'il ajouterait une ambiguïté horrible et en fait dissuasive fonctionnalité. Les propriétés indexées signifieraient que vous n'avez pas d'instance de collecte d'enfants. C'est à la fois bon et mauvais. Il est moins difficile à mettre en œuvre et vous n'avez pas besoin d'une référence retour à la classe de propriétaire enclosing. Mais cela signifie aussi que vous ne pouvez pas passer cette collection d'enfants à quoi que ce soit; vous auriez probablement à énumérer chaque fois. Ni pouvez-vous faire un foreach sur elle. Pire que tout, vous ne pouvez pas dire en regardant une propriété indexée si c'est cela ou une propriété de collection.
l'idée est rationnelle mais elle ne mène qu'à l'inflexibilité et à la maladresse abrupte.
je trouve le manque de propriétés indexées très frustrant en essayant d'écrire du code propre et concis. Une propriété indexée a une connotation très différente de fournir une référence de classe qui est indexée ou de fournir des méthodes individuelles. Je trouve un peu troublant que fournir l'accès à un objet interne qui implémente une propriété indexée soit même considéré comme acceptable car cela brise souvent l'un des composants clés de l'orientation de l'objet: l'encapsulation.
Je rencontre ce problème assez souvent, mais je viens de le rencontrer à nouveau aujourd'hui donc je vais fournir un exemple de code du monde réel. L'interface et la classe étant écrite stocke la configuration de l'application qui est une collection d'informations vaguement liées. J'avais besoin d'ajouter des fragments de script nommés et l'utilisation de l'indexeur de classe sans nom aurait impliqué un contexte très erroné puisque les fragments de script ne sont qu'une partie de la configuration.
si les propriétés indexées étaient disponibles en C # je pourrais ont implémenté le code ci-dessous (la syntaxe est cette[clé] changée en PropertyName[clé]).
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
}
malheureusement les propriétés indexées ne sont pas implémentées donc j'ai implémenté une classe pour les stocker et fourni l'accès à cela. Il s'agit d'une implémentation indésirable car le but de la classe de configuration dans ce modèle de domaine est d'encapsuler tous les détails. Les Clients de cette classe accéderont à des fragments de script spécifiques par leur nom et n'auront aucune raison de compter ou énumérer sur eux.
je pourrais avoir mis en place ce que:
public string ScriptGet(string name)
public void ScriptSet(string name, string value)
que j'aurais probablement dû avoir, mais c'est une illustration utile de la raison pour laquelle l'utilisation de classes indexées en remplacement de cette caractéristique manquante n'est souvent pas un substitut raisonnable.
pour implémenter une capacité similaire en tant que propriété indexée, j'ai dû écrire le code ci-dessous qui est beaucoup plus long, plus complexe et donc plus difficile à lire., comprendre et maintenir.
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;
}
}
une autre solution est listée à création facile de propriétés qui supportent l'indexation dans C# , qui nécessite moins de travail.
EDIT : je devrais également ajouter qu'en réponse à la question originale, que mon si nous pouvons accomplir la syntaxe désirée, avec le soutien de bibliothèque, alors je pense qu'il doit y avoir un cas très fort pour l'ajouter directement à la langue, afin de minimiser le bloat de langue.
Eh bien, je dirais qu'ils ne l'ont pas ajouté parce que cela ne valait pas le coût de la conception, de la mise en œuvre, de la mise à l'essai et de la documentation.
blague à part, c'est probablement parce que les solutions de rechange sont simples et que la fonction ne fait jamais le temps par rapport à la réduction des avantages. Je ne serais pas surpris de voir cela apparaître comme un changement.
vous avez également oublié de mentionner qu'une solution plus facile est de faire une méthode régulière:
public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}
il existe une solution générale simple utilisant lambdas pour remplacer la fonctionnalité d'indexation
pour l'indexation en lecture seule
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);
}
}
}
pour indexation 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);
}
}
}
et une usine
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);
}
}
dans mon propre code je l'utilise comme
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;
}
}
}
et avec une instance de MoineauFlankContours je peux faire
MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];
vient de découvrir moi aussi que vous pouvez utiliser des interfaces explicitement implémentées pour y parvenir, comme indiqué ici: nommé propriété indexée dans C#? (voir la deuxième réponse)