Почему в C # не реализованы индексированные свойства?

83

Я знаю, я знаю ... Эрик Липперт обычно отвечает на этот вопрос примерно так: « Потому что это не стоило затрат на его разработку, внедрение, тестирование и документирование ».

Но все же мне хотелось бы получить более подробное объяснение ... Я читал это сообщение в блоге о новых функциях C # 4 , и в разделе о COM-взаимодействии мое внимание привлекла следующая часть:

Кстати, в этом коде используется еще одна новая функция: индексированные свойства (обратите внимание на квадратные скобки после 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;
        }
    }
}

Но, конечно, в этом случае свойство действительно вернет a IIndexer<int, string>и не будет на самом деле индексированным свойством ... Было бы лучше сгенерировать реальное индексированное свойство CLR.

Что вы думаете ? Хотели бы вы увидеть эту функцию на C #? Если нет, то почему?

Томас Левеск
источник
1
У меня такое ощущение, что это еще одна из тех проблем, «которые мы получаем по X, но не больше, чем по Y» .
ChaosPandion
1
@ChaosPandion, да, вы, вероятно, правы ... Но эту функцию, вероятно, было бы довольно легко реализовать, и хотя она, конечно, не обязательна, она определенно попадает в категорию "приятно иметь"
Томас Левеск,
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.
Эрик Липперт
12
+1: Читая это, люди могут многое узнать об управлении программными проектами. И это всего несколько строк.
Брайан Маккей
22

Индексатор AC # - это индексируемое свойство. Он назван Itemпо умолчанию (и вы можете ссылаться на него, например, в VB), и вы можете изменить его с помощью IndexerNameAttribute, если хотите.

Я не уверен, почему именно так оно было разработано, но, похоже, это намеренное ограничение. Однако это согласуется с Рекомендациями по проектированию Framework, которые рекомендуют подход, когда неиндексированное свойство возвращает индексируемый объект для коллекций членов. Т.е. «быть индексируемым» - это характеристика типа; если он индексируется более чем одним способом, его действительно следует разделить на несколько типов.

Павел Минаев
источник
1
Спасибо! Я неоднократно боролся с ошибками при реализации интерфейсов COM-взаимодействия, у которых есть indxer по умолчанию (DISPID 0), который импортируется как this [int], но чье имя изначально не было «Item» (иногда это «item», «value» или подобное ). Это все равно компилируется и запускается, но приводит к предупреждениям FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes, проблемам соответствия CLS (идентификаторы отличаются только регистром) и т. Д., Поскольку имя не совсем подходит. [IndexerName] - это все, что было нужно, но мне так и не удалось его найти.
puetzk
Спасибо!!! Атрибут IndexerName позволил мне завершить преобразование сборки VB в C # без нарушения подписей MSIL.
Джон Тирджан,
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 05
Вам нравится распределять переходные процессы?
Джошуа
9

Раньше я отдавал предпочтение идее индексированных свойств, но потом понял, что это добавит ужасной двусмысленности и фактически лишит функциональности. Индексированные свойства означают, что у вас нет экземпляра дочерней коллекции. Это и хорошо, и плохо. Это проще реализовать, и вам не нужна обратная ссылка на включающий класс владельца. Но это также означает, что вы не можете передать эту дочернюю коллекцию чему-либо; вам, вероятно, придется перечислять каждый раз. Вы также не можете выполнить на нем foreach. Хуже всего то, что, глядя на проиндексированное свойство, нельзя сказать, является ли это свойством коллекции.

Идея рациональная, но только приводит к негибкости и резкой неловкости.

Джошуа А. Шеффер
источник
Интересные мысли, даже если ответ немного запоздал;). Вы делаете очень хорошие замечания, +1.
Thomas Levesque
В vb.net класс может иметь как индексируемое, так и неиндексированное свойство с одним и тем же именем [например, Bar]. Выражение Thing.Bar(5)будет использовать индексированное свойство Baron Thing, тогда как (Thing.Bar)(5)будет использовать неиндексированное свойство, Barа затем использовать индексатор по умолчанию для полученного объекта. На мой взгляд, разрешение Thing.Bar[5]быть свойством Thing, а не свойством Thing.Bar, - это хорошо, поскольку, среди прочего, возможно, что в какой-то момент времени значение слова Thing.Bar[4]может быть ясным, но надлежащий эффект ...
supercat 02
... что-то вроде var temp=Thing.Bar; do_stuff_with_thing; var q=temp[4]может быть непонятным. Рассмотрим также понятие, которое Thingможет содержать данные Barв поле, которое может быть либо совместно используемым неизменяемым объектом, либо неразделенным изменяемым объектом; попытка записи в то время, Barкогда резервное поле является неизменяемым, должна создавать изменяемую копию, а попытка чтения из нее - нет. Если Barэто именованное индексированное свойство, индексированный метод получения может оставить резервную коллекцию в покое (изменяемой или нет), в то время как метод установки может при необходимости создать новую изменяемую копию.
supercat 02
индексатор собственности - это не
счетчик
6

Я считаю, что отсутствие индексированных свойств очень расстраивает, когда я пытаюсь написать чистый и лаконичный код. Индексированное свойство имеет совершенно иной смысл, чем предоставление индексированной ссылки на класс или предоставление отдельных методов. Меня немного беспокоит то, что предоставление доступа к внутреннему объекту, реализующему индексированное свойство, даже считается приемлемым, поскольку это часто нарушает один из ключевых компонентов объектной ориентации: инкапсуляцию.

Я сталкиваюсь с этой проблемой достаточно часто, но сегодня я столкнулся с ней снова, поэтому я приведу пример кода из реального мира. Написанный интерфейс и класс хранят конфигурацию приложения, которая представляет собой набор слабо связанной информации. Мне нужно было добавить именованные фрагменты сценария, и использование безымянного индексатора классов означало бы очень неправильный контекст, поскольку фрагменты сценария являются только частью конфигурации.

Если бы индексированные свойства были доступны в C #, я мог бы реализовать приведенный ниже код (синтаксис: this [key] изменен на 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
}

К сожалению, индексированные свойства не реализованы, поэтому я реализовал класс для их хранения и предоставил к нему доступ. Это нежелательная реализация, потому что целью класса конфигурации в этой модели предметной области является инкапсуляция всех деталей. Клиенты этого класса будут обращаться к определенным фрагментам скрипта по имени, и у них нет причин для их подсчета или перечисления.

Я мог бы реализовать это как:

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 # , которое требует меньше усилий.

РЕДАКТИРОВАТЬ : Я также должен добавить, что в ответ на исходный вопрос, что, если мы сможем выполнить желаемый синтаксис с поддержкой библиотеки, тогда я думаю, что должны быть очень веские аргументы, чтобы добавить его непосредственно в язык, чтобы свести к минимуму языковой раздувание.

cdiggins
источник
2
В ответ на ваше изменение: я не думаю, что это вызовет раздувание языка; синтаксис уже существует для class indexers ( 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];
брэдгонсерфинг
источник
Умно, но, наверное, лучше было бы кешировать индексатор, а не создавать его каждый раз;)
Томас Левеск