实例化IDictionary<K, IEnumerable<V>>

4
我有一个方法,签名如下:
public void Foo(IDictionary<string, IEnumerable<string>> data)
{
}

并希望传递

private Dictionary<string, HashSet<string>> myInput = 
    new Dictionary<string, HashSet<string>>()
{
};      

这是一行文本。

Foo(myInput);

产生编译错误:
参数1:无法将“System.Collections.Generic.Dictionary>”转换为“System.Collections.Generic.IDictionary>”
Dictionary实现IDictionary,HashSet实现IEnumerable。
为什么编译器无法执行转换?
我如何创建一个数据实例,以便将其传递给Foo?
注意:
如果我更改签名为
public void Foo(IDictionary<string, HashSet<string>> data)

编译成功。然而,我不需要知道传递的是像HashSet<T>这样的具体类型。任何IEnumerable<T>都可以。
更新:
以下代码可以编译:
public void Foo(IDictionary<string, IEnumerable<string>> data)
{
    List<string> item = new List<string>() { "foo", "bar", "baz" };
    data.Add("key", item);
    HashSet<string> item2 = new HashSet<string>() { "quu" };
    data.Add("key2", item2);
}

很明显,data可以接受各种类型的值,只要它们都实现了IEnumerable<T>


泛型中的协变性和逆变性 - poke
@poke:我以前读过这个,但不明白它在这种情况下如何适用。答案可能就潜伏在那里(问题标记为协变性)。 - Eric J.
这里的问题出在 IDictionary<TKey, TValue> 接口上。它的类型参数都不是协变的,因此只能分配完全匹配该类型的内容。没有其他选择。 - Jesse C. Slicer
3个回答

4
这不起作用的原因是由于泛型中协变和逆变的限制。最好的方法是通过一个例子来解释为什么这个操作必须失败
假设Foo有一个有效的签名,类型系统承诺以下操作可以正常工作:
var myInput = new Dictionary<string, HashSet<string>>();

// assuming a valid signature for `Foo`
Foo(myInput);

// according to the type of `myInput` the following MUST work
HashSet<string> item = myInput["foo"];
item.Add("baz");

这是绝对需要工作的。因此,Foo 所做的任何事情都必须确保这仍然有效。

现在暂时忽略上面的内容,让我们假设以下Foo有效实现:

public void Foo (IDictionary<string, IEnumerable<string>> data)
{
    List<string> item = new List<string>(){ "foo", "bar" };
    data.Add("foo", item);
}

因为 List<string> 实现了 IEnumerable<string>,所以将列表对象添加到存储 IEnumerable<string> 的字典中是完全可行的。再次强调:上面是 Foo 签名的有效实现。
然而,如果我们现在将两个代码段组合起来,它就会崩溃:存储在键 "foo" 处的对象不是 HashSet<string> 而是 List<string>。因此,在 HashSet<string> item = myInput["foo"] 处的赋值将失败。但这里存在冲突!类型系统应确保赋值无论发生什么都能正常工作;但是,Foo 的实现对其签名也完全有效。
因此,与其在这里制定一些武断和不透明的规则,不如禁止这样做。类型系统只是防止使用不兼容的参数调用 Foo。因为 IDictionary 是不变的,所以不能绕过此限制。

好的,第二部分 - 我怎样可以实例化数据以便传递给 Foo?或者那基本上是不可能的? - Eric J.
看一下我的修改。事实证明,您可以将List<string>HashSet<string>都添加到IDictionary<string, IEnumerable<string>> data中。 - Eric J.
但是类型系统保证我它可以工作。如果我有一个存储哈希集的字典,并且字典发生了任何变化(在Foo内部),那么我仍然可以从中获取类型为HashSet的项。这与Foo内部发生的事情无关。 - poke
为什么类型系统承诺可以将 IEnumerable<T> 分配给 HashSet<T>?这似乎与您的论点不符。 - Eric J.
这并不是问题所在。类型系统保证了对于 Dictionary<S, HashSet<T>>,当我从中获取一个项目时,该项目的类型为 HashSet<T>。因此,如果我将一个 IEnumerable<T> 添加到字典中,那么这个承诺就无法得到保证,这就是整个过程失败的原因。 - poke
显示剩余3条评论

1

@poke的答案已经解释了为什么,所以我不会花太多时间在这方面。

您可以通过使用带有where限制的通用方法来管理此限制,例如:

public void Foo<T>(IDictionary<string, T> data)
    where T : IEnumerable<string>
{
}

现在你的Foo方法接受任何实现IEnumerable<string>接口的对象类型,无论可枚举对象的实际类型如何。你可以对字典做任何想做的事情,除了添加具有内容的项。
但是假设你确实想要添加一些项:
public void Foo<T>(IDictionary<string, T> data)
    where T : ICollection<string>, new()
{
    var col = new T();
    col.Add("bar");

    data["col"] = col;
}

这里是一个快速测试:

var a = new Dictionary<string, HashSet<string>>();
var b = new Dictionary<string, List<string>>();
var c = new Dictionary<string, LinkedList<string>>();

Foo(a);
Foo(b);
Foo(c);

编译和运行都没有问题。所有三个字典最终都包含了适当实体类型的新集合,其中包含字符串bar

您不能将错误类型的集合添加到Dictionary中。无论您尝试对该对象执行什么操作,都必须与该对象的实体类型一致。这就是为什么在这种情况下Foo方法需要是通用的原因。


当然回答问题的第二部分。 - Eric J.

0
根据我的评论,以下是使用LINQ帮助使您的调用工作的方法:
Foo(myInput.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.AsEnumerable()));

2
虽然这样可以让你使用适当的格式调用 FoomyInput 的内容,但是 Foo 所期望的副作用现在将会在临时的 Dictionary<string, IEnumerable<string>> 上操作,而不是在 myInput 本身上操作。 - Corey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接