Различия в декомпиляторе.NET между "использованием" и "попыткой... наконец"

Учитывая следующий код C#, в котором метод Dispose вызывается двумя различными способами:

class Disposable : IDisposable
{
    public void Dispose()
    {
    }
}

class Program
{
    static void Main(string[] args)
    {
        using (var disposable1 = new Disposable())
        {
            Console.WriteLine("using");
        }

        var disposable2 = new Disposable();
        try
        {
            Console.WriteLine("try");
        }
        finally
        {
            if (disposable2 != null)
                ((IDisposable)disposable2).Dispose();
        }
    }
}

После компиляции с использованием конфигурации выпуска и дизассемблирования с помощью ildasm MSIL выглядит следующим образом:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       57 (0x39)
  .maxstack  1
  .locals init ([0] class ConsoleApplication9.Disposable disposable2,
           [1] class ConsoleApplication9.Disposable disposable1)
  IL_0000:  newobj     instance void ConsoleApplication9.Disposable::.ctor()
  IL_0005:  stloc.1
  .try
  {
    IL_0006:  ldstr      "using"
    IL_000b:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0010:  leave.s    IL_001c
  }  // end .try
  finally
  {
    IL_0012:  ldloc.1
    IL_0013:  brfalse.s  IL_001b
    IL_0015:  ldloc.1
    IL_0016:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_001b:  endfinally
  }  // end handler
  IL_001c:  newobj     instance void ConsoleApplication9.Disposable::.ctor()
  IL_0021:  stloc.0
  .try
  {
    IL_0022:  ldstr      "try"
    IL_0027:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_002c:  leave.s    IL_0038
  }  // end .try
  finally
  {
    IL_002e:  ldloc.0
    IL_002f:  brfalse.s  IL_0037
    IL_0031:  ldloc.0
    IL_0032:  callvirt   instance void [mscorlib]System.IDisposable::Dispose()
    IL_0037:  endfinally
  }  // end handler
  IL_0038:  ret
} // end of method Program::Main

Как декомпилятор.NET, такой как DotPeek или JustDecompile, делает разницу между использованием и попыткой... наконец?

2 ответа

Решение

Это не имеет значения на самом деле. Как говорит Марк в комментариях - если вы напишите тот же код, который компилятор сгенерирует для using - декомпилятор не сможет изменить ситуацию.

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

Чтобы DotPeek не использовал ваши файлы с исходным кодом, перейдите в Инструменты> Параметры> Декомпилятор и снимите флажок "Использовать источники из файлов символов, когда они доступны". Затем скомпилируйте ваш код в Release и обратите внимание, что DotPeek декомпилирует оба оператора как using,

Как декомпилятор.NET, такой как DotPeek или JustDecompile, делает разницу между использованием и попыткой... наконец?

Декомпиляторы в основном работают над сопоставлением с образцом. Обычно IL переводится в простейшее эквивалентное представление, возможное на целевом языке (в данном случае, на C#). Эта модель кода затем проходит серию преобразований, которые пытаются сопоставить последовательности кода с хорошо известными шаблонами. С помощью отладочной сборки ILSpy вы можете фактически просмотреть выходные данные на разных этапах этого конвейера.

Конвейер декомпилятора может включать преобразования, такие как перезапись цикла. Переписчик цикла может восстановить for петли, ища while циклы, которым предшествует инициализатор переменной и которые также содержат общие итерационные операторы перед каждым обратным фронтом. Когда такой цикл обнаружен, он переписывается как более сжатый for петля. Он не знает, что оригинальный код на самом деле содержал for петля; он просто пытается найти наиболее краткий способ представления кода при сохранении правильности.

Аналогичным образом, using переписчик будет искать try/finally блоки, где finally содержит простую нулевую проверку и Dispose() позвоните, а затем переписать те, как using блоки, которые являются более краткими, но все еще правильными в соответствии со спецификацией языка. Декомпилятор не знает, что код содержал using блок, но так как почти никто не использует явное try/finally форма, результаты, как правило, соответствуют исходному источнику.

Другие вопросы по тегам