Почему C # не реализует индексированные свойства? - PullRequest
79 голосов
/ 11 мая 2010

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

Но все же, я хотел бы получить лучшее объяснение ... Я читал этот пост в блоге о новых возможностях 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;
        }
    }
}

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

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

Ответы [ 9 ]

111 голосов
/ 11 мая 2010

Вот как мы спроектировали C # 4.

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

Затем мы сгруппировали функции в «это плохо, мы никогда не должны этого делать», «это здорово, мы должны это сделать», и «это хорошо, но давайте не будем делать это на этот раз».

Затем мы посмотрели, сколько у нас было бюджета на разработку, реализацию, тестирование, документирование, поставку и поддержку функций «должен иметь», и обнаружили, что мы на 100% превышаем бюджет.

Итак, мы переместили кучу вещей из корзины "должен иметь" в корзину "приятно иметь".

Индексированные свойства никогда не были нигде рядом с вершиной списка "должен иметь". Они очень низки в списке «хороших» и заигрывают со списком «плохих идей».

Каждую минуту, которую мы тратим на разработку, реализацию, тестирование, документирование или поддержку полезной функции X, мы не можем потратить на потрясающие функции A, B, C, D, E, F и G. Мы должны безжалостно расставить приоритеты, чтобы что мы делаем только лучшие возможности. Индексированные свойства были бы хороши, но они не настолько хороши, чтобы их можно было реализовать на практике.

19 голосов
/ 11 мая 2010

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

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

14 голосов
/ 11 мая 2010

Поскольку вы уже можете делать это, и это заставляет вас думать об аспектах OO, добавление индексированных свойств просто добавит больше шума в язык. И просто еще один способ сделать что-то другое.

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

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

foo.Values[0]

Лично я предпочел бы видеть только один способ сделать что-то, а не 10 способов. Но конечно это субъективное мнение.

8 голосов
/ 24 августа 2012

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

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

5 голосов
/ 05 апреля 2014

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

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

Если бы индексированные свойства были доступны в 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 голосов
/ 27 июля 2010

Другой обходной путь указан в Простое создание свойств, поддерживающих индексирование в C # , требующее меньше работы.

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

1 голос
/ 28 мая 2014

Существует простое общее решение, использующее лямбда-выражения для функции индексирования

Для индексации только для чтения

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];
1 голос
/ 11 мая 2010

Ну, я бы сказал, что они не добавили его, потому что это не стоило затрат на его разработку, внедрение, тестирование и документирование.

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

Вы также забыли упомянуть, что проще обойтись, просто сделайте обычный метод:

public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}
0 голосов
/ 30 мая 2016

Я также обнаружил, что вы можете использовать явно реализованные интерфейсы для достижения этой цели, как показано здесь: Именованное индексированное свойство в C #? (см. Второй способ, показанный в этом ответе)

...