Переопределение универсального итератора приводит к возникновению исключения BadImageFormatException при компиляции с помощью Visual Studio 2010

tl;dr:

  • Переопределение универсального метода итератора в сконструированном производном классе приводит к выдаче BadImageFormatException при компиляции с Visual Studio 2010 (VS2010), независимо от версии .NET (2.0, 3.0, 3.5 или 4), платформы или конфигурации. Проблема не воспроизводится в Visual Studio 2012 (VS2012) и выше.
  • Содержимое базового метода (при условии, что исходный код компилируется) не имеет значения, поскольку он не выполняется.

Как этого избежать?


Описание проблемы

При переходе к in in Main в коде в MVCE ниже (который обычно переводит выполнение в метод итератора) _ 4_ выдается при компиляции кода в Visual Studio 2010:

BadImageFormatException в VS2010

но не в Visual Studio 2012 и выше:

BadImageFormatException в VS2012


MCVE

public class Program
{
    public static void Main(string[] args)
    {
        foreach ( var item in new ScrappyDoo().GetIEnumerableItems() )
            Console.WriteLine(item.ToString());
    }
}

public class ScoobyDoo<T>
    where T : new()
{
    public virtual IEnumerable<T> GetIEnumerableItems()
    {
        yield return new T();
    }
}

public class ScrappyDoo : ScoobyDoo<object>
{
    public override IEnumerable<object> GetIEnumerableItems()
    {
        foreach ( var item in base.GetIEnumerableItems() )
            yield return item;
    }
}

Примечания

  • При проверке кода с помощью ILSpy скомпилированный IL для ScrappyDoo.GetIEnumerableItems был одинаковым для двоичных файлов VS2010 и VS2012:

    .method public hidebysig virtual 
        instance class [mscorlib]System.Collections.Generic.IEnumerable`1<object> GetIEnumerableItems () cil managed 
    {
        // Method begins at RVA 0x244c
        // Code size 21 (0x15)
        .maxstack 2
        .locals init (
            [0] class MysteryMachine.ScrappyDoo/'<GetIEnumerableItems>d__0',
            [1] class [mscorlib]System.Collections.Generic.IEnumerable`1<object>
        )
    
        IL_0000: ldc.i4.s -2
        IL_0002: newobj instance void MysteryMachine.ScrappyDoo/'<GetIEnumerableItems>d__0'::.ctor(int32)
        IL_0007: stloc.0
        IL_0008: ldloc.0
        IL_0009: ldarg.0
        IL_000a: stfld class MysteryMachine.ScrappyDoo MysteryMachine.ScrappyDoo/'<GetIEnumerableItems>d__0'::'<>4__this'
        IL_000f: ldloc.0
        IL_0010: stloc.1
        IL_0011: br.s IL_0013
    
        IL_0013: ldloc.1
        IL_0014: ret
    } // end of method ScrappyDoo::GetIEnumerableItems
    
  • Точно так же IL для метода Main одинаков для двоичных файлов VS2010 и VS2012:

    .method public hidebysig static 
        void Main (
            string[] args
        ) cil managed 
    {
        // Method begins at RVA 0x2050
        // Code size 69 (0x45)
        .maxstack 2
        .entrypoint
        .locals init (
            [0] object item,
            [1] class [mscorlib]System.Collections.Generic.IEnumerator`1<object> CS$5$0000,
            [2] bool CS$4$0001
        )
    
        IL_0000: nop
        IL_0001: nop
        IL_0002: newobj instance void MysteryMachine.ScrappyDoo::.ctor()
        IL_0007: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerable`1<!0> class MysteryMachine.ScoobyDoo`1<object>::get_GetIEnumerableItems()
        IL_000c: callvirt instance class [mscorlib]System.Collections.Generic.IEnumerator`1<!0> class [mscorlib]System.Collections.Generic.IEnumerable`1<object>::GetEnumerator()
        IL_0011: stloc.1
        .try
        {
            IL_0012: br.s IL_0027
            // loop start (head: IL_0027)
                IL_0014: ldloc.1
                IL_0015: callvirt instance !0 class [mscorlib]System.Collections.Generic.IEnumerator`1<object>::get_Current()
                IL_001a: stloc.0
                IL_001b: ldloc.0
                IL_001c: callvirt instance string [mscorlib]System.Object::ToString()
                IL_0021: call void [mscorlib]System.Console::WriteLine(string)
                IL_0026: nop
    
                IL_0027: ldloc.1
                IL_0028: callvirt instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
                IL_002d: stloc.2
                IL_002e: ldloc.2
                IL_002f: brtrue.s IL_0014
            // end loop
    
            IL_0031: leave.s IL_0043
        } // end .try
        finally
        {
            IL_0033: ldloc.1
            IL_0034: ldnull
            IL_0035: ceq
            IL_0037: stloc.2
            IL_0038: ldloc.2
            IL_0039: brtrue.s IL_0042
    
            IL_003b: ldloc.1
            IL_003c: callvirt instance void [mscorlib]System.IDisposable::Dispose()
            IL_0041: nop
    
            IL_0042: endfinally
        } // end handler
    
        IL_0043: nop
        IL_0044: ret
    } // end of method Program::Main
    
  • В двоичных файлах, скомпилированных VS2012, есть метод <>n__FabricatedMethod4, которого нет в VS2010:

    VS2012:

    FabricatedMethod в Visual Studio 2012

    VS2010:

    FabricatedMethod в Visual Studio 2010

    ILSpy не может проверить IL на предмет «сломанного» метода в двоичных файлах VS2010 и обнаруживает следующее исключение:

    System.NullReferenceException: Object reference not set to an instance of an object.
       at ICSharpCode.Decompiler.Disassembler.DisassemblerHelpers.WriteTo(TypeReference type, ITextOutput writer, ILNameSyntax syntax)
       at ICSharpCode.Decompiler.Disassembler.DisassemblerHelpers.WriteTo(TypeReference type, ITextOutput writer, ILNameSyntax syntax)
       at ICSharpCode.Decompiler.Disassembler.ReflectionDisassembler.DisassembleMethodInternal(MethodDefinition method)
       at ICSharpCode.ILSpy.TextView.DecompilerTextView.DecompileNodes(DecompilationContext context, ITextOutput textOutput)
       at ICSharpCode.ILSpy.TextView.DecompilerTextView.<>c__DisplayClass31_0.<DecompileAsync>b__0()
    

    Точно так же он не может просматривать содержимое метода ScrappyDoo.GetIEnumerableItems как C # и показывает аналогичное исключение:

    ICSharpCode.Decompiler.DecompilerException: Error decompiling System.Collections.Generic.IEnumerable`1<System.Object> MysteryMachine.ScrappyDoo::GetIEnumerableItems()
     ---> System.NullReferenceException: Object reference not set to an instance of an object.
       // stack trace elided
    
  • При проверке двоичных файлов с помощью DotPeek декомпилированный код для кода, скомпилированного с VS2010 и VS2012, отличается выражение оператора foreach:

    VS2010:

    // ISSUE: reference to a compiler-generated method
    foreach (object obj in (IEnumerable<object>) this.<>n__FabricatedMethod4())
      yield return obj;
    

    VS2012 (обратите внимание, что декомпилированный C #, как и ожидалось, совпадает с исходным):

    foreach (object obj in base.GetIEnumerableItems())
      yield return obj;
    
  • Проблема не решается изменением метода на свойство или добавлением дополнительной логики либо в базу, либо в переопределение.

  • Изменение базового метода на возврат IEnumerable<object> вместо IEnumerable<T> устраняет проблему (в этом надуманном случае), но это неприемлемое решение.

  • Проблема возникает при ориентации на .NET 2.0, .NET 3.0, .NET 3.5 и .NET 4 в VS2010. При компиляции с VS2012 и более поздними версиями целевая версия платформы не имеет значения, и код ведет себя так, как ожидалось.

  • Я знаю, что Visual Studio не компилирует код - он просто вызывает MSBuild (или Roslyn), но эта проблема по-прежнему существует на машине с установленными VS2010 и VS2012: при запуске кода в VS2010 проблема сохраняется, а при работе в VS2012 - нет. Установив для параметра подробности вывода сборки значение «Диагностика», я обнаружил, что как VS2010, так и VS2012 используют одни и те же двоичные файлы MSBuild в

    C:\Windows\Microsoft.NET\Framework\v4.0.30319
    
  • Проблема не возникает в VS2015 (с использованием Roslyn для компиляции) - IL другой, но я думаю, этого следовало ожидать.

  • Мне нужно использовать Visual Studio 2010, поскольку там, где я работаю, мы занимаемся разработкой для Windows XP, которая поддерживает только версии 2010 и ниже.

  • PEVerify дает следующий вывод для кода, скомпилированного VS2010:

    > peverify MysteryMachine2010.exe
    
    Microsoft (R) .NET Framework PE Verifier.  Version  4.0.30319.0
    Copyright (c) Microsoft Corporation.  All rights reserved.
    
    [IL]: Error: [MysteryMachine2010.exe : MysteryMachine.ScrappyDoo::<>n__FabricatedMethod4]  [HRESULT 0x8007000B] - An attempt was made to load a program with an incorrect format.
    
    [IL]: Error: [MysteryMachine2010.exe : MysteryMachine.ScrappyDoo+<getIEnumerableItems>d__0::MoveNext]  [HRESULT 0x8007000B] - An attempt was made to load a program with an incorrect format.
    
    2 Error(s) Verifying MysteryMachine2010.exe
    

    тогда как для двоичных файлов, скомпилированных через VS2012 и выше, результат, как и ожидалось:

    > peverify "MysteryMachine2012.exe"
    
    Microsoft (R) .NET Framework PE Verifier.  Version  4.0.30319.0
    Copyright (c) Microsoft Corporation.  All rights reserved.
    
    All Classes and Methods in MysteryMachine2012.exe Verified.
    
  • При запуске кода, скомпилированного с VS2010, из командной строки получается следующий вывод:

    > MysteryMachine2010.exe
    
    Unhandled Exception: System.BadImageFormatException: An attempt was made to load a program with an incorrect format. (Exception from HRESULT: 0x8007000B)
       at MysteryMachine.ScrappyDoo.<getIEnumerableItems>d__0.MoveNext()
       at MysteryMachine.Program.Main(String[] args) in MysteryMachine\Program.cs:line 11
    

Мой актуальный вопрос

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


person Wai Ha Lee    schedule 20.09.2016    source источник
comment
Несомненно, это ошибка старого компилятора. Если компилятор компилирует его, он должен аварийно завершить работу с исключением, относящимся к коду. Исключение BadImageFormatException из кода, вывод которого компилятор является ошибкой, простой и понятный. Это должно ответить, почему это так. Что касается того, как этого избежать, я не знаю, кроме как просто изменить код. Вы можете зайти в Microsoft Connect и сообщить об ошибке, но я серьезно сомневаюсь, что они вернутся и исправят эти старые компиляторы на этом уровне, это, вероятно, не является широко распространенной проблемой. Я обдумываю конкретное решение, позволяющее избежать этого.   -  person Lasse V. Karlsen    schedule 20.09.2016
comment
Что, если базовый класс не использует yield return, это что-то меняет?   -  person Lasse V. Karlsen    schedule 20.09.2016
comment
@ LasseV.Karlsen - если я изменю базу, скажем, на yield break, throw или любой другой компилируемый код, возникнет та же проблема. Базовый метод никогда не выполняется.   -  person Wai Ha Lee    schedule 20.09.2016
comment
Итак, мы знаем, что проблема не связана с методом перечислителя как таковым, например, когда компилятор запускается из-за генерируемого конечного автомата. Затем мы вернемся к замене универсального метода конкретным методом.   -  person Lasse V. Karlsen    schedule 20.09.2016
comment
@ LasseV.Karlsen - Я подозревал, что Microsoft вряд ли вернется к шестилетней версии Visual Studio и исправит ошибку, которую (предположительно) они явно исправили в более поздней версии. Я также подозреваю, что если у меня есть тип возврата базового List<T> и добавлено переопределение в его список, все будет в порядке. Думаю, это было бы не так ужасно.   -  person Wai Ha Lee    schedule 20.09.2016
comment
Что peverify говорит о получившейся сборке? Возникает ли эта ошибка, когда код запускается вне VS 2010 или только при его отладке? Я ожидал, что BadImageFormatException будет выброшен только при первой загрузке сборки, но в вашем MCVE не происходит никакой дополнительной нагрузки, если VS не делает что-то интересное в фоновом режиме. В том смысле, исчезнет ли проблема, если в параметрах отладки будет включен / выключен процесс хостинга?   -  person Jeroen Mostert    schedule 21.09.2016
comment
@JeroenMostert - Я отредактировал вопрос, чтобы ответить на ваши вопросы: (a) PEVerify не любит плохую сборку. (б) Проблема возникает внутри и вне VS2010. (c) Проблема все еще возникает, если я выключу процесс хостинга VS.   -  person Wai Ha Lee    schedule 21.09.2016
comment
Тот факт, что peverify также дает исключение, очень странно. Его вся цель - диагностика плохого IL, поэтому он не должен жаловаться на плохие изображения, а должен объяснять, что с ними не так. Это говорит о том, что проблема очень низкого уровня. К сожалению, у меня больше нет VS 2010, иначе было бы интересно посмотреть. Что касается избежания проблемы, поскольку она, похоже, связана с конечным автоматом, созданным VS, отказ от использования итератора (в производном классе) должен решить ее. Если итератор не слишком сложен, реализовать IEnumerable самостоятельно не сложно.   -  person Jeroen Mostert    schedule 21.09.2016


Ответы (1)


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

Переместите реализацию итератора в метод, который не является виртуальным / переопределенным.

public override IEnumerable<object> GetIEnumerableItems()
{
    return getIEnumerableItems();
}

IEnumerable<object> getIEnumerableItems() 
{
    foreach ( var item in base.GetIEnumerableItems() )
        yield return item;
}

Перенести базовый вызов из итератора, очевидный способ

public override IEnumerable<object> GetIEnumerableItems()
{
    foreach ( var item in baseItems() )
    {
        yield return item;
    }
}

IEnumerable<object> baseItems() 
{
    return base.GetIEnumerableItems();
}

Этому потенциально может помешать встраивание, но я не думаю, что компилятор будет беспокоиться (традиционно такие вещи оставлены на уровне IL).

Переместите базовый вызов из итератора, задействованным способом

public override IEnumerable<object> GetIEnumerableItems()
{
    return getIEnumerableItems(base.GetIEnumerableItems());
}

IEnumerable<object> getIEnumerableItems(IEnumerable<object> baseItems) 
{
    foreach ( var item in baseItems )
        yield return item;
}

Отказ от ответственности: ничего из этого не тестируется из-за отсутствия установки VS 2010.

person Jeroen Mostert    schedule 21.09.2016
comment
Первый способ не сработал, а вот второй и третий - сработали. Проблема со вторым и третьим методами заключается в том, что они требуют дополнительных методов для каждого переопределения базового метода - в моем случае существует около 20 переопределений базового метода. Метод 2, по крайней мере, имеет итератор в фактическом переопределении, что, вероятно, предпочтительнее, потому что очевидно, куда должно идти больше операторов yield return. - person Wai Ha Lee; 22.09.2016
comment
Я принимаю ваш ответ, поскольку вы предлагаете способы решения этой проблемы. - person Wai Ha Lee; 22.09.2016
comment
@WaiHaLee: Я действительно ждал ваших отзывов, прежде чем продолжить. На какую версию .NET вы нацеливаетесь? Если хотя бы 3.5, то что также может решить проблему, это base.GetEnumerableItems().AsEnumerable(). - person Jeroen Mostert; 22.09.2016
comment
У AsEnumerable() была такая же проблема (как и у _2 _ / _ 3_): комбинация использования base и yield return вызывает проблему, которая (я думаю) является причиной того, что 2 и 3 работают, а 1 - нет. - person Wai Ha Lee; 22.09.2016
comment
@WaiHaLee: хорошо, еще кое-что. Не могли бы вы попробовать определить protected static IEnumerable<U> unwrap<U>(Func<IEnumerable<U>> baseItems) { return baseItems(); } в ScoobyDoo, а затем вызвать foreach ( var item in unwrap(() => base.GetIEnumerableItems())) в ScrappyDoo? Даже если это сработает, я не уверен, что мне это нужно как решение (трудно читать, трудно понять), но это уменьшит изменение в производных классах до одной строки. - person Jeroen Mostert; 22.09.2016
comment
Никаких кубиков. Тем не менее, BadImageFormatException теперь появляется на return baseItems(), а не на in из-за передачи Func для выполнения работы. - person Wai Ha Lee; 22.09.2016
comment
@WaiHaLee: Вау. Хорошо, я действительно не понимаю, в чем заключается проблема компилятора, и я думаю, что тоже не хочу знать. Я сожалею о вашей потере. :-) Лучшее, что я могу предложить, - это определить фрагмент кода для шаблона. - person Jeroen Mostert; 22.09.2016
comment
Ммкей, я попробовал несколько обходных путей, но без особого успеха - спасибо за попытку. :) - person Wai Ha Lee; 22.09.2016