Чому C # не реалізує індексовані властивості?


83

Я знаю, я знаю ... Відповідь Еріка Ліпперта на подібне запитання, як правило, виглядає приблизно так: " тому що це не коштувало витрат на проектування, впровадження, тестування та документування цього ".

Але все-таки я хотів би отримати кращі пояснення ... Я читав цю публікацію в блозі про нові функції C # 4 , і в розділі про COM Interop привернула мою увагу наступна частина:

До речі, цей код використовує ще одну нову функцію: індексовані властивості (уважніше розгляньте ці квадратні дужки після Range.) Але ця функція доступна лише для взаємодії COM; ви не можете створити свої власні індексовані властивості в C # 4.0 .

Добре, але чому? Я вже знав і шкодував, що неможливо створити індексовані властивості в C #, але це речення змусило мене ще раз задуматися над цим. Я бачу кілька вагомих причин для його реалізації:

  • CLR підтримує це (наприклад, PropertyInfo.GetValueмає indexпараметр), тому шкода, що ми не можемо цим скористатися в C #
  • він підтримується для взаємодії COM, як показано в статті (за допомогою динамічного відправлення)
  • він реалізований у VB.NET
  • вже можна створювати індексатори, тобто застосовувати індекс до самого об'єкта, тому, мабуть, не буде великою проблемою поширити ідею на властивості, зберігаючи той самий синтаксис і просто замінюючи thisна ім'я властивості

Це дозволило б писати такі речі:

public class Foo
{
    private string[] _values = new string[3];
    public string Values[int index]
    {
        get { return _values[index]; }
        set { _values[index] = value; }
    }
}

На даний момент єдиним обхідним шляхом, яке мені відомо, є створення внутрішнього класу ( ValuesCollectionнаприклад), який реалізує індексатор, і змінаValues властивості так, щоб воно повертало екземпляр цього внутрішнього класу.

Зробити це дуже просто, але дратує ... Тож, можливо, компілятор міг би це зробити за нас! Варіантом може бути створення внутрішнього класу, який реалізує індексатор, та виставлення його через загальнодоступний загальний інтерфейс:

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

Але, звичайно, у такому випадку майно фактично поверне aIIndexer<int, string> , а насправді не було б індексованим властивістю ... Краще було б створити справжнє індексоване властивість CLR.

Що ти думаєш ? Ви хотіли б бачити цю функцію в C #? Якщо ні, то чому?


1
Я відчуваю, що це ще одна з тих "ми отримуємо запити на X, але не більше, ніж на Y" .
ChaosPandion

1
@ChaosPandion, так, ти, мабуть, маєш рацію ... Але цю функцію, мабуть, було б досить просто реалізувати, і хоча вона, безумовно, не є “must have”, вона точно потрапляє в категорію “nice to have”
Томас Левеск

4
Індексатори вже трохи дратують з точки зору CLR. Вони додають новий граничний регістр до коду, який хоче працювати зі властивостями, оскільки тепер будь-яка властивість потенційно може мати параметри індексатора. Я думаю, що реалізація C # має сенс, оскільки концепція, яку індексатор зазвичай представляє, є не властивістю об'єкта, а його "вмістом". Якщо ви надаєте довільні властивості індексатора, ви маєте на увазі, що клас може мати різні групи вмісту, що, природно, призводить до інкапсуляції складного підвмісту як нового класу. Моє запитання: чому CLR надає індексовані властивості?
Ден Брайант,

1
@tk_ дякую за ваш конструктивний коментар. Ви публікуєте подібні коментарі до всіх дописів про мови, які не є Free Pascal? Ну, сподіваюся, це змусить вас почувати себе добре ...
Томас Левеск

3
Це одна з небагатьох ситуацій, коли C ++ / CLI та VB.net кращі за C #. Я реалізував багато індексованих властивостей у своєму коді C ++ / CLI, і тепер при перетворенні його на C # я повинен знайти обхідні шляхи для всіх них. :-( СМОС !!! !!! Твоє Це дозволило б писати, що такі речі я робив роками.
Тобіас Кнаус

Відповіді:


122

Ось як ми розробили C # 4.

Спочатку ми склали список усіх можливих функцій, які ми могли подумати додати до мови.

Потім ми розділили функції на "це погано, ми ніколи не повинні цього робити", "це чудово, ми повинні це робити" і "це добре, але не давайте цього робити".

Потім ми розглянули, скільки бюджету нам довелося розробити, впровадити, протестувати, задокументувати, доставити та підтримувати «треба мати» функції, і виявили, що ми перевищили бюджет на 100%.

Тож ми перенесли купу речей із відра "повинен бути" у відро "приємно мати".

Проіндексовані властивості ніколи не знаходились поблизу вершини списку "повинен бути". Вони дуже низько потрапляють до списку "приємних" і заграють до списку "поганих ідей".

Кожну хвилину, яку ми витрачаємо на проектування, впровадження, тестування, документування чи підтримку приємної функції X, - це хвилина, яку ми не можемо витратити на приголомшливі функції A, B, C, D, E, F та G. Нам слід безжально ставити пріоритети, щоб ми лише робити найкращі можливості. Проіндексовані властивості були б непоганими, але приємний не є десь навіть настільки добрим, щоб насправді бути реалізованим.


20
Чи можу я додати голос за внесення його до поганого списку? Я насправді не бачу, як поточна реалізація є великим обмеженням, коли ви можете просто викрити вкладений тип, який реалізує індексатор. Я думаю, ви б почали бачити багато хакерства, щоб спробувати щось забити в прив'язку даних та властивості, які повинні бути методами.
Джош

11
І, сподіваємось, автоматично реалізована INotifyPropertyChanged набагато вище у списку, ніж індексовані властивості. :)
Джош

3
@ Ерік, гаразд, це те, про що я підозрював ... дякую за відповідь у будь-якому разі! Думаю, я можу жити без індексованих властивостей, як це роками;)
Томас Левеск

5
@Martin: Я не фахівець у тому, як великі команди програмного забезпечення визначають бюджети. Ваше питання має бути адресоване Сомі, Джейсону Зандеру чи Скотту Вілтамуту, я вважаю, що всі вони час від часу пишуть блоги. Ваше порівняння із Scala - це порівняння яблук із апельсинами; Scala не несе більшості витрат, як у C #; щоб назвати одного, у нього немає мільйонів користувачів з надзвичайно важливими вимогами до зворотної сумісності. Я міг би назвати ще багато факторів, які можуть спричинити значні відмінності у вартості між C # та Scala.
Eric Lippert,

12
+1: Читаючи це, люди можуть дізнатися багато нового про управління програмними проектами. І це всього кілька рядків.
Брайан Маккей,

22

Індексатор AC # є індексованою властивістю. Він іменований Itemза замовчуванням (і ви можете посилатися на нього як такий із, наприклад, VB), і ви можете змінити його за допомогою IndexerNameAttribute, якщо хочете.

Я не впевнений, чому, зокрема, це було розроблено саме так, але, схоже, це навмисне обмеження. Однак, це узгоджується з Настановами Framework Design, які рекомендують підхід до неіндексованого властивості, що повертає індексуваний об'єкт для колекцій членів. Тобто "бути індексованим" - це риса типу; якщо він індексується більш ніж одним способом, тоді його дійсно слід розділити на кілька типів.


1
Дякую! Я неодноразово стикався з помилками під час реалізації інтерфейсів взаємодії COM, які мають за замовчуванням індексатор (DISPID 0), який імпортується як цей [int], але ім'я якого спочатку не було "Елемент" (іноді це "Елемент", "Значення" або подібне ). Це так чи інакше компілюється та виконується, але веде до попереджень FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes, проблем із дотриманням CLS (ідентифікатори відрізняються лише у випадку) тощо, оскільки назва не зовсім підходить. [IndexerName] - це все, що було потрібно, але мені так і не вдалося його знайти.
puetzk

Дякую!!! Атрибут IndexerName дозволив мені завершити перетворення збірки VB на C #, не порушуючи підписів MSIL.
Jon Tirjan

15

Оскільки ви вже можете це зробити, і це змушує вас думати в аспектах ОО, додавання індексованих властивостей просто додасть більше шуму мові. І просто ще один спосіб зробити іншу справу.

class Foo
{
    public Values Values { ... }
}

class Values
{
    public string this[int index] { ... }    
}

foo.Values[0]

Я особисто вважаю за краще бачити лише один спосіб щось зробити, а не 10 способів. Але, звичайно, це суб’єктивна думка.


2
+1, це набагато кращий спосіб його реалізації, ніж вигадування мови за допомогою конструкцій VB5.
Джош

1
+1, бо я б так це зробив. І якби ви були хорошими, ви могли б зробити цей загальний засіб.
Тоні

10
Однією з проблем такого підходу є те, що інший код може зробити копію індексатора, і незрозуміло, якою має бути семантика, якщо це відбувається. Якщо в коді написано "var userList = Foo.Users; Foo.RemoveSomeUsers (); someUser = userList [5];" це повинно бути тим, що раніше було елементом [5] Foo (до RemoveSomeUsers), або після? Якби один userList [] був індексованою властивістю, його не потрібно було б виставляти безпосередньо.
supercat

Вам подобається розподіляти перехідні процеси?
Джошуа

9

Раніше я віддавав перевагу ідеї індексованих властивостей, але потім зрозумів, що це додасть жахливу двозначність і насправді позбавить функціональності. Індексовані властивості означатимуть, що у вас немає екземпляра дочірньої колекції. Це і добре, і погано. Це менше проблем із реалізацією, і вам не потрібна повернення до класу власника, що вкладається. Але це також означає, що ви не можете нікому передати цю колекцію дітей; вам, мабуть, доведеться перераховувати кожен раз. Ви також не можете зробити передчуття на це. Найгірше за все, ви не можете визначити, переглядаючи проіндексовану властивість, це чи властивість колекції.

Ідея раціональна, але вона просто призводить до негнучкості та різкої незграбності.


Цікаві думки, навіть якщо відповідь трохи запізнилася;). Ви ставите дуже хороші бали, +1.
Томас Левеск,

У vb.net клас може мати як індексовану, так і неіндексовану властивість з однойменною назвою [наприклад Bar]. Вираз Thing.Bar(5)буде використовувати індексовану властивість Barна Thing, в той час як (Thing.Bar)(5)буде використовувати неіндексованих властивість , Barа потім використовувати індексатор за замовчуванням результуючого об'єкта. На мою прив'язку, дозволяти Thing.Bar[5]бути властивістю Thing, а не властивістю Thing.Bar, добре, оскільки, крім іншого, можливо, що в якийсь момент часу значення Thing.Bar[4]може бути зрозумілим, але належний ефект ...
supercat

... щось подібне var temp=Thing.Bar; do_stuff_with_thing; var q=temp[4]може бути незрозумілим. Розглянемо також поняття, яке Thingможе містити дані Barв полі, яке може бути або спільним незмінним об'єктом, або незміненим змінним об'єктом; спроба писати в тому Barвипадку, коли поле резервної копії незмінне, має робити змінну копію, але спроба читати з нього не повинна. Якщо Barє іменованим індексованим властивістю, індексований отримувач може залишити колекцію резервних копій у спокої (незалежно від того, змінюється вона чи ні), тоді як установник може створити нову змінну копію, якщо це необхідно.
supercat

індексатор властивості не є перелічувачем - це ключ
Джордж Бірбіліс

6

Мені здається, що відсутність індексованих властивостей дуже засмучує при спробі написати чистий, стислий код. Індексована властивість має зовсім інший відтінок, ніж надання посилання на клас, що індексується, або надання окремих методів. Мені здається трохи тривожним, що надання доступу до внутрішнього об'єкта, що реалізує індексовану властивість, навіть вважається прийнятним, оскільки це часто порушує одну з ключових складових орієнтації об'єкта: інкапсуляцію.

Я стикаюся з цією проблемою досить часто, але я щойно зіткнувся з нею сьогодні, тому надам приклад реального коду. Інтерфейс та клас, що записується, зберігає конфігурацію програми, яка є сукупністю вільно пов’язаної інформації. Мені потрібно було додати іменовані фрагменти сценарію, і використання неіменованого індексатора класів означало б дуже неправильний контекст, оскільки фрагменти сценарію є лише частиною конфігурації.

Якби в C # були доступні індексовані властивості, я міг би реалізувати наведений нижче код (синтаксис - це [ключ] змінено на PropertyName [ключ]).

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
}

На жаль, індексовані властивості не реалізовані, тому я реалізував клас для їх зберігання та надав доступ до нього. Це небажана реалізація, оскільки метою класу конфігурації в цій моделі домену є інкапсуляція всіх деталей. Клієнти цього класу будуть отримувати доступ до конкретних фрагментів сценарію за іменами і не матимуть підстав вважати або перераховувати їх.

Я міг реалізувати це як:

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

Що я, мабуть, мав би мати, але це корисна ілюстрація того, чому використання індексованих класів як заміни цієї відсутньої функції часто не є розумною заміною.

Для реалізації подібних можливостей як індексованої властивості мені довелося написати наведений нижче код, який ви помітите, значно довший, складніший і, отже, важчий для читання, розуміння та обслуговування.

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

2

Ще одне обхідне рішення наведено при простому створенні властивостей, які підтримують індексацію в C # , що вимагає меншої роботи.

EDIT : Я також повинен додати, що у відповідь на оригінальне запитання, що, якщо ми зможемо виконати бажаний синтаксис, за підтримки бібліотеки, то я думаю, що має бути дуже вагомий випадок, щоб додати його безпосередньо до мови, щоб мінімізувати роздуття мови.


2
У відповідь на вашу редакцію: я не думаю, що це спричинить роздуття мови; синтаксис вже існує для індексаторів класів ( this[]), їм просто потрібно дозволити ідентифікатор замість this. Але я сумніваюся, що це коли-небудь буде включено в цю мову, з причин, пояснених Еріком у своїй відповіді
Томас Левеск

1

Ну, я б сказав, що вони його не додали, тому що це не коштувало витрат на проектування, впровадження, тестування та документування.

Жартуючи, це, мабуть, тому, що обхідні шляхи прості, і ця функція ніколи не зменшує час проти вигоди. Я б не здивувався, побачивши, що це з’являється як зміна внизу.

Ви також забули згадати, що простішим способом вирішення проблеми є просто зробити звичайний метод:

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

Дуже правильно. Якщо синтаксис властивостей є життєво важливим, тоді ви можете скористатися обхідним шляхом Ion (можливо, з деякими узагальненнями, щоб дозволити різні типи повернення). Незважаючи на це, я думаю, це говорить про відносну легкість отримання тієї самої роботи без додаткових мовних можливостей.
Рон Уорголік,

1

Існує просте загальне рішення, яке використовує лямбди для проксі-функцій індексації

Для індексації лише для читання

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

Для змінної індексації

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

і фабрика

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

у своєму власному коді я використовую це як

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

}

і з екземпляром MoineauFlankContours я можу це зробити

MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];

Розумно, але, мабуть, було б краще кешувати індексатор, замість того, щоб створювати його щоразу;)
Томас Левеск,

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.