Является ли автоматическое добавление индексаторов в словари и коллекции хорошим дизайнерским решением?

когда индексатор может автоматически добавлять элементы в коллекцию / словарь? Разумно ли это или противоречит передовой практике?

public class I { /* snip */  }
public class D : Dictionary<string, I>
{
    public I this[string name]
    {
        get
        {
            I item;
            if (!this.TryGetValue(name, out item))
            {
                item = new I();
                this.Add(name, item);
            }
            return item;
        }
    }
}

пример того, как это может использоваться в коллекции:

public class I
{
    public I(string name) {/* snip */}
    public string Name { get; private set; }
    /* snip */
}
public class C : Collection<I>
{
    private Dictionary<string, I> nameIndex = new Dictionary<string, I>();

    public I this[string name]
    {
        get
        {
            I item;
            if (!nameIndex.TryGetValue(name, out item))
            {
                item = new I(name);
                this.Add(item); // Will also add the item to nameIndex
            }
            return item;
        }
    }

    //// Snip: code that manages nameIndex 
    // protected override void ClearItems()
    // protected override void InsertItem(int index, I item)
    // protected override void RemoveItem(int index)
    // protected override void SetItem(int index, I item)
}

6 ответов


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

во-первых, наследование от типов коллекций .NET BCL обычно не является хорошей идеей. Основная причина этого заключается в том, что большинство методов этих типов (например,Add и Remove) не являются виртуальными - и если вы предоставляете свои собственные реализации в производном классе, они не будут вызываться, если вы передадите свою коллекцию как базовый тип. В вашем случае, скрывая Dictionary<TK,TV> свойство индексатора, вы создаете ситуацию, когда вызов с использованием ссылки на базовый класс будет делать что-то другое, чем вызов с использованием ссылки на производный класс ... нарушение Принцип Подстановки Лисков:

var derived = new D();
var firstItem = derived["puppy"]; // adds the puppy entry

var base = (Dictionary<string,I>)derived;
var secondItem = base["kitten"]; // kitten WAS NOT added .. BAD!

во-вторых, и что более важно, создание индексатора, который вставляет элемент при попытке найти один совершенно неожиданный. Индексаторы четко определили get и set операции - реализация get операция по изменению коллекции очень плохая.

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

public static class DictionaryExtensions
{ 
    public static TValue FindOrAdd<TKey,TValue>( 
             this IDictionary<TKey,TValue> dictionary, TKey key, TValue value )
        where TValue : new()
    { 
        TValue value; 
        if (!this.TryGetValue(key, out value)) 
        { 
            value = new TValue(); 
            this.Add(key, value); 
        } 
        return value; 
    } 
}

без другой информации о том, что вы делаете, это выглядит как удивительное поведение для меня. Я надеюсь, что вы сделаете это очень ясно из контекста (т. е. назовите его AutoInitializingDictionary или что-то) что и следовало ожидать.

Я лично предпочел бы сделать это методом, а не индексатором; что-то вроде D.FindOrCreate. (У меня такое чувство, что есть идиоматическое название метода, который делает это, которое я временно забыл.)


Я бы сказал, что это нарушает два принципа. 1) принцип наименьшего сюрприза. И 2) что геттеры ничего не изменит.

Я не ожидал бы добавить пару {"foo", null}, если foo не существует в colleciton.

x = collection["Foo"]

Я думаю, что это прекрасно, пока это поведение прекрасно понял. У меня 2 класса декораторов:

public class DefaultValueDictionary<K, V> : IDictionary<K, V>
{
  public DefaultValueDictionary(IDictionary<K, V> baseDictionary, Func<K, V> defaultValueFunc)
  {
    ...
  }
}

и

public class ParameterlessCtorDefaultValueDictionary<K, V> 
            : DefaultValueDictionary<K, V> where V : new()
{
  public ParameterlessCtorDefaultValueDictionary(IDictionary<K, V> baseDictionary)
     : base(baseDictionary, k => new V())
  {
    ...
  }
}

второй класс идеально подходит для счетчиков и шаблонов, таких как IDictionary<K,List<V>>; Я могу сделать

var dict = new ParameterlessCtorDefaultValueDictionary<string, int>();
...
dict[key]++;

вместо трудоемкий:

int count;
if(!dict.TryGetValue(key, out count))
  dict[count] = 1;
else dict[count] = count + 1;

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


когда это приемлемо для индексатора автоматическое добавление элементов в a коллекция/словарь?

никогда

это разумно, или наоборот передовая практика?

противоречит лучшим практикам

тем не менее, если класс назван соответствующим образом, это было бы приемлемо. Я бы лично использовал GetOrAdd.