IDictionary 和 IReadOnlyDictionary 的扩展方法

本文关键字:扩展 方法 IReadOnlyDictionary IDictionary | 更新日期: 2023-09-27 18:30:15

我的问题与上一个问题相似,但该问题的答案不适用于这个问题。

好吧,我想为 IDictionaryIReadOnlyDictionary 接口编写一个扩展方法:

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)

这可能吗?

IDictionary 和 IReadOnlyDictionary 的扩展方法

您只需要一种扩展方法。按如下方式重新定义它:

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>接口上调用方法,那么是的,您需要查看您的对象是一个还是另一个,以了解哪些方法有效调用。

更新:您可能(很可能)不应该使用此解决方案

因此,从技术上讲,如前所述,此解决方案在回答OP的问题时是正确的。然而,这真的不是一个好主意,正如其他人在下面评论的那样,正如我承认这些评论是正确的。

首先,字典/地图的全部意义在于O(1)(常量时间)查找。通过使用上面的解决方案,您现在已经将 O(1) 操作转换为 O(n)(线性时间)操作。如果您的字典中有 1,000,000 个项目,则查找键值需要长达 100 万倍的时间(如果您真的很倒霉的话)。这可能会对性能产生重大影响。

一本小字典呢?那么问题来了,你真的需要字典吗?你最好有一个清单吗?或者也许是一个更好的问题:您从哪里开始注意到使用 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 命名空间的原因之一(除其他外)。此命名空间中的集合表示真正不可变的集合。这些集合的"突变"会导致返回由突变状态组成的全新集合,使原始集合不受影响。

经过一番实验,我想起/发现 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> 变体时更新重载。编译器甚至会提醒您在更改参数类型时更新重载,除非您使用默认值添加新参数。

要在您的案例中获得所需的行为,请参阅四点午夜的答案。


但是,要直接回答您的问题,不,当有两种扩展方法时,无法使myDic.GetNullable语法起作用。您收到的错误消息说它模棱两可是正确的,因为它无法知道要调用哪个。如果你的IReadOnlyDictionary方法碰巧做了一些不同的事情,它应该这样做还是不这样做Dictionary<TKey, TValue>

正是由于这个原因,AsEnumerable 方法才存在。扩展方法是为具有相同名称的IEnumerableIQueryable定义的,因此有时需要强制转换实现两者的对象,以确保将调用特定的扩展方法。

假设您的两个方法确实有不同的实现,您可以包含类似的方法,如下所示:

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,但这仅用于演示)