Что более эффективно: Словарь TryGetValue или ContainsKey + Item?

Из записи MSDN на Dictionary.TryGetValue Method:

Этот метод объединяет функциональность метода ContainsKey и свойство Item.

Если ключ не найден, то параметр значения получает соответствующий значение по умолчанию для типа значения TValue; например, 0 (ноль) для целочисленные типы, false для типов Boolean и null для ссылочных типов.

Используйте метод TryGetValue, если ваш код часто пытается получить доступ ключи, которые не находятся в словаре. Использование этого метода больше эффективнее, чем поймать исключение KeyNotFoundException, заданное элементом свойство.

Этот метод приближается к операции O (1).

Из описания не ясно, эффективнее ли это или просто удобнее вызова ContainsKey, а затем выполняет поиск. Выполняет ли реализация TryGetValue только вызов ContainsKey, а затем Item или на самом деле более эффективный, чем при выполнении одного поиска?

Другими словами, что более эффективно (т.е. которое выполняет меньше запросов):

Dictionary<int,int> dict;
//...//
int ival;
if(dict.ContainsKey(ikey))
{
  ival = dict[ikey];
}
else
{
  ival = default(int);
}

или

Dictionary<int,int> dict;
//...//
int ival;
dict.TryGetValue(ikey, out ival);

Примечание. Я не ищу тест!

Ответ 1

TryGetValue будет быстрее.

ContainsKey использует ту же проверку, что и TryGetValue, которая внутренне относится к фактическому местоположению записи. Свойство Item фактически имеет почти идентичные функциональные возможности кода как TryGetValue, за исключением того, что оно генерирует исключение вместо возврата false.

Использование ContainsKey, за которым следует элемент, в основном дублирует функциональность поиска, которая является основной частью вычислений в этом случае.

Ответ 2

Быстрый тест показывает, что TryGetValue имеет небольшое ребро:

    static void Main() {
        var d = new Dictionary<string, string> {{"a", "b"}};
        var start = DateTime.Now;
        for (int i = 0; i != 10000000; i++) {
            string x;
            if (!d.TryGetValue("a", out x)) throw new ApplicationException("Oops");
            if (d.TryGetValue("b", out x)) throw new ApplicationException("Oops");
        }
        Console.WriteLine(DateTime.Now-start);
        start = DateTime.Now;
        for (int i = 0; i != 10000000; i++) {
            string x;
            if (d.ContainsKey("a")) {
                x = d["a"];
            } else {
                x = default(string);
            }
            if (d.ContainsKey("b")) {
                x = d["b"];
            } else {
                x = default(string);
            }
        }
   }

Это создает

00:00:00.7600000
00:00:01.0610000

делает доступ к ContainsKey + Item примерно на 40% медленнее, предполагая равномерное сочетание хитов и промахов.

Кроме того, когда я меняю программу, чтобы всегда пропустить (т.е. всегда смотря вверх "b"), обе версии становятся одинаково быстрыми:

00:00:00.2850000
00:00:00.2720000

Когда я делаю это "все хиты", тем не менее, TryGetValue остается явным победителем:

00:00:00.4930000
00:00:00.8110000

Ответ 3

Поскольку ни один из ответов на данный момент фактически не отвечает на вопрос, вот приемлемый ответ, который я нашел после некоторого исследования:

Если вы декомпилируете TryGetValue, вы увидите, что он делает это:

public bool TryGetValue(TKey key, out TValue value)
{
  int index = this.FindEntry(key);
  if (index >= 0)
  {
    value = this.entries[index].value;
    return true;
  }
  value = default(TValue);
  return false;
}

тогда как метод ContainsKey:

public bool ContainsKey(TKey key)
{
  return (this.FindEntry(key) >= 0);
}

поэтому TryGetValue просто ContainsKey плюс поиск массива, если элемент присутствует.

Источник

Похоже, что TryGetValue будет почти в два раза быстрее, чем ContainsKey + Item.

Ответ 4

Кому нужно: -)

Вероятно, вы спрашиваете, потому что TryGetValue является болью для использования, поэтому инкапсулируйте его таким образом с помощью метода расширения.

public static class CollectionUtils
{
    // my original method
    // public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dic, K key)
    // {
    //    V ret;
    //    bool found = dic.TryGetValue(key, out ret);
    //    if (found)
    //    {
    //        return ret;
    //    }
    //    return default(V);
    // }


    // EDIT: one of many possible improved versions
    public static TValue GetValueOrDefault<K, V>(this IDictionary<K, V> dictionary, K key)
    {
        // initialized to default value (such as 0 or null depending upon type of TValue)
        TValue value;  

        // attempt to get the value of the key from the dictionary
        dictionary.TryGetValue(key, out value);
        return value;
    }

Затем просто позвоните:

dict.GetValueOrDefault("keyname")

или

(dict.GetValueOrDefault("keyname") ?? fallbackValue) 

Ответ 5

Почему бы вам не проверить его?

Но я уверен, что TryGetValue быстрее, потому что он выполняет только один поиск. Конечно, это не гарантируется, то есть разные реализации могут иметь разные характеристики производительности.

То, как я реализую словарь, - это создать внутреннюю функцию Find, которая найдет слот для элемента, а затем построит остальные поверх этого.

Ответ 6

Все ответы до сих пор, хотя и хорошие, пропустить жизненно важную точку.

Методы в классы API (например,.NET framework) составляют часть определения интерфейса (а не интерфейс С# или VB, но интерфейс в значении компьютерной науки).

Как обычно, обычно неправильно спрашивать, быстрее ли вызов такого метода, если только скорость не является частью определения формального интерфейса (что в этом случае не так).

Традиционно этот вид ярлыка (сочетающий поиск и извлечение) более эффективен независимо от языка, инфраструктуры, ОС, платформы или машинной архитектуры. Это также более читаемо, потому что оно явно выражает ваше намерение, а не подразумевает его (из структуры вашего кода).

Таким образом, ответ (от серого старого взлома) определенно "Да" (TryGetValue предпочтительнее комбинации ContainsKey и Item [Get] для извлечения значения из Словаря).

Если вы думаете, что это звучит странно, подумайте об этом так: Даже если текущие реализации TryGetValue, ContainsKey и Item [Get] не дают никакой разницы в скорости, вы можете предположить, что, вероятно, будущая реализация (например .NET v5) (TryGetValue будет быстрее). Подумайте о жизни вашего программного обеспечения.

В стороне, интересно отметить, что типичные современные технологии определения интерфейса по-прежнему редко предоставляют какие-либо средства формального определения ограничений по времени. Возможно .NET v5?

Ответ 7

Создание быстрой тестовой программы, безусловно, улучшает использование TryGetValue с 1 миллионом элементов в словаре.

Результаты:

ContainsKey + Item for 1000000 просмотров: 45ms

TryGetValue для 1000000 просмотров: 26ms

Вот тестовое приложение:

static void Main(string[] args)
{
    const int size = 1000000;

    var dict = new Dictionary<int, string>();

    for (int i = 0; i < size; i++)
    {
        dict.Add(i, i.ToString());
    }

    var sw = new Stopwatch();
    string result;

    sw.Start();

    for (int i = 0; i < size; i++)
    {
        if (dict.ContainsKey(i))
            result = dict[i];
    }

    sw.Stop();
    Console.WriteLine("ContainsKey + Item for {0} hits: {1}ms", size, sw.ElapsedMilliseconds);

    sw.Reset();
    sw.Start();

    for (int i = 0; i < size; i++)
    {
        dict.TryGetValue(i, out result);
    }

    sw.Stop();
    Console.WriteLine("TryGetValue for {0} hits: {1}ms", size, sw.ElapsedMilliseconds);

}

Ответ 8

На моей машине с нагрузкой ОЗУ при запуске в режиме RELEASE (не DEBUG) ContainsKey равно TryGetValue/try-catch, если все записи в Dictionary<> найдены.

ContainsKey намного превосходит их всех, если не найдено нескольких слов в словаре (в моем примере ниже, установите MAXVAL на что-либо большее, чем ENTRIES, чтобы пропустить некоторые записи):

Результаты:

Finished evaluation .... Time distribution:
Size: 000010: TryGetValue: 53,24%, ContainsKey: 1,74%, try-catch: 45,01% - Total: 2.006,00
Size: 000020: TryGetValue: 37,66%, ContainsKey: 0,53%, try-catch: 61,81% - Total: 2.443,00
Size: 000040: TryGetValue: 22,02%, ContainsKey: 0,73%, try-catch: 77,25% - Total: 7.147,00
Size: 000080: TryGetValue: 31,46%, ContainsKey: 0,42%, try-catch: 68,12% - Total: 17.793,00
Size: 000160: TryGetValue: 33,66%, ContainsKey: 0,37%, try-catch: 65,97% - Total: 36.840,00
Size: 000320: TryGetValue: 34,53%, ContainsKey: 0,39%, try-catch: 65,09% - Total: 71.059,00
Size: 000640: TryGetValue: 32,91%, ContainsKey: 0,32%, try-catch: 66,77% - Total: 141.789,00
Size: 001280: TryGetValue: 39,02%, ContainsKey: 0,35%, try-catch: 60,64% - Total: 244.657,00
Size: 002560: TryGetValue: 35,48%, ContainsKey: 0,19%, try-catch: 64,33% - Total: 420.121,00
Size: 005120: TryGetValue: 43,41%, ContainsKey: 0,24%, try-catch: 56,34% - Total: 625.969,00
Size: 010240: TryGetValue: 29,64%, ContainsKey: 0,61%, try-catch: 69,75% - Total: 1.197.242,00
Size: 020480: TryGetValue: 35,14%, ContainsKey: 0,53%, try-catch: 64,33% - Total: 2.405.821,00
Size: 040960: TryGetValue: 37,28%, ContainsKey: 0,24%, try-catch: 62,48% - Total: 4.200.839,00
Size: 081920: TryGetValue: 29,68%, ContainsKey: 0,54%, try-catch: 69,77% - Total: 8.980.230,00

Здесь мой код:

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;

    namespace ConsoleApplication1
    {
        class Program
        {
            static void Main(string[] args)
            {
                const int ENTRIES = 10000, MAXVAL = 15000, TRIALS = 100000, MULTIPLIER = 2;
                Dictionary<int, int> values = new Dictionary<int, int>();
                Random r = new Random();
                int[] lookups = new int[TRIALS];
                int val;
                List<Tuple<long, long, long>> durations = new List<Tuple<long, long, long>>(8);

                for (int i = 0;i < ENTRIES;++i) try
                    {
                        values.Add(r.Next(MAXVAL), r.Next());
                    }
                    catch { --i; }

                for (int i = 0;i < TRIALS;++i) lookups[i] = r.Next(MAXVAL);

                Stopwatch sw = new Stopwatch();
                ConsoleColor bu = Console.ForegroundColor;

                for (int size = 10;size <= TRIALS;size *= MULTIPLIER)
                {
                    long a, b, c;

                    Console.ForegroundColor = ConsoleColor.Yellow;
                    Console.WriteLine("Loop size: {0}", size);
                    Console.ForegroundColor = bu;

                    // ---------------------------------------------------------------------
                    sw.Start();
                    for (int i = 0;i < size;++i) values.TryGetValue(lookups[i], out val);
                    sw.Stop();
                    Console.WriteLine("TryGetValue: {0}", a = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    sw.Restart();
                    for (int i = 0;i < size;++i) val = values.ContainsKey(lookups[i]) ? values[lookups[i]] : default(int);
                    sw.Stop();
                    Console.WriteLine("ContainsKey: {0}", b = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    sw.Restart();
                    for (int i = 0;i < size;++i)
                        try { val = values[lookups[i]]; }
                        catch { }
                    sw.Stop();
                    Console.WriteLine("try-catch: {0}", c = sw.ElapsedTicks);

                    // ---------------------------------------------------------------------
                    Console.WriteLine();

                    durations.Add(new Tuple<long, long, long>(a, b, c));
                }

                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Finished evaluation .... Time distribution:");
                Console.ForegroundColor = bu;

                val = 10;
                foreach (Tuple<long, long, long> d in durations)
                {
                    long sum = d.Item1 + d.Item2 + d.Item3;

                    Console.WriteLine("Size: {0:D6}:", val);
                    Console.WriteLine("TryGetValue: {0:P2}, ContainsKey: {1:P2}, try-catch: {2:P2} - Total: {3:N}", (decimal)d.Item1 / sum, (decimal)d.Item2 / sum, (decimal)d.Item3 / sum, sum);
                    val *= MULTIPLIER;
                }

                Console.WriteLine();
            }
        }
    }

Ответ 9

Если вы пытаетесь извлечь значение из словаря, TryGetValue (значение ключа, выход) является лучшим вариантом, но если вы проверяете наличие ключа, для новой вставки, без перезаписи старые ключи, и только с этой областью, ContainsKey (ключ) является лучшим вариантом, эталонный тест может подтвердить это:

using System;
using System.Threading;
using System.Diagnostics;
using System.Collections.Generic;
using System.Collections;

namespace benchmark
{
class Program
{
    public static Random m_Rand = new Random();
    public static Dictionary<int, int> testdict = new Dictionary<int, int>();
    public static Hashtable testhash = new Hashtable();

    public static void Main(string[] args)
    {
        Console.WriteLine("Adding elements into hashtable...");
        Stopwatch watch = Stopwatch.StartNew();
        for(int i=0; i<1000000; i++)
            testhash[i]=m_Rand.Next();
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds);
        Thread.Sleep(4000);
        Console.WriteLine("Adding elements into dictionary...");
        watch = Stopwatch.StartNew();
        for(int i=0; i<1000000; i++)
            testdict[i]=m_Rand.Next();
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds);
        Thread.Sleep(4000);

        Console.WriteLine("Finding the first free number for insertion");
        Console.WriteLine("First method: ContainsKey");
        watch = Stopwatch.StartNew();
        int intero=0;
        while (testdict.ContainsKey(intero))
        {
            intero++;
        }
        testdict.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero);
        Thread.Sleep(4000);
        Console.WriteLine("Second method: TryGetValue");
        watch = Stopwatch.StartNew();
        intero=0;
        int result=0;
        while(testdict.TryGetValue(intero, out result))
        {
            intero++;
        }
        testdict.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero);
        Thread.Sleep(4000);
        Console.WriteLine("Test hashtable");
        watch = Stopwatch.StartNew();
        intero=0;
        while(testhash.Contains(intero))
        {
            intero++;
        }
        testhash.Add(intero, m_Rand.Next());
        watch.Stop();
        Console.WriteLine("Done in {0:F4} -- added value {1} into hashtable -- pause....", watch.Elapsed.TotalSeconds, intero);
        Console.Write("Press any key to continue . . . ");
        Console.ReadKey(true);
    }
}
}

Это истинный пример. У меня есть служба, которая для каждого созданного "элемента" ассоциируется с прогрессивным числом, это число при каждом создании нового элемента должно быть бесплатным, если вы удалите элемент, бесплатный номер становится бесплатным, конечно, это не оптимизировано, так как у меня есть статический var, который кэширует текущий номер, но если вы закончите все числа, вы можете снова начать с 0 до UInt32.MaxValue

Выполненный тест:
Добавление элементов в хэш-таблицу...
Сделано в 0,5908 - пауза....
Добавление элементов в словарь...
Сделано в 0,2679 - пауза....
Поиск первого бесплатного номера для вставки
Первый метод: ContainsKey
Выполнено в 0,0561 - добавлено значение 1000000 в словаре - пауза....
Второй метод: TryGetValue
Выполнено в 0,0643 - добавленная стоимость 1000001 в словаре - пауза....
Тест хэш-таблицы
Сделано в 0,3015 - добавленная стоимость 1000000 в хэш-таблицу - пауза....
Нажмите любую клавишу для продолжения.,

Если некоторые из вас могут спросить, может ли ContainsKeys иметь преимущество, я даже попытался инвертировать TryGetValue с помощью клавиши Contains, результат будет таким же.

Итак, для меня, с окончательным учетом, все зависит от того, как ведет себя программа.

Ответ 10

Помимо разработки микробенчмарка, который даст точные результаты в практической обстановке, вы можете проверить справочный источник .NET Framework.

Все они вызывают метод FindEntry(TKey) который выполняет большую часть работы и не запоминает его результат, поэтому вызов TryGetValue почти в два раза быстрее, чем ContainsKey + Item.


TryGetValue интерфейс TryGetValue можно адаптировать с помощью метода расширения:

using System.Collections.Generic;

namespace Project.Common.Extensions
{
    public static class DictionaryExtensions
    {
        public static TValue GetValueOrDefault<TKey, TValue>(
            this IDictionary<TKey, TValue> dictionary,
            TKey key,
            TValue defaultValue = default(TValue))
        {
            if (dictionary.TryGetValue(key, out TValue value))
            {
                return value;
            }
            return defaultValue;
        }
    }
}

Начиная с С# 7.1, вы можете заменить default(TValue) обычным default.Тип выведен.

Использование:

var dict = new Dictionary<string, string>();
string val = dict.GetValueOrDefault("theKey", "value used if theKey is not found in dict");

Он возвращает null для ссылочных типов, поиск которых не удался, если не указано явное значение по умолчанию.

var dictObj = new Dictionary<string, object>();
object valObj = dictObj.GetValueOrDefault("nonexistent");
Debug.Assert(valObj == null);

val dictInt = new Dictionary<string, int>();
int valInt = dictInt.GetValueOrDefault("nonexistent");
Debug.Assert(valInt == 0);

Ответ 11

/// <summary>
        /// dict.GetValueOrDefault("keyname") or (dict.GetValueOrDefault("keyname") ?? fallbackValue) 
        /// </summary>
        /// <typeparam name="TKey"></typeparam>
        /// <typeparam name="TValue"></typeparam>
        /// <param name="dictionary"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
        {
            TValue value;                        
            dictionary.TryGetValue(key, out value);
            return value;
        }