Почему 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 ответов:
вот как мы разработали 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#? (см. Второй способ, показанный в этом ответе)