Що насправді відбувається при спробі {return x; } нарешті {x = null; } заява?


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: нарешті виконується після обчислення поверненої вартості і перед тим, як дійсно повернутися з funczion ???
мммммммм

"блок оброблених винятком" Я думаю, що цей сценарій не має нічого спільного з винятками та обробкою виключень. Йдеться про те, як .NET реалізує нарешті конструкцію захисту ресурсів .
g.pickardou

361

Зрештою оператор виконується, але значення повернення не впливає. Порядок виконання:

  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";
        }
    }
}

Це друкує "спробувати" (тому що це повернуто), а потім "нарешті", тому що це нове значення x.

Звичайно, якщо ми повертаємо посилання на об'єкт, що змінюється (наприклад, StringBuilder), то будь-які зміни, внесені до об'єкта в остаточному блоці, будуть помітні при поверненні - це не вплинуло на саме значення повернення (що є лише довідник).


Мені хотілося б запитати, чи є можливість у візуальній студії побачити створений проміжний мову (IL) для написаного коду C # на момент виконання ....
Enigma State

Виняток "якщо ми повертаємо посилання на об'єкт, що змінюється (наприклад, StringBuilder), то будь-які зміни, внесені до об'єкта в остаточному блоці, будуть помітні при поверненні", якщо об'єкт StringBuilder встановлений на нульовому рівні в остаточному блоці, у такому випадку повертається ненульовий об’єкт.
Нік

4
@ Nick: Це не зміна об'єкта - це зміна змінної . Це не впливає на об'єкт, на який попереднє значення змінної взагалі згадувалося. Так що ні, це не виняток.
Джон Скіт

3
@Skeet Це означає "повернення заяви повертає копію"?
прабхакаран

4
@prabhakaran: Добре оцінює вираз у точці returnзаяви, і це значення буде повернуто. Вираз не оцінюється як контроль залишає метод.
Джон Скіт

19

Кінцевий пункт виконується після оператора return, але перед тим, як фактично повернутися з функції. Думаю, це мало стосується безпеки ниток. Це не злом - нарешті гарантовано завжди працювати незалежно від того, що ви робите у своєму блоці спробу або блоці улову.


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");
        }
    }
}

Посилання, яке повертається, вже було оцінено до того, як локальній змінній буде призначено нове посилання в остаточному блоці.

Виконання по суті:

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();
        }
    }
}

Друга річ, яку слід врахувати про спробу повернення, нарешті, це те, що параметри, передані "за посиланням", все ще можуть бути змінені після повернення. Оцінено лише повернене значення і зберігається у тимчасовій змінній, що чекає повернення, будь-які інші змінні все ще модифікуються звичайним чином. Контракт з параметром out може навіть не виконати, поки остаточно не перекриє цей шлях.

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;
        }
    }
}

Як і будь-яка інша конструкція потоку, "спробуйте повернутися, нарешті", має своє місце і може дозволити отримання більш чистого вигляду коду, ніж написання структури, яку він фактично компілює. Але його потрібно обережно використовувати, щоб уникнути гетчі.


4

Якщо xце локальна змінна, я не бачу сенсу, оскільки xвона буде ефективно встановлена ​​на нуль у будь-якому випадку, коли метод вийшов, а значення повернутого значення не буде нульовим (оскільки воно було розміщене в реєстрі перед викликом для встановлення xнульовим).

Я бачу, як це відбувається, лише якщо ви хочете гарантувати зміну значення поля після повернення (і після визначення значення повернення).


Якщо тільки локальну змінну не захопить також делегат :)
Йон Скіт

Потім відбувається закриття, і об'єкт не може бути зібраний сміттям, оскільки досі є посилання.
рекурсивна

Але я досі не бачу, чому ви б використовували локальний var у делеґаті, якщо ви не збиралися використовувати його значення.
рекурсивна
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.