Есть ли встроенный общий интерфейс с параметром ковариантного типа, возвращаемым индексатором?

В этой теме

Как получить null вместо KeyNotFoundException доступ к значению словаря по ключу?

в моем собственном ответе я использовал явную реализацию интерфейса, чтобы изменить базовое поведение индексатора словаря, чтобы не выбрасывать KeyNotFoundException, если ключ не присутствовал в словаре (поскольку мне было удобно получить null в таком случае прямо встроенный).

Вот:

public interface INullValueDictionary<T, U>
    where U : class
{
    U this[T key] { get; }
}

public class NullValueDictionary<T, U> : Dictionary<T, U>, INullValueDictionary<T, U>
    where U : class
{
    U INullValueDictionary<T, U>.this[T key]
    {
        get
        {
            if (ContainsKey(key))
                return this[key];
            else
                return null;
        }
    }
}

Так как в реальном приложении у меня был список словарей, мне нужен был способ доступа к словарям из коллекции в качестве интерфейса. Я использовал простой индексатор int для доступа к каждому элементу списка.

var list = new List<NullValueDictionary<string, string>>();
int index = 0;
//...
list[index]["somekey"] = "somevalue";

Проще всего было сделать что-то вроде этого:

var idict = (INullValueDictionary<string, string>)list[index];
string value = idict["somekey"];

Вопрос возник, когда я решил попробовать использовать функцию ковариации, чтобы вместо этого использовать набор интерфейсов. Поэтому мне понадобился интерфейс с параметром ковариантного типа для работы приведения. Первое, что пришло мне в голову, это IEnumerable<T>, поэтому код будет выглядеть так:

IEnumerable<INullValueDictionary<string, string>> ilist = list;
string value = ilist.ElementAt(index)["somekey"];

Не очень приятно, кроме того, что ElementAt вместо индексатора намного хуже. Индексатор для List<T> определен в IList<T>, а T не является ковариантным.

Что мне было делать? Решил написать свой:

public interface IIndexedEnumerable<out T>
{
    T this[int index] { get; }
}

public class ExtendedList<T> : List<T>, IIndexedEnumerable<T>
{

}

Ну, несколько строчек кода (мне даже не нужно ничего писать в ExtendedList<T>), и работает так, как я хотел:

var elist = new ExtendedList<NullValueDictionary<string, string>>();
IIndexedEnumerable<INullValueDictionary<string, string>> ielist = elist;
int index = 0;
//...
elist[index]["somekey"] = "somevalue";
string value = ielist[index]["somekey"];

Наконец, вопрос: можно ли каким-то образом добиться этого ковариантного приведения без создания дополнительной коллекции?


person horgh    schedule 04.01.2013    source источник
comment
Почему использование ElementAt(index) хуже для вас? IEnumerable проверяет, реализована ли базовая коллекция ICollection - если это так, она использует свой индексатор для возврата элемента вместо перечислителя, поэтому с точки зрения эффективности это не должно быть такой большой проблемой?   -  person Marcin Deptuła    schedule 04.01.2013
comment
@Ravande Во-первых, это намного уродливее - в любом случае индексаторы - это в основном синтаксический сахар.   -  person SWeko    schedule 04.01.2013
comment
что не так с TryGetValue(key)?   -  person jgauffin    schedule 04.01.2013
comment
@Ravadre SWeko прав. Однако я был бы признателен, если бы вы могли дать мне ссылку, описывающую оптимизацию, о которой вы говорите.   -  person horgh    schedule 04.01.2013
comment
@SWeko, не могли бы вы добавить больше деталей, пожалуйста?   -  person horgh    schedule 04.01.2013
comment
один уточняющий вопрос - вы хотите ielist[index]["somekey"] а не elist[index]["somekey"] в последнем разделе?   -  person Ilya Ivanov    schedule 04.01.2013
comment
@IlyaIvanov определенно. Большое спасибо. Это была ужасная опечатка.   -  person horgh    schedule 04.01.2013
comment
Я немного обновил ответ. Я знаю, что это все еще не то, что вы хотели, но хоть какая-то дополнительная информация об отсутствии ограничений приведения совместимых типов не будет лишней. Привет и отличный вопрос   -  person Ilya Ivanov    schedule 04.01.2013
comment
@jgauffin прочитайте сообщение, на которое я ссылался в самом начале вопроса. В той теме я говорю, почему TryGetValue мне не удобно.   -  person horgh    schedule 04.01.2013
comment
@IlyaIvanov, ты ошибаешься, говоря это не то, что я хотел. Это!   -  person horgh    schedule 04.01.2013
comment
@KonstantinVasilcov редко когда получаешь такое удовольствие от слов "ты не прав" )   -  person Ilya Ivanov    schedule 04.01.2013
comment
@KonstantinVasilcov Я попытаюсь найти ссылку позже, но одно место, которое вы можете проверить и убедиться, что оно работает, - декомпилировать метод расширения System.Linq.Enumerable.ElementAt с помощью любого декомпилятора С#. Интересный код, который можно найти: IList<TSource> tSources = source as IList<TSource>; if (tSources != null) { return tSources[index]; } //... Rest of code (к тому же я был неправ, это не ICollection, а IList<T>, который должен быть реализован, что является более строгим   -  person Marcin Deptuła    schedule 04.01.2013
comment
@Ravadre спасибо за ваш ответ ... Я подумаю над всем этим через некоторое время   -  person horgh    schedule 04.01.2013


Ответы (1)


Вы можете попробовать использовать IReadOnlyList<T>, который реализован List<T>.

Обратите внимание, что я добавил один экземпляр NullValueDictionary<string, string> в List, чтобы вы не получили ArgumentOutOfRangeException в строке elist[index].

IReadOnlyList<NullValueDictionary<string, string>> elist = new List<NullValueDictionary<string, string>>
                                                                    { 
                                                                        new NullValueDictionary<string, string>() 
                                                                    };
IReadOnlyList<INullValueDictionary<string, string>> ielist = elist;

int index = 0;
//...
elist[index]["somekey"] = "somevalue";
string value = elist[index]["somekey"];

Изменить. Я искал ковариантные интерфейсы и коллекции с индексами до .NET 4.5, но ничего не нашел. Тем не менее, я думаю, что есть несколько более простое решение, чем создавать отдельный интерфейс - просто приводить одну коллекцию к другой.

List<INullValueDictionary<string, string>> ielist = elist.Cast<INullValueDictionary<string, string>>().ToList();

Или используйте ковариацию, полученную из массивов

INullValueDictionary<string, string>[] ielist = elist.ToArray()

LINQ имеет некоторую оптимизацию, которая работает с полной совместимостью типов, поэтому вы не будете перебирать последовательность, если эти типы совместимы.

Реализация Cast взята из MONO Linq< /а>

public static IEnumerable<TResult> Cast<TResult> (this IEnumerable source)
{
    var actual = source as IEnumerable<TResult>;
    if (actual != null)
        return actual;

    return CreateCastIterator<TResult> (source);
}

Обратите внимание, что я изменил интерфейс INullValueDictionary<T, U>, добавив в свойство set, чтобы ielist[index]["somekey"] = "somevalue"; работало.

public interface INullValueDictionary<T, U> where U : class
{
    U this[T key] { get; set; }
}

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

В поисках ковариации в mscorlib

Возможно, вам это будет неинтересно, но я просто хотел узнать, какие типы являются ковариантными в сборке mscorlib. Запустив следующий скрипт, я получил только 17 ковариантных типов, 9 из которых Funcs. Я пропустил реализацию IsCovariant, потому что этот ответ слишком длинный даже без него

typeof(int).Assembly.GetTypes()
                    .Where(type => type.IsGenericType)
                    .Where(type=>type.GetGenericArguments().Any(IsCovariant))
                    .Select(type => type.Name)
                    .Dump();

//Converter`2 
//IEnumerator`1 
//IEnumerable`1 
//IReadOnlyCollection`1 
//IReadOnlyList`1 
//IObservable`1 
//Indexer_Get_Delegate`1 
//GetEnumerator_Delegate`1 
person Ilya Ivanov    schedule 04.01.2013
comment
+1: это действительно подойдет, но доступно с .Net Framework 4.5, а я ограничен .Net Framework 4. - person horgh; 04.01.2013
comment
Моя ошибка заключалась в том, что я полностью забыл о простом приведении типов, думая в терминах ко\противоречивости. Это 100% то, что мне было нужно. - person horgh; 04.01.2013
comment
Отлично, что это решило вашу проблему. Как видите, я начал углубляться в этот вопрос ) - person Ilya Ivanov; 04.01.2013
comment
В статье Ковариантность и контравариантность MSDN говорится: ковариантность и контравариантность позволяют использовать неявные преобразование ссылок для типов массивов, типов делегатов и аргументов универсального типа. Я думаю, вот где подсказка - person horgh; 04.01.2013
comment
Да, но вам все равно нужно позвонить ToArray(). Это действительно становится проще INullValueDictionary<string, string>[] ielist = elist.ToArray() - person Ilya Ivanov; 04.01.2013
comment
Ну да. Однако из-за того, что я не вижу здесь какой-либо встроенной коллекции для использования конвариантности, я думаю, что приведение достаточно подходит для использования вместо дополнительного интерфейса и класса. Ну по крайней мере в моих нынешних условиях не хуже - person horgh; 04.01.2013