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


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

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

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

9 ответов:

вот как мы разработали C# 4.

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

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

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

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

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

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

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

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

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

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

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

foo.Values[0]

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

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

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

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

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

Если бы индексированные свойства были доступны в 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;
    }
}

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

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

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

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

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

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

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

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

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

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