在向字典中添加键之前,最好的方法是如何检查该键是否存在?

14

当你不确定是否存在某个键时,你通常会使用TryGetValue而不是ContainsKey加上获取索引器来避免检查两次键的开销。换句话说,代码应该是这样的:

string password;
if (accounts.TryGetValue(username, out password))
{
    // use the password
}

这种方式比起这样更优:
if (accounts.ContainsKey(username))
{
    string password = accounts[username];
}

如果我想在设置键值之前检查键是否已经存在怎么办?例如,我想在用新密码覆盖用户名之前检查该用户名是否已经存在:

if (!accounts.ContainsKey(username))
{
    accounts.Add(username, password);
}
else
{
    Console.WriteLine("Username is taken!");
}

vs

// this doesn't exist
if (!accounts.TrySetValue(username, password))
{
    Console.WriteLine("Username is taken!");
}

是否有更加高效的替代方法可以代替使用ContainsKeyAdd实现这个功能呢?


var password = keyValue.Where(entry => entry.Key.Contains("password")) .Select(item => item.Key).FirstOrDefault(); - Sanjeev S
1
@SanjeevS 谢谢您的回答,但是LINQ甚至比我上面发布的后备解决方案还要低效。 - James Ko
1
@JamesKo:你正在追逐幻影。TryGetValue 调用了私有函数 FindEntryContainsKey 也调用了同样的私有 FindEntry 函数。索引器也调用了 FindEntry。据我所知,FindEntry 看起来非常快。你对代码进行了性能分析吗?你想解决什么问题? - Sam Axe
1
@JamesKo 这就是为什么你要先调用 ContainsKey。键集合是经过哈希处理的,查找的预期复杂度为 O(1)。最坏情况下的复杂度是 O(n),但这只有在有很多哈希冲突的情况下才会发生(这就是为什么仔细重写 GetHashCode 很重要)。我会和其他人一样建议你对代码进行性能分析。除非你试图进行“理论优化”,否则没有明确的答案。 - Grx70
显示剩余4条评论
4个回答

6
如果您认为插入一个新名称是常见情况,而尝试插入重复名称的情况很少见,那么您可能只需使用捕获异常的开销。
try
{
    accounts.Add(username, password);
}
catch (ArgumentException)
{
    Console.WriteLine("Username is taken!");
}

如果您使用已存在的键调用Add方法,则会抛出ArgumentException异常。即使您经常有重复的情况,这种方法仍然比ContainsKey检查更加高效。

2
这个答案中的数字表明,如果“罕见情况”发生的概率超过0.16%(每625次中有1次),仅使用try-catch会变得更慢,并且随着异常数量的增加而显著恶化,而ContainsKey检查将对大多数代码添加可忽略的开销。另外...如果在常规程序流程中有一种简单的方法可以避免异常,那么这总是首选的做法。请参阅以下文章:https://msdn.microsoft.com/en-us/library/ms229009(v=vs.100).aspx,https://stackoverflow.com/a/891230/804797,https://dev59.com/EnVC5IYBdhLWcg3w21Iq#161965 - Arkaine55

5
如果您不想覆盖,我认为最好编写自己的扩展方法,例如TryGetValue。没有标准方法。
或者,使用ConcurrentDictionary,它有TryAdd方法,但您需要同步开销。
所以,简单的答案是 - 没有这样的方法。

1
除非您可以访问“Dictionary”的私有成员,否则无法编写这样的扩展方法。 - James Ko
@JamesKo 你需要哪些私有方法?你已经有了ContainsKey,这已经足够了。 - Backs
1
这个问题是关于性能的。如果你先调用 ContainsKey,再调用 Add,那么你就会检查键是否存在两次。这就是为什么 TryGetValue 存在的原因,也是它不能满足这个问题的原因。 - James Ko
2
@JamesKo,这是什么原因?你真的有性能问题吗?还是只是为了证明它是可能的?我认为,Dictionary<,> 是非常优化的类。所以,如果你有性能问题 - 添加图形或其他东西来查看问题是否在检查中。简短的回答是 - 不,你不能 :) - Backs
那么如果我不是在为 Linux 内核编写绝对速度关键的代码,就不能在这里询问关于性能方面的问题吗?请别生气,我只是在问一个问题。 - James Ko
显示剩余7条评论

5

我倾向于根据需要编写自己的扩展。

比如,GetValueOrDefault 就像这样:

public static V GetValueOrDefault<K, V>(this IDictionary<K, V> @this, K key, Func<V> @default)
{
    return @this.ContainsKey(key) ? @this[key] : @default();
}

它可以像这样使用:

var password = accounts.GetValueOrDefault(username, () => null);
if (password != null)
{
    //do stuff
}

或者使用SetValueIfExists函数:
public static V SetValueIfExists<K, V>(this IDictionary<K, V> @this, K key, V value)
{
    if (@this.ContainsKey(key))
    {
        @this[key] = value;
    }
}

或者 SetValueIfNotExists:
public static V SetValueIfNotExists<K, V>(this IDictionary<K, V> @this, K key, V value)
{
    if (!@this.ContainsKey(key))
    {
        @this[key] = value;
    }
}

@Enigmativity 哈哈,谢谢你,但是如果你注意到的话,你的扩展方法和我在问题中提到的完全一样。 - James Ko
@JamesKo - 是的,但作为扩展方法,它可以使使用更整洁。这不是你想要的吗?还是你想要一些内置于语言中的东西? - Enigmativity
1
@Enigmativity ContainsKey + 索引器会将键索引两次。正如我在问题中所说,我想知道是否有一种方法只索引一次键。此外,您的新扩展方法仍然不是我在问题中要求的“SetValueIfNotExists”。 - James Ko
@JamesKo - 那么你一定是在寻找字典本身内置的功能。你可以使用智能感知来检查它。无法避免双重检查。我建议的方法是从代码中删除所认为的双重检查,但实际上无法删除它。 - Enigmativity
@Enigmativity,为什么你在@this之前加上@?或者你只是称它为你的字典?在同一行中有两个指向不同事物的this单词有点令人困惑。 - Kris
@Kris - 在变量名前使用@符号允许使用C#关键字作为变量名。在扩展方法中,我经常使用@this来清楚地表明这个变量是扩展方法的this。我发现这正好相反,不令人困惑。 - Enigmativity

2

我知道我来晚了,但你可以使用一个技巧,在索引器设置之前存储计数,并在索引器设置之后检查计数。如果计数相同,则已覆盖键,否则添加了新的映射:

public static bool AddOrUpdate<TKey, TValue>(this IDictionary<TKey, TValue> 
    dictionary, TKey key, TValue value)
{
    var countBefore = dictionary.Count;
    dictionary[key] = value;
    return countBefore != dictionary.Count;
}

有趣的想法。但请注意,这在“通常”情况下并没有帮助,因为您正在检查它是否存在,因为您不想覆盖现有条目。或者,如果您要覆盖,您希望返回被覆盖的值(例如,您需要处理旧值)。在第二种情况下,“AddOrUpdate”将返回“TValue”,并且会执行以下操作:“var valueBefore;”“dictionary.TryGetValue(key, out value);”“dictionary[key] = value;”“return valueBefore;”。当然,这具有James Ko试图避免的开销,只是提供一个有用的替代方案。 - ToolmakerSteve
这是真的(我只读了标题)。我想你在TryGet中意味着使用valueBefore。 - MaximGurschi

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