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

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

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

Я имею в виду, действительно ли предложение finally выполняется после инструкции return? Как небезопасный этот код? Можете ли вы придумать какой-либо дополнительный хакер, который можно сделать w.r.t. это try-finally hack?

Ответ 1

Нет - на уровне 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;
}

Ответ 2

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

  • Выполняется оператор "Код перед возвратом"
  • Выражение выражения в возвращаемом выражении
  • выполняется окончательный блок
  • Возвращается результат, полученный на шаге 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, будут видны при возврате - это не повлияло на само возвращаемое значение (что является просто ссылкой).

Ответ 3

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

Ответ 4

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

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

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" имеет свое место и может обеспечить более чистый выглядящий код, чем запись структуры, которую он на самом деле компилируется. Но его следует использовать осторожно, чтобы избежать ошибок.

Ответ 5

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

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