Как этот контекст окружения становится нулевым?
Может ли кто-нибудь помочь мне объяснить, как TimeProvider.Current
может стать нулевым в следующем классе?
public abstract class TimeProvider
{
private static TimeProvider current =
DefaultTimeProvider.Instance;
public static TimeProvider Current
{
get { return TimeProvider.current; }
set
{
if (value == null)
{
throw new ArgumentNullException("value");
}
TimeProvider.current = value;
}
}
public abstract DateTime UtcNow { get; }
public static void ResetToDefault()
{
TimeProvider.current = DefaultTimeProvider.Instance;
}
}
Наблюдения
- Все модульные тесты, которые напрямую ссылаются на TimeProvider, также вызывают ResetToDefault() в их отключении Fixture.
- Не используется многопоточный код.
- Время от времени один из модульных тестов выходит из строя, потому что
TimeProvider.Current
имеет значение null (исключение NullReferenceException).
- Это происходит только при запуске всего пакета, но не тогда, когда я запускаю только один unit test, предлагая мне, что происходит некоторая тонкая тестовая взаимозависимость.
- Это происходит примерно раз каждые пять или шесть тестовых прогонов.
- Когда происходит сбой, похоже, это происходит в первых выполненных тестах, которые включают
TimeProvider.Current
.
- Более одного теста может потерпеть неудачу, но только один сбой в данном тестовом прогоне.
FWIW, здесь также класс DefaultTimeProvider:
public class DefaultTimeProvider : TimeProvider
{
private readonly static DefaultTimeProvider instance =
new DefaultTimeProvider();
private DefaultTimeProvider() { }
public override DateTime UtcNow
{
get { return DateTime.UtcNow; }
}
public static DefaultTimeProvider Instance
{
get { return DefaultTimeProvider.instance; }
}
}
Я подозреваю, что некоторые тонкие взаимодействия происходят со статической инициализацией, где на время выполнения фактически разрешен доступ к TimeProvider.Current
до того, как закончится статическая инициализация, но я не могу на нее наложить.
Любая помощь приветствуется.
FWIW Я просто бросил
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
в получателе, и он последовательно сообщает один и тот же идентификатор для всех тестовых примеров в тестовом прогоне, поэтому проблема не связана с потоковой обработкой.
Ответы
Ответ 1
Основываясь только на этом коде, Current
может быть null
на основе того, что он установлен на null
. Это явно не поможет вам.
Не могли бы вы предоставить код для тестов? Если есть тестовая взаимозависимость, для читателей было бы полезно предоставить любую обратную связь.
В то же время возможно, что статья Jon Skeet, посвященная синглонам, может быть полезна, поскольку DefaultTimeProvider
эффективно действует как singleton: http://csharpindepth.com/Articles/General/Singleton.aspx
Ответ 2
У меня может быть частичный ответ на этот вопрос, благодаря ссылкам, предоставленным Питером Ритчи, хотя я не могу полностью объяснить, что происходит. Казалось бы, произошла какая-то гонка между статической инициализацией TimeProvider и DefaultTimeProvider. Это может иметь отношение к beforefieldinit.
Изменение реализации, похоже, решило проблему. Если нет, это, безусловно, сделало условие гонки намного реже, до такой степени, что я еще не видел его.
Я изменил инициализацию TimeProvider на это:
public abstract class TimeProvider
{
private static TimeProvider current;
static TimeProvider()
{
TimeProvider.current = new DefaultTimeProvider();
}
//...
}
И DefaultTimeProvider просто для этого:
public class DefaultTimeProvider : TimeProvider
{
public override DateTime UtcNow
{
get { return DateTime.UtcNow; }
}
}
В игре есть только один статический инициализатор (TimeProvider), и поскольку он является явным статическим конструктором, класс не помечен beforefieldinit.
Это похоже на трюк...