Отражение 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 или в простом классе юнит-тестов с приведенным выше кодом все работает нормально. Но в моем решении, где у меня есть несколько модульных тестов, которые хотят работать со всеми экземплярами этих полей, 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 с использованием C# 6.0 в Visual Studio 2015.

Доступно воспроизведение проблемы

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

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


person ErikE    schedule 21.10.2016    source источник
comment
Добавлены теги @Fredou.   -  person ErikE    schedule 01.11.2016
comment
@Fredou Да, извините, добавлено в конец сообщения.   -  person ErikE    schedule 01.11.2016
comment
@Fredou Я только что обновился до 4.6.1 в своем урезанном проекте, где я пытаюсь создать минимальное воспроизведение проблемы.   -  person ErikE    schedule 01.11.2016
comment
и последний вопрос, 32 или 64 бита?   -  person Fredou    schedule 01.11.2016
comment
@Fredou Платформа предназначена для любого процессора, это 64-битная версия Windows, однако не уверен, какой тип EXE создается.   -  person ErikE    schedule 01.11.2016
comment
дайте нам знать, когда вы сможете воспроизвести его (отладка? выпуск? оптимизация кода? любая другая конкретная настройка?) со своей стороны, я пробовал несколько вещей, и до сих пор я никогда не видел null   -  person Fredou    schedule 01.11.2016
comment
@Fredou Никаких других специальных настроек. Отлаживать. Все остальные стандартные настройки проекта.   -  person ErikE    schedule 01.11.2016
comment
@ErikE Если вы можете, загрузите упомянутое вами решение из 12 файлов на github, чтобы мы могли загрузить его и попытаться вам помочь.   -  person dlcardozo    schedule 01.11.2016
comment
Вы используете VS с повышенными привилегиями? Попробуйте запустить VS с привилегиями и без них, чтобы увидеть, дают ли оба случая одинаковый результат.   -  person dlcardozo    schedule 01.11.2016
comment
@camaron Подойдет, когда вернусь домой. Да при запуске от имени администратора уже.   -  person ErikE    schedule 01.11.2016
comment
Вы описываете поведение программы с ошибкой гонки потоков. Это реальная проблема с инициализаторами типов, CLR только делает все возможное, чтобы обеспечить требуемую блокировку, но без писка сдастся, если обнаружит возможную взаимоблокировку. В этом случае это позволяет потоку участвовать в гонке, поэтому получение нулевого значения из-за того, что инициализатор типа для SubType еще не завершен, представляет собой реальную опасность. Единственное, что совершенно не очевидно из вопроса, это то, как два потока могут пытаться запустить этот код, особенно в сценарии модульного тестирования. Украсьте пожалуйста.   -  person Hans Passant    schedule 01.11.2016
comment
Добавлена ​​ссылка @Fredou на github.   -  person ErikE    schedule 01.11.2016
comment
Добавлена ​​ссылка @camaron на github.   -  person ErikE    schedule 01.11.2016
comment
Добавлена ​​ссылка на github @HansPassant.   -  person ErikE    schedule 01.11.2016
comment
Вау, как можно MainType инициализировать переменную, используя член производного класса? Может ли базовый класс узнать свой производный класс, если производный класс определен после базового класса?   -  person Marson Mao    schedule 02.11.2016
comment
@MarsonMao Это головоломка, которую вам нужно решить, не так ли, поскольку код работал нормально, пока я не попытался провести его модульное тестирование. Подсказка: не путайте определение с инициализацией.   -  person ErikE    schedule 02.11.2016


Ответы (2)


Проблема

Проблема не имеет ничего общего с 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

Так что же здесь происходит? Посмотрим:

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

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

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

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

Вот эквивалентный дизайн без таких проблем (с использованием C # 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), поэтому не имеют побочных эффектов, вызванных вызовами базового конструктора.

person Ivan Stoev    schedule 01.11.2016
comment
Благодарю за ваш ответ. Это определенно кажется полезным ответом. Я добавил ссылку на github к вопросу, чтобы вы могли поиграть с реальным сценарием. - person ErikE; 01.11.2016
comment
Я уверен, что награжу вас большей частью награды, если не всей; Я просто жду, чтобы узнать, есть ли у кого-нибудь еще дополнительная информация (например, нестандартный способ продолжать использовать поля) и, возможно, некоторые более общие рекомендации и контекст (теория информатики?), которые помогут мне избежать таких ловушек в будущем. - person ErikE; 01.11.2016
comment
Инициализация статики — сложная штука. У Джона Скита есть тема в его книге, я почти уверен, что есть и другие источники ты можешь найти. Но в целом это ненадежно, и вы не можете контролировать это из классов, что нарушает инкапсуляцию. Если вы можете заставить пользователей вашего кода всегда сначала вызывать метод, который вызывает MainType статическую инициализацию (при запуске приложения или что-то в этом роде), вы можете продолжать использовать поля, или если поля не имеют перекрестных зависимостей и т. д., но поскольку создание статическое свойство только для чтения теперь... - person Ivan Stoev; 01.11.2016
comment
... это так просто - в основном такое же количество кода, они действительно должны быть первым выбором. Кстати, спасибо за редактирование ответа! Ваше здоровье. - person Ivan Stoev; 01.11.2016

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

person Alex Pashkin    schedule 01.11.2016
comment
К сожалению, это не так. Это не удается в режиме отладки, и флаг оптимизации не установлен. - person ErikE; 01.11.2016