web-dev-qa-db-fra.com

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 généralement quelque chose comme " parce que cela ne valait pas le coût de la conception, de l'implémentation, du test et de la documentation".

Mais encore, je voudrais 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:

À propos, ce code utilise une nouvelle fonctionnalité: les propriétés indexées (regardez de plus près ces crochets après Range.) 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 .

D'accord mais pourquoi ? Je savais et regrettais déjà qu'il n'était pas possible de créer des propriétés indexées en C #, mais cette phrase m'a fait réfléchir à nouveau. Je peux voir plusieurs bonnes raisons de le mettre en œuvre:

  • le CLR le prend en charge (par exemple, PropertyInfo.GetValue a un paramètre index), donc c'est dommage qu'on ne puisse pas en profiter en C #
  • il est pris en charge pour l'interopérabilité COM, comme indiqué dans l'article (à l'aide de la répartition dynamique)
  • il est implémenté dans VB.NET
  • il est déjà possible de créer des indexeurs, c'est-à-dire d'appliquer un index à l'objet lui-même, il ne serait donc probablement pas très important d'étendre l'idée aux propriétés, en conservant la même syntaxe et en remplaçant simplement this par un nom de propriété

Cela 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 de contournement que je connaisse est de créer une classe interne (ValuesCollection par exemple) qui implémente un indexeur et de modifier la propriété Values afin qu'elle renvoie une instance de cette classe interne.

C'est très facile à faire, mais ennuyant ... 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 via une interface générique publique:

// 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, la propriété en fait renverra un IIndexer<int, string>, et ne serait pas vraiment une propriété indexée ... Il serait préférable de générer une vraie propriété indexée CLR.

Qu'est-ce que tu penses ? Souhaitez-vous voir cette fonctionnalité en C #? Sinon, pourquoi?

80
Thomas Levesque

Voici comment nous avons conçu C # 4.

Nous avons d'abord fait une liste de toutes les fonctionnalités possibles que nous pourrions penser à ajouter à la langue.

Ensuite, nous avons regroupé les fonctionnalités en "c'est mauvais, nous ne devons jamais le faire", "c'est génial, nous devons le faire" et "c'est bien mais ne le faisons pas cette fois".

Ensuite, nous avons examiné le budget dont nous disposions pour concevoir, implémenter, tester, documenter, expédier et maintenir les fonctionnalités "dois avoir" et nous avons découvert que nous étions 100% supérieurs au budget.

Nous avons donc déplacé un tas de trucs du seau "dois avoir" au seau "Nice to have".

Les propriétés indexées n'étaient jamais nulle part près en haut de la liste "dois avoir". Ils sont très bas sur la liste "Nice" et flirtent avec la liste "mauvaise idée".

Chaque minute que nous passons à concevoir, implémenter, tester, documenter ou entretenir la fonctionnalité Nice X est une minute que nous ne pouvons pas consacrer aux fonctionnalités impressionnantes A, B, C, D, E, F et G. Nous devons impitoyablement établir des priorités afin que nous ne faire les meilleures fonctionnalités possibles. Les propriétés indexées seraient Nice, mais Nice n'est nulle part assez proche pour être réellement implémenté.

113
Eric Lippert

Un indexeur C # 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 par exemple VB), et vous pouvez le changer avec IndexerNameAttribute si vous le souhaitez.

Je ne sais pas pourquoi, spécifiquement, il a été conçu de cette façon, mais cela semble être une limitation intentionnelle. Il est cependant cohérent avec les directives de conception de Framework, qui recommandent l'approche d'une propriété non indexée renvoyant un objet indexable pour les collections membres. C'est à dire. "être indexable" est un trait d'un type; s'il est indexable de plusieurs manières, il doit alors être divisé en plusieurs types.

19
Pavel Minaev

Parce que vous pouvez déjà le faire en quelque sorte, et cela vous oblige à penser dans les aspects OO, l'ajout de propriétés indexées ajouterait simplement plus de bruit au langage. 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érerais ne voir qu'une seule façon de faire quelque chose, plutôt que 10 façons. Mais bien sûr, c'est une opinion subjective.

14
Ion Todirel

J'avais l'habitude de privilégier l'idée de propriétés indexées, mais je me suis ensuite rendu compte que cela ajouterait une horrible ambiguïté et en fait dissuaderait la fonctionnalité. Les propriétés indexées signifieraient que vous n'avez pas d'instance de collection enfant. C'est à la fois bon et mauvais. Il est moins difficile à implémenter et vous n'avez pas besoin d'une référence à la classe propriétaire incluse. Mais cela signifie également que vous ne pouvez pas transmettre cette collection enfant à quoi que ce soit; vous devrez probablement énumérer chaque fois. Vous ne pouvez pas non plus y faire un avant-goût. Le pire de tout, vous ne pouvez pas dire en regardant une propriété indexée s'il s'agit de cette propriété ou d'une propriété de collection.

L'idée est rationnelle mais elle conduit juste à l'inflexibilité et à une maladresse brutale.

8
Joshua A. Schaeffer

Je trouve le manque de propriétés indexées très frustrant lorsque j'essaie d'écrire du code propre et concis. Une propriété indexée a une connotation très différente de celle de fournir une référence de classe indexée ou de fournir des méthodes individuelles. Je trouve un peu dérangeant que donner accès à un objet interne qui implémente une propriété indexée soit même considéré comme acceptable car cela rompt souvent l'un des composants clés de l'orientation d'objet: l'encapsulation.

Je rencontre ce problème assez souvent, mais je viens de le rencontrer à nouveau aujourd'hui, je vais donc vous fournir un exemple de code réel. L'interface et la classe en cours d'écriture stockent la configuration d'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 incorrect puisque les fragments de script ne sont qu'une partie de la configuration.

Si les propriétés indexées étaient disponibles en C #, j'aurais pu implémenter le code ci-dessous (la syntaxe est-ce que [key] a été changé en PropertyName [key]).

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, j'ai donc implémenté une classe pour les stocker et fourni un 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 les compter ou de les énumérer.

J'aurais pu implémenter cela comme:

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

Ce que je devrais probablement avoir, mais c'est une illustration utile de la raison pour laquelle l'utilisation de classes indexées en remplacement de cette fonctionnalité 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, vous le remarquerez, est considérablement 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
James Higgins

Une autre solution de contournement est répertoriée dans Création facile de propriétés qui prennent en charge l'indexation en C # , qui nécessite moins de travail.

[~ # ~] modifier [~ # ~] : je devrais également ajouter qu'en réponse à la question d'origine, que mon si nous pouvons accomplir le désir syntaxe, avec le support de la bibliothèque, alors je pense qu'il doit y avoir un cas très solide pour l'ajouter directement à la langue, afin de minimiser le gonflement de la langue.

2
cdiggins

Eh bien, je dirais qu'ils ne l'ont pas ajouté, car cela ne valait pas le coût de la conception, de la mise en œuvre, des tests et de la documentation.

Blague à part, c'est probablement parce que les solutions de contournement sont simples et que la fonctionnalité ne réduit jamais le temps par rapport aux avantages. Je ne serais pas surpris de voir cela apparaître comme un changement sur toute la ligne.

Vous avez également oublié de mentionner qu'une solution de contournement plus simple consiste simplement à créer une méthode régulière:

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

Il existe une solution générale simple utilisant lambdas pour proxy 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 l'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
bradgonesurfing

Je viens également de découvrir que vous pouvez utiliser des interfaces implémentées explicitement pour y parvenir, comme indiqué ici: Propriété indexée nommée en C #? (voir la deuxième façon indiquée dans cette réponse)

0
George Birbilis