Apa yang sebenarnya terjadi dalam percobaan {return x; } akhirnya {x = null; } pernyataan?

259

Saya melihat tip ini dalam pertanyaan lain dan bertanya-tanya apakah seseorang dapat menjelaskan kepada saya bagaimana cara kerjanya?

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

Maksudku, apakah finallyklausul benar-benar mengeksekusi setelah itu returnpernyataan? Bagaimana tidak amannya kode ini? Bisakah Anda memikirkan try-finallyperetasan tambahan yang dapat dilakukan dengan peretasan ini ?

Dmitri Nesteruk
sumber

Jawaban:

235

Tidak - pada level IL Anda tidak dapat kembali dari dalam blok penanganan-terkecuali. Ini pada dasarnya menyimpannya dalam variabel dan kembali setelahnya

yaitu mirip dengan:

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

misalnya (menggunakan reflektor):

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

mengkompilasi ke:

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

Ini pada dasarnya mendeklarasikan variabel lokal ( CS$1$0000), menempatkan nilai ke dalam variabel (di dalam blok yang ditangani), kemudian setelah keluar dari blok memuat variabel, kemudian mengembalikannya. Reflektor menjadikan ini sebagai:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
Marc Gravell
sumber
10
Bukankah ini persis seperti yang dikatakan ocdedio: akhirnya dieksekusi setelah perhitungan nilai kembali dan sebelum benar-benar kembali dari fungsi ???
mmmmmmmm
"blok penanganan pengecualian" Saya pikir skenario ini tidak ada hubungannya dengan pengecualian dan penanganan pengecualian. Ini adalah tentang bagaimana .NET mengimplementasikan konstruksi penjaga sumber daya akhirnya .
g.pickardou
361

Pernyataan terakhir dijalankan, tetapi nilai kembali tidak terpengaruh. Urutan eksekusi adalah:

  1. Kode sebelum pernyataan pengembalian dieksekusi
  2. Pernyataan ekspresi balik dievaluasi
  3. akhirnya blok dieksekusi
  4. Hasil yang dievaluasi pada langkah 2 dikembalikan

Berikut ini adalah program singkat untuk diperagakan:

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

Ini mencetak "coba" (karena itulah yang dikembalikan) dan kemudian "akhirnya" karena itulah nilai baru x.

Tentu saja, jika kita mengembalikan referensi ke objek yang dapat diubah (mis. StringBuilder) maka setiap perubahan yang dilakukan pada objek di blok akhirnya akan terlihat saat kembali - ini tidak memengaruhi nilai pengembalian itu sendiri (yang hanya merupakan referensi).

Jon Skeet
sumber
Saya ingin bertanya, Apakah ada opsi di studio visual untuk melihat bahasa Intermediate (IL) yang dihasilkan untuk kode C # yang ditulis pada saat eksekusi ....
Enigma State
Pengecualian untuk "jika kita mengembalikan referensi ke objek yang bisa diubah (mis. StringBuilder) maka setiap perubahan yang dilakukan pada objek di blok akhirnya akan terlihat saat kembali" adalah jika objek StringBuilder diatur ke nol di blok terakhir, dalam hal ini objek non-null dikembalikan.
Nick
4
@Nick: Itu bukan perubahan ke objek - itu adalah perubahan variabel . Itu tidak mempengaruhi objek yang nilai variabel sebelumnya dirujuk sama sekali. Jadi tidak, itu bukan pengecualian.
Jon Skeet
3
@Skeet Apakah itu berarti "pernyataan pengembalian mengembalikan salinan"?
prabhakaran
4
@prabhakaran: Ya itu mengevaluasi ekspresi pada titik returnpernyataan, dan nilai itu akan dikembalikan. Ekspresi tidak dievaluasi ketika kontrol meninggalkan metode.
Jon Skeet
19

Klausa akhirnya dieksekusi setelah pernyataan kembali tetapi sebelum benar-benar kembali dari fungsi. Ini tidak ada hubungannya dengan keselamatan benang, saya pikir. Ini bukan hack - yang akhirnya dijamin akan selalu berjalan tidak peduli apa yang Anda lakukan di blok percobaan atau blok tangkapan Anda.

Otávio Décio
sumber
13

Menambah jawaban yang diberikan oleh Marc Gravell dan Jon Skeet, penting untuk mencatat objek dan tipe referensi lainnya berperilaku sama ketika dikembalikan tetapi memiliki beberapa perbedaan.

"Apa" yang dikembalikan mengikuti logika yang sama dengan tipe sederhana:

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

Referensi yang dikembalikan sudah dievaluasi sebelum variabel lokal diberi referensi baru di blok akhirnya.

Eksekusi pada dasarnya:

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

Perbedaannya adalah masih mungkin untuk memodifikasi tipe yang bisa berubah menggunakan properti / metode objek yang dapat mengakibatkan perilaku tak terduga jika Anda tidak hati-hati.

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

Hal kedua yang perlu dipertimbangkan tentang try-return-akhirnya adalah bahwa parameter yang lulus "dengan referensi" masih dapat dimodifikasi setelah pengembalian. Hanya nilai kembali yang telah dievaluasi dan disimpan dalam variabel sementara yang menunggu untuk dikembalikan, variabel lainnya masih dimodifikasi dengan cara normal. Kontrak parameter keluar bahkan bisa tidak terpenuhi sampai akhirnya memblokir dengan cara ini.

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

Seperti aliran lain membangun "coba-kembali-akhirnya" memiliki tempatnya dan dapat memungkinkan untuk mencari kode yang lebih bersih daripada menulis struktur yang sebenarnya dikompilasi. Tetapi harus digunakan dengan hati-hati untuk menghindari gotcha.

Arkaine55
sumber
4

Jika xmerupakan variabel lokal, saya tidak melihat intinya, karena xakan secara efektif diatur ke nol pula ketika metode ini keluar dan nilai nilai kembali tidak nol (karena ditempatkan dalam register sebelum panggilan untuk mengatur xke nol).

Saya hanya dapat melihat hal ini terjadi jika Anda ingin menjamin perubahan nilai bidang setelah pengembalian (dan setelah nilai kembali ditentukan).

casperOne
sumber
Kecuali variabel lokal juga ditangkap oleh delegasi :)
Jon Skeet
Lalu ada penutupan, dan objek tidak bisa dikumpulkan sampah, karena masih ada referensi.
rekursif
Tapi saya masih tidak mengerti mengapa Anda akan menggunakan var lokal di delegasi kecuali Anda bermaksud menggunakan nilainya.
rekursif