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ètre index ), 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 ?

79
demandé sur Thomas Levesque 2010-05-11 02:30:01

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é.

107
répondu Eric Lippert 2010-05-10 22:52:16

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.

19
répondu Pavel Minaev 2010-05-10 22:43:57

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.

14
répondu Ion Todirel 2010-05-10 22:38:24

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.

7
répondu Joshua A. Schaeffer 2012-08-24 18:01:13

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;
    }
}
5
répondu James Higgins 2014-04-05 18:45:05

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.

2
répondu cdiggins 2017-05-23 12:10:39

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) {...}
1
répondu Ron Warholic 2010-05-10 22:38:25

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];
1
répondu bradgonesurfing 2014-05-28 08:02:37

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)

0
répondu George Birbilis 2017-05-23 11:47:04