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

Учитывая следующий код С#, в котором метод 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, делает разницу между использованием и попыткой... наконец?

Ответ 1

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

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

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

Ответ 2

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

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

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

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