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

Я знаю, я знаю... Ответ Эрика Липперта на этот вопрос обычно похож на "потому что он не стоил затрат на проектирование, внедрение, тестирование и документирование".

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

Кстати, этот код использует еще одну новую функцию: индексированные свойства (более подробно рассмотрим эти квадратные скобки после Range.) Но эта функция доступна только для COM-взаимодействия; вы не можете создавать свои собственные индексированные свойства в С# 4.0.

ОК, но почему? Я уже знал и сожалел о невозможности создания индексированных свойств в С#, но это предложение заставило меня задуматься об этом. Я вижу несколько веских причин для его реализации:

  • CLR поддерживает его (например, PropertyInfo.GetValue имеет параметр index), поэтому нам жаль, что мы не можем использовать его в С#
  • поддерживается для 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, индексированное.

Как вы думаете? Вы хотите увидеть эту функцию на С#? Если нет, то почему?

Ответ 1

Здесь мы разработали С# 4.

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

Затем мы разделили функции на "это плохо, мы никогда не должны этого делать", "это потрясающе, мы должны это делать", и "это хорошо, но не делайте этого на этот раз".

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

Итак, мы переместили кучу вещей из ведро "gotta have" в ведро "nice to have".

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

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

Ответ 2

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

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

Ответ 3

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

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

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

foo.Values[0]

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

Ответ 7

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

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

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

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

Ответ 8

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

Только для индексирования только для чтения

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

и factory

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

Ответ 9

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