Reflection GetValue статического поля с возвратом круговой зависимости null

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

С этими классами:

public class MainType {
   public static readonly MainType One = new MainType();
   public static readonly MainType Two = SubType.Two;
}

public sealed class SubType : MainType {
   public new static readonly SubType Two = new SubType();
}

Получить поля One и Two:

List<FieldInfo> fieldInfos = typeof(MainType)
   .GetFields(BindingFlags.Static | BindingFlags.Public)
   .Where(f => typeof(MainType).IsAssignableFrom(f.FieldType))
   .ToList();

Наконец, получите их значения:

List<MainType> publicMainTypes = fieldInfos
   .Select(f => (MainType) f.GetValue(null))
   .ToList();

В LinqPad или в простом классе unit test с указанным выше кодом все работает нормально. Но в моем решении, где у меня есть некоторые модульные тесты, которые хотят работать со всеми экземплярами этих полей, GetValue отлично работает, чтобы возвращать поля родительского типа, но там, где предполагается, что родительские поля имеют экземпляры подтипа, они всегда вместо этого дайте null! (Если это произошло здесь, окончательный список будет { One, null } вместо { One, Two }.) Класс тестирования находится в другом проекте из двух типов (каждый в своем собственном файле), но я временно сделал все общедоступным. Я сбросил точку останова и просмотрел все, что я могу проверить, и сделал эквивалент fieldInfos[1].GetValue(null) в выражении Watch, и на самом деле он возвращает null, несмотря на то, что в моем основном классе есть строка точно так же, как второй из MainType выше.

Что не так? Как получить все значения полей подтипа? Как это возможно, чтобы они могли возвращать null без ошибки?

По теории, что по какой-то причине класс подтипа не был статически построен из-за доступа через отражение, я пробовал

System.Runtime.CompilerServices.RuntimeHelpers
  .RunClassConstructor(typeof(SubType).TypeHandle);

вверху перед запуском, но это не помогло (где SubType - фактический класс подтипа в моем проекте).

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

Дополнительная информация

После путаницы код начал работать. Теперь он снова не работает. Я работаю над воспроизведением того, что инициировало запуск кода.

Примечание. Ориентация .Net 4.6.1 с использованием С# 6.0 в Visual Studio 2015.

Репродукция проблемы доступна

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

Отладить модульные тесты. Когда возникает исключение, сделайте шаг, пока не дойдете до строки 20 GlossaryHelper.cs и не увидите возвращаемое значение GetGlossaryMembers на вкладке Locals. Вы можете видеть, что индексы с 3 по 12 являются нулевыми.

Ответ 1

Проблема

Проблема не имеет ничего общего с Reflection, а вместо этого связана с циклической зависимостью между двумя инициализаторами статического поля и порядком их выполнения.

Рассмотрим следующий фрагмент:

var b = MainType.Two;
var a = SubType.Two;
Debug.Assert(a == b); // Success

Теперь давайте обмениваем первые две строки:

var a = SubType.Two;
var b = MainType.Two;
Debug.Assert(a == b); // Fail! b == null

Итак, что здесь происходит? Давайте посмотрим:

  • Код пытается получить доступ к статическому полю SubType.Two в первый раз.
  • Статический инициализатор запускает и выполняет конструктор SubType.
  • Так как SubType наследует от MainType, конструктор MainType также выполняет и запускает статическую инициализацию MainType.
  • Статический инициализатор поля MainType.Two пытается получить доступ к SubType.Two. Поскольку статические инициализаторы выполняются только один раз, а один для SubType.Two уже выполнен (ну, на самом деле, он не выполняется, но считается, что он выполняется), он просто возвращает текущее значение поля (null в этот момент) который затем сохраняется в MainType.Two и будет возвращен дополнительными запросами доступа для этого поля.

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

Как исправить

Если возможно, избегайте таких зависимостей статического поля. Вместо этого используйте static readonly свойства. Они дают вам полный контроль, а также позволяют устранить дублирование поля (в настоящее время у вас есть два разных поля, которые содержат одно и то же значение).

Вот эквивалентная конструкция без таких проблем (с использованием С# 6.0):

public class MainType
{
    public static MainType One { get; } = new MainType();
    public static MainType Two => SubType.Two;
}

public sealed class SubType : MainType
{
    public new static SubType Two { get; } = new SubType();
}

Конечно, это потребует изменения кода отражения, чтобы использовать GetProperties вместо GetFields, но я думаю, что это того стоит.

Обновление: Еще один способ решения проблемы - переместить статические поля в вложенные абстрактные классы контейнеров:

public class MainType
{
    public abstract class Fields
    {
        public static readonly MainType One = new MainType();
        public static readonly MainType Two = SubType.Fields.Two;
    }
}

public sealed class SubType : MainType
{
    public new abstract class Fields : MainType.Fields
    {
        public new static readonly SubType Two = new SubType();
    }
}

Теперь оба теста успешно завершены:

var a = SubType.Fields.Two;
var b = MainType.Fields.Two;
Debug.Assert(a == b); // Success

и

var b = MainType.Fields.Two;
var a = SubType.Fields.Two;
Debug.Assert(a == b); // Success

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

Ответ 2

У меня была аналогичная проблема. Проблема заключалась в том, что я реализовал статическое поле класса и через отражение попытался использовать его значение. Он отлично работал в моем решении Debug, но не работал в моей производственной среде. Проблема заключается в том, что компилятор в конфигурации Release обнаружил, что этот статический метод никогда не используется и удаляет недостижимый код. Чтобы решить эту проблему, вы должны удалить флажок Оптимизировать код.