如何在通过键访问字典值时获得 null 而不是 KeyNotFoundException?

114
在某些特定情况下,当访问字典中不存在的键时,我发现有一种简洁易读的方式来获取null值而不是抛出KeyNotFoundException异常对我很有用。我的第一个想法是使用扩展方法:
public static U GetValueByKeyOrNull<T, U>(this Dictionary<T, U> dict, T key)
        where U : class //it's acceptable for me to have this constraint
{
    if (dict.ContainsKey(key))
        return dict[key];
    else 
        //it could be default(U) to use without U class constraint
        //however, I didn't need this.
        return null; 
}

实际上这并不是一个很简短的表述,当你写出像这样的东西时:

string.Format("{0}:{1};{2}:{3}",                                                 
              dict.GetValueByKeyOrNull("key1"),
              dict.GetValueByKeyOrNull("key2"),
              dict.GetValueByKeyOrNull("key3"),
              dict.GetValueByKeyOrNull("key4"));

我认为,最好有一种接近基本语法的方式: dict["key4"].

然后,我想到了一个使用私有字典字段的类,它可以暴露我需要的功能:

public class MyDictionary<T, U> //here I may add any of interfaces, implemented
                                //by dictionary itself to get an opportunity to,
                                //say, use foreach, etc. and implement them
                                // using the dictionary field.
        where U : class
{
    private Dictionary<T, U> dict;

    public MyDictionary()
    {
        dict = new Dictionary<T, U>();
    }

    public U this[T key]
    {
        get
        {
            if (dict.ContainsKey(key))
                return dict[key];
            else
                return null;
        }
        set
        {
            dict[key] = value;
        }
    }
}

但是为了获得基本行为的轻微更改,似乎有点过头了。

另一个解决方法可能是在当前上下文中定义一个Func,像这样:

Func<string, string> GetDictValueByKeyOrNull = (key) =>
{
    if (dict.ContainsKey(key))
        return dict[key];
    else
        return null;
};

因此,它可以像GetDictValueByKeyOrNull("key1")一样使用。

请问您能否给我更多建议或帮助我选择更好的方案?


1
相关:https://dev59.com/3HRB5IYBdhLWcg3wuZQ1?rq=1 - nawfal
2
也许这很新鲜,但为什么我们不直接使用 dict.TryGetValue 呢?http://msdn.microsoft.com/en-us/library/bb347013(v=vs.110).aspx - Jess
1
@Jess 这个并不像我想要的那么简洁。 - horgh
这个问题并不是https://dev59.com/3HRB5IYBdhLWcg3wuZQ1的重复,因为默认值(ValueType)不是null。 - shannon
@JamesMoore:可重入代码可能会关心ContainsKey检查执行额外的de-ref。 - shannon
显示剩余4条评论
7个回答

97

这是我从自己的库中提取的解决方案,作为扩展方法实现。我只是发布它,因为它是从字典 接口 实现的,并允许传入可选的默认值。

实现

public static TV GetValue<TK, TV>(this IDictionary<TK, TV> dict, TK key, TV defaultValue = default(TV))
{
    TV value;
    return dict.TryGetValue(key, out value) ? value : defaultValue;
}

使用方法

 MyDictionary.GetValue("key1");
 MyDictionary.GetValue("key2", -1);
 MyDictionary.GetValue("key3")?.SomeMethod();

14
依我看,最佳解决方案是使用C# 7中的内联out变量,这甚至可以成为一行代码的调用。 - Mladen Mihajlovic
9
Mladen指的是TryGetValue(key, out TV value) - silvalli
5
自从Core 2.0版本以来,就有一个GetValueOrDefault扩展方法可以实现相同的功能。(实际上有两个方法,一个带有defaultValue参数,一个没有)https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.getvalueordefault - Daniel

39
您无法使用扩展方法获得所需的语法,正如其他人所说,覆盖方法/运算符以更改其行为通常不是一个好主意。我认为您能做的最好的事情就是缩短您使用的名称。
如果您不与任何需要IDictionary的代码进行接口,则可以自由定义自己的接口,并且[]运算符的工作方式不会成为问题。
无论您最终称呼这个函数,都应该像这样实现它:
public static U Get<T, U>(this Dictionary<T, U> dict, T key)
    where U : class
{
    U val;
    dict.TryGetValue(key, out val);
    return val;
}

它只进行一次查找,而你的实现需要进行两次。

1
这并没有真正回答问题。不过感谢提供这些信息。 - horgh
1
应该是 dict.TryGetValue - Igor Pashchuk
1
@IgorPashchuk 已修复,谢谢。 - bmm6o

27
最终我使用了一种从字典类派生并具有显式接口实现的变体:
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
        {
            U val;
            this.TryGetValue(key, out val);
            return val;
        }
    }
}

所以它通过以下方式公开我需要的功能:
//create some dictionary
NullValueDictionary<int, string> dict = new NullValueDictionary<int, string>
{
    {1,"one"}
};
//have a reference to the interface
INullValueDictionary<int, string> idict = dict;

try
{
    //this throws an exception, as the base class implementation is utilized
    Console.WriteLine(dict[2] ?? "null");
}
catch { }
//this prints null, as the explicit interface implementation 
//in the derived class is used
Console.WriteLine(idict[2] ?? "null");

也可以放弃接口并用 new public U this[T key] 替换 U INullValueDictionary<T, U>.this[T key] - OfirD

24
添加一个DictionaryExtension类。
public static class DictionaryExtension
{
    public static TValue GetValueOrDefault<TKey, TValue>
        (   this IDictionary<TKey, TValue> dictionary,TKey key)
    {
        TValue value;
        return dictionary.TryGetValue(key, out value) ? value : default(TValue);
    }
}

如果在字典中找不到键,则它可以返回默认值

如果这是引用类型,则null为默认值。

_dic.GetValueOrDefault();

13
这个扩展现在已经存在于框架中。 - Paul Taylor

8
值得一提的是,HybridDictionary默认情况下就可以实现这个功能。虽然您会失去泛型类型,但可以获得找不到键值对时返回null的功能。而且(至少从理论上讲),在值较少的情况下,可能会获得性能上的优势。

1
这只值得一条注释。 - horgh
2
如果是一个字符串,字符串字典(StringDictionary)就不会失去类型。 - regisbsb

5

我想先声明,我不建议使用这种方法。虽然在某些情况下new关键字很有用,但它可能会导致难以发现的错误。除此之外,你可以尝试使用这个类。

class MyDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
    public new TValue this[TKey key]
    {
        get
        {
            TValue value;
            return TryGetValue(key, out value) ? value : default(TValue);
        }
        set { base[key] = value; }
    }
}

7
是的,这违反了**里氏替换原则**,由此造成的缺陷非常难以处理。 - Honza Brestan
我也不会使用这个))) 不管怎样,谢谢。 - horgh
1
@HonzaBrestan 你能解释一下吗?谢谢。 - arao6
8
这将改变“字典”类的合约。不再抛出异常,而是会得到一个默认值。这意味着当键不存在时可能会发生空引用异常,并且您无法知道键是否缺失以及键是否存在但包含空值。考虑一个第三方方法,它接受一个“字典”,依赖于其行为,但是您提供了带有此新的冲突行为的“MyDictionary”。LSP表示子类应该在基类可以使用的任何地方都是安全(正确)的。由于合约的更改,这显然违反了LSP原则。 - Honza Brestan
5
另一种看待这个问题的方式是通过类型系统 - 将异常视为方法签名的一部分(扩展返回类型),行为“在键不存在时引发异常”也是如此(当然,C#类型系统不能表达这一点,但我们现在可以想象它)。那么,这个新的MyDictionary索引器将具有不同的返回类型,而不是基类中原始的索引器 - 这可能不是您创建子类时想要的。 - Honza Brestan
1
哦,我现在明白了。再次感谢。 - arao6

0

我以前做过这个,只需继承常规的Dictionary类并隐藏索引器即可,效果非常好。这种方式非常简洁,因此您自动获得了所有常规Dictionary类的可靠性和熟悉度。

public class NullSafeDict<TKey, TValue> : Dictionary<TKey, TValue> where TValue : class
{
    public new TValue this[TKey key]
    {
        get
        {
            if (!ContainsKey(key))
                return null;
            else
                return base[key];
        }
        set
        {
            if (!ContainsKey(key))
                Add(key, value);
            else
                base[key] = value;
        }
    }
}

这可能会降低你的性能。 - rollsch

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