Почему быстрее проверять, содержит ли словарь ключ, а не улавливать исключение в случае, если это не так?

Представьте код:

public class obj
{
    // elided
}

public static Dictionary<string, obj> dict = new Dictionary<string, obj>();

Метод 1

public static obj FromDict1(string name)
{
    if (dict.ContainsKey(name))
    {
        return dict[name];
    }
    return null;
}

Метод 2

public static obj FromDict2(string name)
{
    try
    {
        return dict[name];
    }
    catch (KeyNotFoundException)
    {
        return null;
    }
}

Мне было любопытно, есть ли разница в производительности этих двух функций, потому что первый должен быть SLOWER, чем второй, - учитывая, что ему нужно дважды проверять, если словарь содержит значение, а вторая функция должна получить доступ словарь только один раз, но WOW, это фактически противоположно:

Петля для 1 000 000 значений (со 100 000 существующих и 900 000 не существующих):

первая функция: 306 миллисекунд

вторая функция: 20483 миллисекунды

Почему это?

EDIT: как вы можете заметить в комментариях ниже этого вопроса, производительность второй функции на самом деле немного лучше, чем первая, если есть 0 не существующих ключей. Но как только есть хотя бы один или несколько не существующих ключей, производительность второго снижается быстро.

Ответ 1

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

BTW: Правильный способ сделать это - использовать TryGetValue

obj item;
if(!dict.TryGetValue(name, out item))
    return null;
return item;

Доступ к словарю осуществляется только один раз, а не дважды. Если вы действительно хотите просто вернуть null, если ключ не существует, вышеуказанный код можно упростить далее:

obj item;
dict.TryGetValue(name, out item);
return item;

Это работает, потому что TryGetValue устанавливает item в null, если нет ключа с name.

Ответ 2

Словари специально разработаны для быстрого быстрого поиска ключей. Они реализованы как hashtables, и чем больше записей, тем быстрее они относятся к другим методам. Использование механизма исключений допускается только тогда, когда ваш метод не смог выполнить то, что вы его разработали, потому что это большой набор объектов, который дает вам много возможностей для обработки ошибок. Я построил целый класс библиотеки один раз со всем, что было окружено блоками try catch, и был потрясен, увидев вывод отладки, который содержал отдельную строку для каждого из более чем 600 исключений!