Методы расширения для IDictionary и IReadOnlyDictionary

Мой вопрос похож на предыдущий, но ответ на него вопрос к этому не относится.

Итак, я хочу написать метод расширения для интерфейсов IDictionary и IReadOnlyDictionary:

public static TValue? GetNullable<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary, TKey key)
    where TValue : struct
{
    return dictionary.ContainsKey(key)
        ? (TValue?)dictionary[key]
        : null;
}

public static TValue? GetNullable<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
    where TValue : struct
{
    return dictionary.ContainsKey(key)
        ? (TValue?)dictionary[key]
        : null;
}

Но когда я использую его с классами, реализующими оба интерфейса (например, Dictionary<Tkey, TValue>), я получаю «неоднозначный вызов». Я не хочу вводить var value = myDic.GetNullable<IReadOnlyDictionary<MyKeyType, MyValueType>>(key), я хочу, чтобы это было просто var value = myDic.GetNullable(key).

Это возможно?


person user1067514    schedule 05.09.2013    source источник
comment
Почему вы не сохраняете только версию IDictionary?   -  person mircea    schedule 05.09.2013
comment
@mircea: к сожалению, IReadOnlyDictionary<TKey, TValue> не наследуется от IDictionary<TKey, TValue>. Поэтому, если OP хочет использовать один и тот же метод расширения для обоих типов объектов, ему нужно написать метод, используя тип, от которого наследуются оба объекта, в данном случае IEnumerable<KeyValuePair<TKey, TValue>>.   -  person fourpastmidnight    schedule 05.09.2013
comment
Потому что некоторые классы могут реализовать только IDictionry.   -  person user1067514    schedule 05.09.2013


Ответы (3)


Вам нужен только один метод расширения. Переопределите его следующим образом:

public static TValue? GetNullableKey<TKey, TValue>(this IEnumerable<KeyValuePair<TKey, TValue>> dictionary, TKey, key) where TValue : struct
{
    // Your code here.
    // Per @nmclean: to reap performance benefits of dictionary lookup, try to cast
    //               dictionary to the expected dictionary type, e.g. IDictionary<K, V> or
    //               IReadOnlyDictionary<K, V>. Thanks for that nmclean!
}

И IDictionary<TKey, TValue>, и IReadOnlyDictionary<TKey, TValue> наследуются от IEnumerable<KeyValuePair<TKey, TValue>>.

ХТН.

ОБНОВЛЕНИЕ: чтобы ответить на ваш вопрос в комментариях

Если вам не нужно вызывать методы, которые появляются только в одном или другом интерфейсе (т. е. вы вызываете только методы, которые существуют в IEnumerable<KeyValuePair<TKey, TValue>>), то нет, вам не нужен этот код.

Если вам нужно вызывать методы в интерфейсе IDictionary<TKey, TValue> или IReadOnlyDictionary<TKey, TValue>, которых нет в общем базовом интерфейсе, тогда да, вам нужно будет увидеть, является ли ваш объект одним или другим, чтобы узнать, какие методы допустимы. называется.

ОБНОВЛЕНИЕ: возможно (скорее всего) вам не следует использовать это решение

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

Во-первых, весь смысл словаря/карты заключается в поиске O (1) (постоянное время). Используя приведенное выше решение, вы превратили операцию O(1) в операцию O(n) (линейное время). Если в вашем словаре 1 000 000 элементов, поиск значения ключа занимает в 1 миллион раз больше времени (если вам действительно не повезло). Это может существенно повлиять на производительность.

А маленький словарь? Что ж, тогда возникает вопрос, действительно ли вам нужен словарь? Не лучше ли со списком? Или, возможно, еще лучший вопрос: когда вы начинаете замечать влияние на производительность использования поиска O(n) вместо O(1) и как часто вы ожидаете выполнять такой поиск?

Наконец, ситуация OP с объектом, который реализует оба IReadOnlyDictionary<TKey, TValue> и IDictionary<TKey, TValue>, довольно странная, поскольку поведение IDictionary<TKey, TValue> является надмножеством поведения IReadOnlyDictionary<TKey, TValue>. И стандартный класс Dictionary<TKey, TValue> уже реализует оба этих интерфейса. Поэтому, если бы тип OP (я предполагаю, специализированный) вместо этого унаследовал от Dictionary<TKey, TValue>, то, когда требовалась функциональность только для чтения, тип можно было бы просто преобразовать в IReadOnlyDictionary<TKey, TValue>, возможно, вообще избегая всей этой проблемы. Даже если проблема не была неизбежной (например, где-то делается приведение к одному из интерфейсов), все равно было бы лучше реализовать два метода расширения, по одному для каждого из интерфейсов.

Я думаю, что прежде чем я закончу эту тему, необходимо сделать еще один пункт. Преобразование Dictionary<TKey, TValue> в IReadOnlyDictionary<TKey, TValue> гарантирует только то, что все, что получает преобразованное значение, само не сможет изменить базовую коллекцию. Однако это не означает, что другие ссылки на экземпляр словаря, из которого была создана приведенная ссылка, не изменят базовую коллекцию. Это одна из проблем, связанных с интерфейсами коллекций IReadOnly*. Коллекции, на которые ссылаются эти интерфейсы, не могут быть действительно коллекциями «только для чтения» (или, как часто подразумевается, неизменяемыми), они просто предотвращают изменение коллекции конкретной ссылкой (несмотря на фактический экземпляр конкретного класса ReadOnly*). Это одна из причин (среди прочих), что было создано пространство имен System.Collections.Immutable классов коллекций. Коллекции в этом пространстве имен представляют собой действительно неизменяемые коллекции. «Мутации» этих коллекций приводят к тому, что возвращается совершенно новая коллекция, состоящая из измененного состояния, оставляя исходную коллекцию неизменной.

person fourpastmidnight    schedule 05.09.2013
comment
Так должен ли «ваш код здесь» выглядеть как if(dictionary is IReadonlyDictionary) ... else if(dictionary is IDictionary) ... else throw(new NotDictionaryException())? - person user1067514; 05.09.2013
comment
@user1067514 user1067514 У вас определенно может быть код, который специально работает для IDictionary<K,V> и IReadOnlyDictionary<K,V>, но нет причин не реализовывать что-то, что работает, если это просто IEnumerable<KeyValuePair<K,V>> - person juharr; 05.09.2013
comment
@user1067514 user1067514 Как говорит juharr, вы можете реализовать его исключительно на основе IEnumerable<KeyValuePair>, но вам нужно будет перечислить все ключи, лишив словари преимущества в производительности. Было бы лучше сначала попытаться выполнить приведение к IReadOnlyDictionary и IDictionary, чтобы вы могли использовать их методы ContainsKey, а затем вернуться к IEnumerable и Where, если это не сработало. - person nmclean; 05.09.2013
comment
Почему отрицательный голос? Это не был вопрос о производительности (если это то, что было отрицательным голосом за того, кто проголосовал против, - и в целом ответ, каким он был изначально, был правильным сам по себе. - person fourpastmidnight; 05.09.2013
comment
@fourpastmidnight Ну, отрицательный голос был не моим - на самом деле я думаю, что он уже был на 4 балла, прежде чем я прокомментировал производительность. - person nmclean; 05.09.2013
comment
Нет проблем @nmclean, ваш комментарий, тем не менее, был на высоте! Еще раз спасибо! - person fourpastmidnight; 05.09.2013
comment
Но повышать приведение не обязательно, чтобы сделать что-то подобное! - person binki; 25.01.2016
comment
@binki См. комментарий @nmclean выше о влиянии на производительность. Весь смысл словарей в поиске O(1). Но, преобразовав в IEnumerable<KeyValuePair<TKey, TValue>>, мы просто сделали поиск ключа операцией O(n) — очень дорогой по сравнению с O(1). - person fourpastmidnight; 26.01.2016
comment
@fourpastmidnight Но вам не нужно использовать приведение во время выполнения. Разве система типов не должна помогать вам, а не бороться с вами? Кроме того, идея иметь возможность вернуться к «сканированию таблицы» в случае сбоя при восходящем преобразовании путем перечисления списка KeyValuePair довольно крутая, но потребует от вас написания кода, который выполняет одно и то же несколько раз. Если вам интересно увидеть альтернативу восходящему преобразованию, см. мой новый ответ, где я адаптирую IDictionary<TKey, TValue> как IReadOnlyDictionary<TKey, TValue>. - person binki; 26.01.2016
comment
@binki Я вижу, к чему вы клоните - с тремя методами расширения. Да, это тоже правильно. ОП хотел один метод, но он не всегда правильный ответ;). Спасибо, что поделились своим ответом!! - person fourpastmidnight; 27.01.2016
comment
Я полностью за приведение к IDictionary<TKey, TValue>/IReadOnlyDictionary<TKey, TValue>, но написать собственный код поиска для IEnumerable<KeyValuePair<TKey, TValue>> может быть сложно. Некоторые реализации имеют дополнительные детали поиска, например, индексатор System.Runtime.Caching.MemoryCache проверяет записи с истекшим сроком действия. В этом конкретном случае перечислитель также отфильтровывает записи с истекшим сроком действия, поэтому он должен работать правильно. Но могут быть ситуации, когда общий поиск не может учесть такие детали. Является ли это ошибкой в ​​​​этой конкретной реализации или вашем коде, подлежит обсуждению. - person Niels van der Rest; 02.05.2016
comment
@NielsvanderRest Да, вы, безусловно, правы. Я просто отвечал на вопрос ОП об одном способе сделать это для типов IDictionary<K,V> и IReadonlyDictionary<K,V>. Я уже отмечал, что, хотя то, о чем просил ОП, можно сделать, это не идеально. YMMV, в зависимости от того, чего вы пытаетесь достичь, и ваших ограничений производительности. Поскольку мое решение превращает операцию O (1) в O (n) !! Не то, что вы хотите сделать легкомысленно, иначе зачем вообще использовать словарь? Просто используйте список! - person fourpastmidnight; 14.02.2018

После некоторых экспериментов я вспомнил/обнаружил, что C# готов разрешать неоднозначности, предпочитая подклассы базовым классам. Таким образом, чтобы сделать это с безопасностью типов, вам нужны 3 метода расширения, а не один. И если у вас есть другие классы, которые, как Dictionary<TKey, TValue>, реализуют и IDictionary<TKey, TValue>, и IReadOnlyDictionary<TKey, TValue>, вам нужно писать все больше и больше методов расширения…

Следующее делает компилятор совершенно счастливым. Не требуется приведения, чтобы использовать переданный словарь в качестве словаря. Моя стратегия состоит в том, чтобы реализовать мой фактический код для наименьшего общего знаменателя, IReadOnlyDictionary<TKey, TValue>, использовать фасад для адаптации IDictionary<TKey, TValue> к этому (потому что вам может быть передан объект, который не реализует IReadOnlyDictionary<TKey, TValue>), и, чтобы избежать ошибки неоднозначности разрешения перегрузки метода , явно расширить Dictionary<TKey, TValue> напрямую.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;

// Proof that this makes the compiler happy.
class Program
{
    static void Main(string[] args)
    {
        // This is written to try to demonstrate an alternative,
        // type-clean way of handling http://stackoverflow.com/q/18641693/429091
        var x = "blah".ToDictionary(
            c => c,
            c => (int)c);
        // This would be where the ambiguity error would be thrown,
        // but since we explicitly extend Dictionary<TKey, TValue> dirctly,
        // we can write this with no issue!
        x.WriteTable(Console.Out, "a");
        IDictionary<char, int> y = x;
        y.WriteTable(Console.Out, "b");
        IReadOnlyDictionary<char, int> z = x;
        z.WriteTable(Console.Out, "lah");
    }
}

// But getting compile-time type safety requires so much code duplication!
static class DictionaryExtensions
{
    // Actual implementation against lowest common denominator
    public static void WriteTable<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dict, TextWriter writer, IEnumerable<TKey> keys)
    {
        writer.WriteLine("-");
        foreach (var key in keys)
            // Access values by key lookup to prove that we’re interested
            // in the concept of an actual dictionary/map/lookup rather
            // than being content with iterating over everything.
            writer.WriteLine($"{key}:{dict[key]}");
    }

    // Use façade/adapter if provided IDictionary<TKey, TValue>
    public static void WriteTable<TKey, TValue>(this IDictionary<TKey, TValue> dict, TextWriter writer, IEnumerable<TKey> keys)
        => new ReadOnlyDictionary<TKey, TValue>(dict).StaticCast<IReadOnlyDictionary<TKey, TValue>>().WriteTable(writer, keys);

    // Use an interface cast (a static known-safe cast).
    public static void WriteTable<TKey, TValue>(this Dictionary<TKey, TValue> dict, TextWriter writer, IEnumerable<TKey> keys)
        => dict.StaticCast<IReadOnlyDictionary<TKey, TValue>>().WriteTable(writer, keys);

    // Require known compiletime-enforced static cast http://stackoverflow.com/q/3894378/429091
    public static T StaticCast<T>(this T o) => o;
}

Самое неприятное в этом подходе то, что вам нужно написать так много шаблонов — два метода расширения тонкой оболочки и одну реальную реализацию. Однако я думаю, что это может быть оправдано, потому что код, использующий расширения, может быть простым и понятным. Уродство содержится в классе расширений. Кроме того, поскольку реализация расширения содержится в одном методе, вам не нужно беспокоиться об обновлении перегрузок при обновлении варианта IReadOnlyDictionary<TKey, TValue>. Компилятор даже напомнит вам обновить перегрузки, если вы измените типы параметров, если вы не добавляете новые параметры со значениями по умолчанию.

person binki    schedule 25.01.2016
comment
Я согласен, самая раздражающая часть — это шаблонный код. Кроме этого, он должен работать так, как ожидалось, и его можно использовать так, как можно было бы ожидать от такого метода. Вы получаете голос от меня :) - person fourpastmidnight; 28.01.2016

Чтобы получить желаемое поведение в вашем случае, см. fourpastmidnight answer.


Однако, чтобы ответить на ваш вопрос напрямую, нет, невозможно заставить работать синтаксис myDic.GetNullable, когда есть два метода расширения. Сообщение об ошибке, которое вы получили, было правильным, говоря, что оно неоднозначно, потому что у него нет возможности узнать, какой из них вызывать. Если ваш метод IReadOnlyDictionary сделал что-то другое, должен он делать это или нет для Dictionary<TKey, TValue>?

Именно по этой причине существует метод AsEnumerable. Методы расширения определены как для IEnumerable, так и для IQueryable с одинаковыми именами, поэтому иногда вам нужно привести объекты, реализующие оба, чтобы гарантировать, что будет вызван конкретный метод расширения.

Предположим, что у ваших двух методов действительно разные реализации, вы можете включить похожие методы, например:

public static IReadOnlyDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(this IDictionary<TKey, TValue> dictionary) {
    return (IReadOnlyDictionary<TKey, TValue>)dictionary;
}

public static IDictionary<TKey, TValue> AsReadWrite<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dictionary) {
    return (IDictionary<TKey, TValue>)dictionary;
}

Затем вызовите методы следующим образом:

dictionary.AsReadOnly().GetNullable(key);
dictionary.AsReadWrite().GetNullable(key);

(Технически метод с именем AsReadOnly должен возвращать настоящий ReadOnlyDictionary, но это просто для демонстрации)

person nmclean    schedule 05.09.2013
comment
Не для этого существуют методы AsEnumerable и AsQueryable. IQueryable<T> расширяет IEnumerable<T>. Невозможно иметь IQueryable, который не является IEnumearable. В этом случае это не двусмысленно из-за цепочки наследования. Более производный тип имеет приоритет, поэтому объект IQueryable использует метод IQueryable. Если вы хотите использовать метод IEnumerable, несмотря на то, что объект реализует IQueryable, вам нужно использовать AsEnumerable. Другое использование связано с тем, что методы экземпляра имеют приоритет над расширениями; если есть метод экземпляра с именем Select... - person Servy; 05.09.2013
comment
@Servy Верно, IQueryable разработчики являются всегда IEnumerable разработчиками, но это не решает проблему. Если вы присваиваете IQueryable полю, которое объявлено как IEnumerable, вы должны привести его для вызова IQueryable методов расширения. Если бы это были классы, это было бы решено с помощью полиморфизма, но с методами расширения интерфейса возникает неоднозначность, которая должна быть явно разрешена путем приведения типов. - person nmclean; 05.09.2013
comment
Я не говорю, что эти методы бесполезны, я говорю, что использование, которое вы упомянули в этом ответе, недопустимо. Кроме того, случай, который вы только что упомянули в комментариях, на самом деле не предназначен для использования этих методов. Они созданы больше для повышения, чем для понижения. - person Servy; 05.09.2013
comment
@Servy На самом деле, вы правы в том, что AsQueryable не предназначен для понижения. Я предположил, что он дополняет AsEnumerable, но на самом деле он предназначен для создания IQueryable, который вызывает IEnumerable методы расширения. Тем не менее, это точно предполагаемое использование AsEnumerable, поэтому я не знаю, почему вы смешиваете его. - person nmclean; 05.09.2013
comment
Обратите внимание, что любой из этих AsReadOnly или AsReadWrite вызовет ошибку приведения во время выполнения, если будет передано что-то, что не реализует оба интерфейса. Кроме того, вы форсируете динамическое приведение. Почему бы просто не сделать так, чтобы обе эти функции принимали Dictionary<TKey, TValue> вместо противоположного интерфейса? По крайней мере, это можно проверить на работу во время компиляции. - person binki; 27.01.2016
comment
Кроме того, что касается вашего комментария о ReadOnlyDictionary, интерфейс IReadOnlyDictionary<TKey, TValue> не подразумевает, что базовый словарь является неизменным. - person binki; 27.01.2016
comment
@binki 1) На самом деле я должен был заставить их каждый принимать свой собственный тип возвращаемого значения, точно так же, как AsEnumerable разработан... Я не уверен, о чем я думал, когда его перевернули. 2) Я не имел в виду, что IReadOnlyDictionary<TKey, TValue> должен быть доступен только для чтения, просто согласно соглашению об именах я думаю, что метод AsReadOnly должен возвращать неизменное представление, например List‹T›.AsReadOnly. - person nmclean; 28.01.2016