Что может заставить это свойство время от времени исключать исключение NullReferenceException?

У меня есть класс asp.net/C#, который изменяет размеры изображений для кеширования на сервере в виде файлов, однако часть кода, которая определяет, какой кодировщик использовать, иногда бросает исключение NullReferenceException.

Вот код, который инициализирует и передает обратно кодеры:

public static class ImageUtilities{    
    private static Dictionary<string, ImageCodecInfo> encoders = null;

    public static Dictionary<string, ImageCodecInfo> Encoders{
        get{
            if (encoders == null){
                encoders = new Dictionary<string, ImageCodecInfo>();
            }

            //if there are no codecs, try loading them
            if (encoders.Count == 0){
                foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders()){
                    encoders.Add(codec.MimeType.ToLower(), codec);
                }
            }

            return encoders;
        }
    }
    ...

Это конкретная строка, на которую распространяется исключение:

encoders.Add(codec.MimeType.ToLower(), codec);

Это текст ошибки:

Object reference not set to an instance of an object.
    at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
    at System.Collections.Generic.Dictionary`2.Add(TKey key, TValue value)

Это единственное место, где вызывается свойство Encoders (а затем строка ниже этой в трассировке стека):

if (Encoders.ContainsKey(lookupKey)){
    foundCodec = Encoders[lookupKey];
}

Даже если lookupKey был нулевым, не должен ли поиск просто возвращать null, а не бросать исключение?

Ответ 1

Вы пытаетесь использовать "ленивый загруженный синглтон", но вы не принимаете concurrency. Самый простой способ сделать это, не жертвуя производительностью, - это Lazy<T>:

private static Lazy<Dictionary<string, ImageCodecInfo>> _encoders =
    new Lazy<Dictionary<string, ImageCodecInfo>>(() =>
        ImageCodecInfo.GetImageEncoders().ToDictionary(x => x.MimeType.ToLower(), x => x));

public static Dictionary<string, ImageCodecInfo> Encoders
{
    get { return _encoders.Value; }
}

Это шаблон # 6 Джон Скит - отличная статья о различных способах реализации этого шаблона.

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

private static Lazy<ReadOnlyDictionary<string, ImageCodecInfo>> _encoders =
    new Lazy<ReadOnlyDictionary<string, ImageCodecInfo>>(() =>
        new ReadOnlyDictionary<string, ImageCodecInfo>(
            ImageCodecInfo.GetImageEncoders()
                .ToDictionary(x => x.MimeType.ToLower(), x => x)));

public static IReadOnlyDictionary<string, ImageCodecInfo> Encoders
{
    get { return _encoders.Value; }
}

Другой способ, которым вы можете справиться с этим, - это ConcurrentDictionary, но это похоже на излишний, поскольку вы не будете часто добавлять элементы.

Ответ 2

Поскольку этот код находится в приложении ASP.NET, могут возникнуть некоторые проблемы с concurrency. Попробуйте создать оператор словаря lock:

private static object _lock = new object();
public static Dictionary<string, ImageCodecInfo> Encoders{
    get{
       lock(_lock) {
        if (encoders == null){
            encoders = new Dictionary<string, ImageCodecInfo>();
        }

        //if there are no codecs, try loading them
        if (encoders.Count == 0){
            foreach (ImageCodecInfo codec in ImageCodecInfo.GetImageEncoders()){
                encoders.Add(codec.MimeType.ToLower(), codec);
            }
        }

        return encoders;
         }
    }
}

Обычно Dictionary не может иметь null ключи (потому что вызов GetHashCode() на каждый объект, который вы ввели). Но поскольку вы вызываете .ToLower() в MimeType - это скорее != null (иначе исключение было бы намного раньше). Если lock не решит проблему, которую вы можете проверить, какое значение вы действительно вставляете в словарь с помощью отладчика.

Ответ 3

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

public static Dictionary<string, ImageCodecInfo> Encoders
{
    get {
        return encoders ??
               (encoders = ImageCodecInfo.GetImageEncoders().ToDictionary(c => c.MimeType.ToLower()));
    }
}