Что на самом деле происходит в try {return x; } наконец {х = ноль; } заявление?

259

Я видел этот совет в другом вопросе и задавался вопросом, может ли кто-нибудь объяснить мне, как на земле это работает?

try { return x; } finally { x = null; }

Я имею в виду, делает finallyположение действительно выполнить после того, как в returnзаявлении? Насколько небезопасен этот код? Можете ли вы вспомнить какие-либо дополнительные хакеры, которые можно сделать с помощью этого try-finallyхака?

Дмитрий Нестерук
источник

Ответы:

235

Нет - на уровне IL вы не можете вернуться изнутри блока с обработкой исключений. По сути, он хранит его в переменной и возвращает потом

то есть похоже на:

int tmp;
try {
  tmp = ...
} finally {
  ...
}
return tmp;

например (с помощью отражателя):

static int Test() {
    try {
        return SomeNumber();
    } finally {
        Foo();
    }
}

компилируется в:

.method private hidebysig static int32 Test() cil managed
{
    .maxstack 1
    .locals init (
        [0] int32 CS$1$0000)
    L_0000: call int32 Program::SomeNumber()
    L_0005: stloc.0 
    L_0006: leave.s L_000e
    L_0008: call void Program::Foo()
    L_000d: endfinally 
    L_000e: ldloc.0 
    L_000f: ret 
    .try L_0000 to L_0008 finally handler L_0008 to L_000e
}

Это в основном объявляет локальную переменную ( CS$1$0000), помещает значение в переменную (внутри обработанного блока), затем после выхода из блока загружает переменную, а затем возвращает ее. Отражатель отображает это как:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
Марк Гравелл
источник
10
Разве это не то, что сказал ocdedio: finally выполняется после вычисления возвращаемого значения и перед тем, как действительно вернуться из функции ???
мммммммм
«Блок обработки исключений» Я думаю, что этот сценарий не имеет ничего общего с исключениями и обработкой исключений. Это о том, как .NET реализует конструкцию окончательной защиты ресурса .
g.pickardou
361

Оператор finally выполняется, но на возвращаемое значение это не влияет. Порядок исполнения:

  1. Код перед выполнением оператора возврата
  2. Выражение в операторе возврата оценивается
  3. наконец, блок выполнен
  4. Результат, оцененный на шаге 2, возвращается

Вот короткая программа для демонстрации:

using System;

class Test
{
    static string x;

    static void Main()
    {
        Console.WriteLine(Method());
        Console.WriteLine(x);
    }

    static string Method()
    {
        try
        {
            x = "try";
            return x;
        }
        finally
        {
            x = "finally";
        }
    }
}

Это печатает «try» (потому что это то, что возвращается), а затем «finally», потому что это новое значение x.

Конечно, если мы возвращаем ссылку на изменяемый объект (например, StringBuilder), то любые изменения, внесенные в объект в блоке finally, будут видны при возврате - это не повлияло на само возвращаемое значение (которое является просто ссылка).

Джон Скит
источник
Я хотел бы спросить, есть ли какая-либо опция в Visual Studio, чтобы увидеть сгенерированный промежуточный язык (IL) для написанного кода C # во время выполнения ....
Enigma State
Исключением является «если мы возвращаем ссылку на изменяемый объект (например, StringBuilder), то любые изменения, внесенные в объект в блоке finally, будут видны при возврате», если объект StringBuilder имеет значение null в блоке finally, в этом случае возвращается ненулевой объект.
Ник
4
@Nick: Это не изменение объекта - это изменение переменной . Это не влияет на объект, на который ссылалось предыдущее значение переменной. Так что нет, это не исключение.
Джон Скит
3
@Skeet Означает ли это, что «оператор возврата возвращает копию»?
Прабхакаран
4
@prabhakaran: Хорошо, он оценивает выражение в точке returnоператора, и это значение будет возвращено. Выражение не оценивается, так как контроль покидает метод.
Джон Скит
19

Предложение finally выполняется после оператора return, но до фактического возврата из функции. Я думаю, это не имеет ничего общего с безопасностью потоков. Это не хак - гарантированно всегда будет выполняться finally, независимо от того, что вы делаете в блоке try или блоке catch.

Otávio Décio
источник
13

Добавляя к ответам, данным Марком Гравеллом и Джоном Скитом, важно отметить, что объекты и другие ссылочные типы ведут себя аналогично при возврате, но имеют некоторые различия.

Возвращаемое «Что» следует той же логике, что и простые типы:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        try {
            return ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
    }
}

Возвращаемая ссылка уже была оценена до того, как локальной переменной была назначена новая ссылка в блоке finally.

Исполнение по сути:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        Exception CS$1$0000 = null;
        try {
            CS$1$0000 = ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
        return CS$1$0000;
    }
}

Разница в том, что все еще можно изменить изменяемые типы, используя свойства / методы объекта, что может привести к неожиданному поведению, если вы не будете осторожны.

class Test2 {
    public static System.IO.MemoryStream BadStream(byte[] buffer) {
        System.IO.MemoryStream ms = new System.IO.MemoryStream(buffer);
        try {
            return ms;
        } finally {
            // Reference unchanged, Referenced Object changed
            ms.Dispose();
        }
    }
}

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

class ByRefTests {
    public static int One(out int i) {
        try {
            i = 1;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 1000;
        }
    }

    public static int Two(ref int i) {
        try {
            i = 2;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 2000;
        }
    }

    public static int Three(out int i) {
        try {
            return 3;
        } finally {
            // This is not a compile error!
            // Return value unchanged, Store new value referenced variable
            i = 3000;
        }
    }
}

Как и любая другая конструкция потока, «try-return-finally» имеет свое место и может обеспечить более чистый вид кода, чем написание структуры, в которую он фактически компилируется. Но это нужно использовать осторожно, чтобы избежать ошибок Гоча.

Arkaine55
источник
4

Если xэто локальная переменная, я не вижу смысла, так как xв любом случае будет эффективно установлено значение NULL, когда метод завершается, а значение возвращаемого значения не равно NULL (так как оно было помещено в регистр до вызова set xк нулю).

Я могу видеть, как это происходит, только если вы хотите гарантировать изменение значения поля при возврате (и после определения возвращаемого значения).

casperOne
источник
Если только локальная переменная не перехвачена делегатом :)
Джон Скит
Затем происходит замыкание, и объект не может быть подвергнут сборке мусора, потому что есть ссылка.
Рекурсивный
Но я все еще не понимаю, почему вы использовали бы локальную переменную в делегате, если вы не намеревались использовать ее значение.
Рекурсивный