如何解决 IReadOnlyDictionary 缺乏协变性的问题?

34
我正在尝试暴露一个只读字典,其中包含具有只读接口的对象。在内部,字典是可写的,其中的对象也是可写的(请参见下面的示例代码)。我的问题是,IReadOnlyDictionary不支持协变转换,因为在此处概述的原因。这意味着我不能将我的内部字典仅作为只读字典公开。
所以我的问题是,是否有一种有效的方法可以将我的内部字典转换为IReadOnlyDictionary,或者其他处理此问题的方法?我能想到的选项是:
  1. 持有两个内部字典并保持它们同步。
  2. 当访问属性时创建一个新的字典并强制转换其中的所有对象。
  3. 在内部使用时将IReadOnly的转换回NotReadOnly。
1似乎很麻烦,2似乎效率低下。3目前听起来最有前途,但仍然很丑陋。我还有其他选择吗?
public class ExposesReadOnly
{
    private Dictionary<int, NotReadOnly> InternalDict { get; set; }
    public IReadOnlyDictionary<int, IReadOnly> PublicList
    {
        get
        {
            // This doesn't work...
            return this.InternalDict;
        }
    }

    // This class can be modified internally, but I don't want
    // to expose this functionality.
    private class NotReadOnly : IReadOnly
    {
        public string Name { get; set; }
    }
}

public interface IReadOnly
{
    string Name { get; }
}
5个回答

25

你可以编写自己的只读字典包装器,例如:

public class ReadOnlyDictionaryWrapper<TKey, TValue, TReadOnlyValue> : IReadOnlyDictionary<TKey, TReadOnlyValue> where TValue : TReadOnlyValue
{
    private IDictionary<TKey, TValue> _dictionary;

    public ReadOnlyDictionaryWrapper(IDictionary<TKey, TValue> dictionary)
    {
        if (dictionary == null) throw new ArgumentNullException("dictionary");
        _dictionary = dictionary;
    }
    public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); }

    public IEnumerable<TKey> Keys { get { return _dictionary.Keys; } }

    public bool TryGetValue(TKey key, out TReadOnlyValue value)
    {
        TValue v;
        var result = _dictionary.TryGetValue(key, out v);
        value = v;
        return result;
    }

    public IEnumerable<TReadOnlyValue> Values { get { return _dictionary.Values.Cast<TReadOnlyValue>(); } }

    public TReadOnlyValue this[TKey key] { get { return _dictionary[key]; } }

    public int Count { get { return _dictionary.Count; } }

    public IEnumerator<KeyValuePair<TKey, TReadOnlyValue>> GetEnumerator()
    {
        return _dictionary
                    .Select(x => new KeyValuePair<TKey, TReadOnlyValue>(x.Key, x.Value))
                    .GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

这更像是一个只读视图的字典。字典本身仍然可以改变。 - vidstige
1
@vidstige,“字典本身仍然可以更改”- 当然,这就是为什么它是私有的原因。 - Joe
3
可以通过在构造函数中从传入的字典创建新的字典来轻松解决这个问题。 - danze
2
+1 是因为你的类在创建时不会读取整个源字典。这意味着即使对于巨大的字典,它也会非常快速。 - Daniel Wolf
1
@vidstige 所有只读包装器都允许您修改源代码,包括 .Net 中包含的那些,因为一个类无法防止对随机给定对象的修改。唯一的解决方法是制作一个防御性副本,但这会破坏使用包装器而不仅仅是复制数据的目的。重要的是,此包装器不允许消费者修改源代码。如果制作包装器的类不接触源代码,则没有任何东西可以修改。 - relatively_random
显示剩余3条评论

2
我建议您定义自己的协变接口,并包括协变访问方法以及一个能创建只读包装对象的方法,该对象实现所需类型的IDictionaryIReadonlyDictionary。在接口中可以忽略IEnumerable<KeyValuePair<TKey,TValue>>
根据您的具体情况,定义一个IFetchByKey<out TValue>可能会有帮助,该接口被IFetchByKey<in TKey, out TValue>继承。前者接受任何类型对象的查询(给定一个对象实例,集合应该能够说它是否包含该实例,即使它是类型或;集合不包含后两种类型的实例,也应该能够说明这一点)。

0
根据您的使用情况,您可能可以通过公开一个Func<int,IReadOnly>来解决问题。
public class ExposesReadOnly
{
    private Dictionary<int, NotReadOnly> InternalDict { get; set; }
    public Func<int,IReadOnly> PublicDictionaryAccess
    {
        get
        {
            return (x)=>this.InternalDict[x];
        }
    }

    // This class can be modified internally, but I don't want
    // to expose this functionality.
    private class NotReadOnly : IReadOnly
    {
        public string Name { get; set; }
    }
}

public interface IReadOnly
{
    string Name { get; }
}

0

也许这个解决方案适合你:

public class ExposesReadOnly
{
    private IDictionary<int, IReadOnly> InternalDict { get; set; }
    public IReadOnlyDictionary<int, IReadOnly> PublicList
    {
        get
        {
            IReadOnlyDictionary<int, IReadOnly> dictionary = new ReadOnlyDictionary<int, IReadOnly>(InternalDict);

            return dictionary;
        }
    }

    private class NotReadOnly : IReadOnly
    {
        public string Name { get; set; }
    }

    public void AddSomeValue()
    {
        InternalDict = new Dictionary<int, NotReadOnly>();
        InternalDict.Add(1, new NotReadOnly() { Name = "SomeValue" });
    }
}

public interface IReadOnly
{
    string Name { get; }
}

class Program
{
    static void Main(string[] args)
    {
        ExposesReadOnly exposesReadOnly = new ExposesReadOnly();
        exposesReadOnly.AddSomeValue();

        Console.WriteLine(exposesReadOnly.PublicList[1].Name);
        Console.ReadLine();

        exposesReadOnly.PublicList[1].Name = "This is not possible!";
    }
}

希望这能帮到你!

问候


这适用于添加值,但我还需要在内部访问它们并对其进行更新。这就是我列出的选项#3所在的位置,因为当我在内部检索它们时,我必须将它们强制转换回NotReadOnly。 - Ocelot20
通过我提供的解决方案,您可以从类内完全访问、更新、删除、添加等字典中的任何值,并仅将其作为只读字典向外部公开。请给出一个具体的例子,说明这段代码可能不足以满足您的需求,以便我更清楚地理解您的问题。 - Thomas Mondel
没有进行强制转换,InternalDict[1].Name = "SomeNewValue" 无法正常工作。 - Ocelot20

-1

针对特定的协变性缺失,另一种方法是:

在 IDictionary 上实现有用的协变性的一种解决方法。

public static class DictionaryExtensions
    {
        public static IReadOnlyDictionary<TKey, IEnumerable<TValue>> ToReadOnlyDictionary<TKey, TValue>(
            this IDictionary<TKey, List<TValue>> toWrap)
        {
            var intermediate = toWrap.ToDictionary(a => a.Key, a =>a.Value!=null? a.Value.ToArray().AsEnumerable():null);
            var wrapper = new ReadOnlyDictionary<TKey, IEnumerable<TValue>>(intermediate);
            return wrapper;
        }   
    }

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