Простое создание свойств, поддерживающих индексацию в C#
В C# я нахожу индексированные свойства чрезвычайно полезными. Например:
var myObj = new MyClass();
myObj[42] = "hello";
Console.WriteLine(myObj[42]);
Однако, насколько я знаю, нет синтаксического сахара для поддержки полей, которые сами поддерживают индексирование (пожалуйста, исправьте меня, если я ошибаюсь). Например:
var myObj = new MyClass();
myObj.field[42] = "hello";
Console.WriteLine(myObj.field[42]);
Мне нужно это потому, что я уже использую свойство index в своем классе, но у меня есть GetNumX()
, GetX()
, а также SetX()
функционирует следующим образом:
public int NumTargetSlots {
get { return _Maker.NumRefs; }
}
public ReferenceTarget GetTarget(int n) {
return ReferenceTarget.Create(_Maker.GetReference(n));
}
public void SetTarget(int n, ReferenceTarget rt) {
_Maker.ReplaceReference(n, rt._Target, true);
}
Как вы, вероятно, видите, экспонирование их как одного индексируемого свойства поля имело бы больше смысла. Я мог бы написать собственный класс для достижения этого каждый раз, когда я хочу синтаксический сахар, но весь стандартный код просто кажется ненужным.
Поэтому я написал собственный класс для инкапсуляции шаблона и облегчения создания свойств, которые можно индексировать. Таким образом, я могу добавить новое свойство следующим образом:
public IndexedProperty<ReferenceTarget> TargetArray {
get {
return new IndexedProperty<int, ReferenceTarget>(
(int n) => GetTarget(n),
(int n, ReferenceTarget rt) => SetTarget(n, rt));
}
}
Код для этого нового класса IndexedProperty выглядит следующим образом:
public class IndexedProperty<IndexT, ValueT>
{
Action<IndexT, ValueT> setAction;
Func<IndexT, ValueT> getFunc;
public IndexedProperty(Func<IndexT, ValueT> getFunc, Action<IndexT, ValueT> setAction)
{
this.getFunc = getFunc;
this.setAction = setAction;
}
public ValueT this[IndexT i]
{
get {
return getFunc(i);
}
set {
setAction(i, value);
}
}
}
Итак, мой вопрос: есть ли лучший способ сделать все это?
Если говорить точнее, есть ли в C# более идиоматический способ создания свойства индексируемого поля, и если нет, то как мне улучшить IndexedProperty
учебный класс?
РЕДАКТИРОВАТЬ: После дальнейших исследований, Джон Скит называет это " именным индексатором".
7 ответов
Ну, самое простое - это свойство вернуть объект, который реализует IList
,
Помните, что только то, что он реализует IList, не означает, что это сама коллекция, просто то, что он реализует определенные методы.
Технически это не правильный способ использования Stackru, но я нашел вашу идею настолько полезной, что расширил ее. Я думал, что это сэкономит людям время, если я опубликую то, что я придумал, и пример того, как его использовать.
Во-первых, мне нужно было иметь возможность поддерживать свойства get-only и set-only, поэтому я немного изменил ваш код для этих сценариев:
Получить и установить (очень незначительные изменения):
public class IndexedProperty<TIndex, TValue>
{
readonly Action<TIndex, TValue> SetAction;
readonly Func<TIndex, TValue> GetFunc;
public IndexedProperty(Func<TIndex, TValue> getFunc, Action<TIndex, TValue> setAction)
{
this.GetFunc = getFunc;
this.SetAction = setAction;
}
public TValue this[TIndex i]
{
get
{
return GetFunc(i);
}
set
{
SetAction(i, value);
}
}
}
Получить только:
public class ReadOnlyIndexedProperty<TIndex, TValue>
{
readonly Func<TIndex, TValue> GetFunc;
public ReadOnlyIndexedProperty(Func<TIndex, TValue> getFunc)
{
this.GetFunc = getFunc;
}
public TValue this[TIndex i]
{
get
{
return GetFunc(i);
}
}
}
Установить только:
public class WriteOnlyIndexedProperty<TIndex, TValue>
{
readonly Action<TIndex, TValue> SetAction;
public WriteOnlyIndexedProperty(Action<TIndex, TValue> setAction)
{
this.SetAction = setAction;
}
public TValue this[TIndex i]
{
set
{
SetAction(i, value);
}
}
}
пример
Вот простой пример использования. Я наследую от Collection и создаю именованный индексатор, как его назвал Джон Скит. Этот пример должен быть простым, а не практичным:
public class ExampleCollection<T> : Collection<T>
{
public IndexedProperty<int, T> ExampleProperty
{
get
{
return new IndexedProperty<int, T>(GetIndex, SetIndex);
}
}
private T GetIndex(int index)
{
return this[index];
}
private void SetIndex(int index, T value)
{
this[index] = value;
}
}
ExampleCollection in the Wild
Этот наскоро построенный модульный тест показывает, как он выглядит, когда вы ExampleCollection в проекте:
[TestClass]
public class IndexPropertyTests
{
[TestMethod]
public void IndexPropertyTest()
{
var MyExample = new ExampleCollection<string>();
MyExample.Add("a");
MyExample.Add("b");
Assert.IsTrue(MyExample.ExampleProperty[0] == "a");
Assert.IsTrue(MyExample.ExampleProperty[1] == "b");
MyExample.ExampleProperty[0] = "c";
Assert.IsTrue(MyExample.ExampleProperty[0] == "c");
}
}
Наконец, если вы хотите использовать версии только для получения и для установки, это выглядит так:
public ReadOnlyIndexedProperty<int, T> ExampleProperty
{
get
{
return new ReadOnlyIndexedProperty<int, T>(GetIndex);
}
}
Или же:
public WriteOnlyIndexedProperty<int, T> ExampleProperty
{
get
{
return new WriteOnlyIndexedProperty<int, T>(SetIndex);
}
}
В обоих случаях результат работает точно так, как вы ожидаете, что будет работать только свойство get-only/set-only.
Я думаю, что дизайн, который вы разместили, - это путь, с одним отличием, которое я определил бы интерфейс:
public interface IIndexed<IndexT, ValueT>
{
ValueT this[IndexT i] { get; set; }
}
А для обычных случаев я бы использовал класс, который вы задали в исходном вопросе (который бы реализовывал этот интерфейс).
Было бы хорошо, если бы библиотека базовых классов предоставила нам подходящий интерфейс, но это не так. Возвращение IList здесь было бы извращением.
Это не отвечает на ваш вопрос, но интересно отметить, что CIL поддерживает создание свойств, как вы описали - некоторые языки (например, F#) позволят вам определять их таким же образом.
this[]
Индексатор в C# это просто конкретный экземпляр одного из них, который переименован в Item
когда вы создаете свое приложение. Компилятор C# знает, как его прочитать, поэтому, если вы напишите "именованный индексатор", Target
в библиотеке F#, и попробуйте использовать его в C#, единственный способ получить доступ к свойству через ... get_Target(int)
а также void set_Target(int, ...)
методы. Отстой.
Почему бы не сделать так, чтобы ваш класс наследовал IList, тогда вы можете просто использовать индекс и добавить к нему свои собственные свойства. Хотя у вас все еще есть функции "Добавить и удалить", их нечестно не использовать. Кроме того, вам может быть полезно иметь их в будущем.
Для получения дополнительной информации о списках и массивах посмотрите: Какой из них лучше использовать массив или список<>?
РЕДАКТИРОВАТЬ:
В MSDN есть статья о свойствах индекса, на которую вы можете взглянуть. Не кажется сложным, просто утомительно.
http://msdn.microsoft.com/en-us/library/aa288464(VS.71).aspx
Есть еще один вариант, где вы можете создать альтернативный метод Add, но в зависимости от типа объекта ваш метод add может вызываться не всегда. Объяснил здесь:
Как переопределить метод Add List
РЕДАКТИРОВАТЬ 2: аналогично первому сообщению
Почему у вас нет скрытого списка объектов в вашем классе, а затем просто создайте свои собственные методы для получения данных. Таким образом, "Добавить" и "Удалить" не отображаются, и список уже проиндексирован.
Также, что вы подразумеваете под "именованным индексатором", вы ищете эквивалент строки ["My_Column_Name"]. Я нашел статью MSDN, которая может оказаться полезной, так как она показывает основной способ реализации этого свойства.
http://msdn.microsoft.com/en-us/library/146h6tk5.aspx
class Test
{
private List<T> index;
public T this[string name]{ get; set; }
public T this[int i]
{
get
{
return index[i];
}
set
{
index[i] = value;
}
}
}
После некоторых исследований я нашел немного другое решение, которое лучше соответствовало моим потребностям. Пример немного придуман, но он подходит для того, что мне нужно для его адаптации.
Использование:
MyClass MC = new MyClass();
int x = MC.IntProperty[5];
И код, чтобы заставить это работать:
public class MyClass
{
public readonly IntIndexing IntProperty;
public MyClass()
{
IntProperty = new IntIndexing(this);
}
private int GetInt(int index)
{
switch (index)
{
case 1:
return 56;
case 2:
return 47;
case 3:
return 88;
case 4:
return 12;
case 5:
return 32;
default:
return -1;
}
}
public class IntIndexing
{
private MyClass MC;
internal IntIndexing(MyClass mc)
{
MC = mc;
}
public int this[int index]
{
get { return MC.GetInt(index); }
}
}
}
Попробуйте явно реализованные интерфейсы, как показано на втором способе, предложенном в ответе здесь: Именованное индексированное свойство в C#?