Отражение GetValue статического поля с циклической зависимостью возвращает null

#c# #reflection #visual-studio-2015 #static #c#-6.0

#c# #отражение #visual-studio-2015 #статическое #c #-6.0

Вопрос:

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

С помощью этих классов:

 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 равны нулю.

Комментарии:

1. Добавлены теги @Fredou.

2. @Fredou Да, извините, добавлено в конец сообщения.

3. @Fredou Я только что обновился до 4.6.1 в моем урезанном проекте, где я пытаюсь создать минимальное воспроизведение проблемы.

4. и последний вопрос, 32 или 64 бита?

5. Целевой платформой @Fredou является «Любой процессор», это 64-разрядная версия Windows, хотя не уверен, какой тип EXE создается.

Ответ №1:

Проблема

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

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

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

Комментарии:

1. Спасибо за ваш ответ. Это определенно кажется полезным ответом. Я добавил ссылку на github к вопросу, чтобы вы могли воспроизвести реальный сценарий.

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

3. Статическая инициализация — сложная вещь. У Джона Скита есть тема в его книге, я почти уверен, что вы можете найти другие источники. Но в целом это ненадежно, и вы не можете контролировать это из классов, что нарушает инкапсуляцию. Если вы можете заставить пользователей вашего кода всегда сначала вызывать метод, который вызывает MainType статическую инициализацию (при запуске приложения или что-то еще), тогда вы могли бы продолжать использовать поля, или если поля не имеют перекрестных зависимостей и т.д., Но с момента создания статического свойства только для чтения…

4. … это так просто — в основном тот же объем кода, они действительно должны быть первым выбором. Кстати, спасибо за редактирование ответа! Приветствия.

Ответ №2:

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

Комментарии:

1. К сожалению, это не так. В режиме отладки происходит сбой, и флаг оптимизации не установлен.