Почему C# не реализует индексированные свойства?
Я знаю, я знаю... Ответ Эрика Липперта на этот вопрос обычно звучит примерно так: " потому что это не стоило затрат на его разработку, внедрение, тестирование и документирование ".
Но все же, я хотел бы получить лучшее объяснение... Я читал этот пост в блоге о новых возможностях 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 ответов
Вот как мы разработали C# 4.
Сначала мы составили список всех возможных функций, которые мы могли бы добавить к языку.
Затем мы объединили функции в "это плохо, мы никогда не должны этого делать", "это круто, мы должны это делать" и "это хорошо, но давайте не будем делать это на этот раз".
Затем мы посмотрели, сколько у нас было бюджета, чтобы спроектировать, внедрить, протестировать, документировать, отправить и поддерживать функции "должен иметь", и обнаружили, что мы на 100% превышаем бюджет.
Таким образом, мы переместили кучу вещей из корзины "надо иметь" в корзину "приятно иметь".
Индексированные свойства никогда не были в верхней части списка "должен иметь". Они очень низки в списке "хороших" и заигрывают со списком "плохих идей".
Каждая минута, которую мы тратим на разработку, реализацию, тестирование, документирование или обслуживание замечательной функции X, - это минута, которую мы не можем потратить на удивительные функции A, B, C, D, E, F и G. Мы должны безжалостно расставить приоритеты, чтобы мы только сделать лучшие возможные функции. Индексированные свойства были бы хороши, но хороши даже не настолько близко, чтобы быть действительно реализованными.
A C# indexer является индексированным свойством. Это называется Item
по умолчанию (и вы можете ссылаться на него как таковое, например, из VB), и вы можете изменить его с помощью IndexerNameAttribute, если хотите.
Я не уверен, почему, в частности, это было разработано таким образом, но это, кажется, преднамеренное ограничение. Это, однако, согласуется с Руководством по проектированию платформы, в котором рекомендуется использовать неиндексированное свойство, возвращающее индексируемый объект для коллекций элементов. Т.е. "индексируемость" - это черта типа; если он индексируется несколькими способами, то его действительно следует разделить на несколько типов.
Поскольку вы уже можете делать это, и это заставляет вас думать об аспектах OO, добавление индексированных свойств просто добавит больше шума в язык. И просто еще один способ сделать что-то другое.
class Foo
{
public Values Values { ... }
}
class Values
{
public string this[int index] { ... }
}
foo.Values[0]
Я лично предпочел бы видеть только один способ сделать что-то, а не 10 способов. Но конечно это субъективное мнение.
Раньше я поддерживал идею индексированных свойств, но потом понял, что это добавит ужасную двусмысленность и фактически лишит функциональности функциональность. Индексированные свойства означают, что у вас нет экземпляра дочерней коллекции. Это и хорошо, и плохо. Это меньше проблем для реализации, и вам не нужна ссылка обратно на класс владельца. Но это также означает, что вы не можете передать эту дочернюю коллекцию чему-либо; вам, вероятно, придется перечислять каждый раз. Вы также не можете сделать foreach на этом. Хуже всего то, что по индексируемому свойству нельзя определить, является ли оно свойством или свойством коллекции.
Идея рациональна, но она просто приводит к негибкости и резкой неловкости.
Я считаю, что отсутствие индексированных свойств очень расстраивает, когда я пытаюсь написать чистый, лаконичный код. Индексированное свойство имеет совсем другое значение, чем предоставление индексированной ссылки на класс или предоставление отдельных методов. Меня немного беспокоит, что предоставление доступа к внутреннему объекту, который реализует индексированное свойство, даже считается приемлемым, поскольку это часто нарушает один из ключевых компонентов ориентации объекта: инкапсуляцию.
Я сталкиваюсь с этой проблемой достаточно часто, но сегодня я столкнулся с ней снова, поэтому приведу пример кода из реальной жизни. Записываемый интерфейс и класс хранят конфигурацию приложения, которая представляет собой набор свободно связанной информации. Мне нужно было добавить именованные фрагменты скрипта, и использование индексатора класса без имени подразумевало бы очень неправильный контекст, так как фрагменты скрипта являются только частью конфигурации.
Если бы индексированные свойства были доступны в 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#", что требует меньше работы.
РЕДАКТИРОВАТЬ: Я должен также добавить, что в ответ на первоначальный вопрос, что если я могу достичь желаемого синтаксиса, с поддержкой библиотеки, то я думаю, что должен быть очень веский аргумент, чтобы добавить его непосредственно к языку, чтобы свести язык к минимуму.
Ну, я бы сказал, что они не добавили его, потому что это не стоило затрат на его разработку, внедрение, тестирование и документирование.
Шутки в сторону, вероятно, потому что обходные пути просты, и эта функция никогда не сокращает время и выгоду. Я не был бы удивлен видеть это как изменение вниз по линии все же.
Вы также забыли упомянуть, что более простой обходной путь - сделать обычный метод:
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#? (см. второй способ, показанный в этом ответе)