parameter tipe struct keluar tidak perlu ditugaskan

9

Saya telah memperhatikan beberapa perilaku aneh dalam kode saya ketika secara tidak sengaja mengomentari suatu baris dalam suatu fungsi selama tinjauan kode. Sangat sulit untuk mereproduksi tetapi saya akan menggambarkan contoh serupa di sini.

Saya mendapat kelas tes ini:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

tidak ada tugas untuk Email GetOutyang biasanya akan menyebabkan kesalahan:

Parameter email 'out' harus ditetapkan sebelum kontrol meninggalkan metode saat ini

Namun jika EmailAddress berada dalam struct dalam majelis terpisah tidak ada kesalahan yang dibuat dan semuanya mengkompilasi dengan baik.

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

Mengapa kompiler tidak memberlakukan bahwa Email harus ditetapkan? Mengapa kode ini dikompilasi jika struct dibuat dalam rakitan yang terpisah, tetapi tidak mengkompilasi jika struct didefinisikan dalam rakitan yang ada?

johnny 5
sumber
2
Jika Anda menggunakan kelas, Anda harus 'membuat' instance objek. Itu tidak diperlukan untuk struct. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… (cari teks ini di halaman itu secara khusus: Tidak seperti kelas, struct dapat di-instantiate tanpa menggunakan operato baru)
Dortimer
1
Segera setelah struct Anjing Anda mendapatkan variabel, variabel itu tidak dapat dikompilasi :)
André Sanson
Dalam contoh ini, dengan struct Dog{}, semuanya baik-baik saja.
Henk Holterman
2
@ johnny5 Lalu tunjukkan contohnya.
André Sanson
1
OKE, ini menarik. Direproduksi dengan aplikasi Core 3 Console dan lib kelas standar.
Henk Holterman

Jawaban:

12

TLDR: Ini adalah bug yang dikenal lama. Saya pertama kali menulis tentang hal itu pada tahun 2010:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

Ini tidak berbahaya dan Anda dapat dengan aman mengabaikannya, dan memberi selamat pada diri sendiri karena menemukan bug yang agak kabur.

Mengapa kompiler tidak menjalankan yang Emailharus ditentukan?

Oh, benar, dengan cara. Itu hanya memiliki ide yang salah tentang kondisi apa yang menyiratkan bahwa variabel pasti ditugaskan, seperti yang akan kita lihat.

Mengapa kode ini dikompilasi jika struct dibuat dalam rakitan yang terpisah, tetapi tidak mengkompilasi jika struct didefinisikan dalam rakitan yang ada?

Itulah inti dari bug. Bug adalah konsekuensi dari persimpangan bagaimana C # compiler melakukan pengecekan tugas yang pasti pada struct dan bagaimana kompiler memuat metadata dari perpustakaan.

Pertimbangkan ini:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

OKE, pada titik ini apa yang kita ketahui? fadalah alias untuk variabel tipe Foo, jadi penyimpanan telah dialokasikan dan jelas setidaknya dalam keadaan keluar dari alokasi penyimpanan. Jika ada nilai yang ditempatkan dalam variabel oleh pemanggil, nilai itu ada di sana.

Apa yang kami butuhkan? Kami mengharuskan yang fpasti ditugaskan pada setiap titik di mana kontrol berjalan Mnormal. Jadi Anda akan mengharapkan sesuatu seperti:

void M(out Foo f)
{
  f = new Foo();
}

yang menetapkan f.xdan f.yke nilai standarnya. Tapi bagaimana dengan ini?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

Itu juga harus baik-baik saja. Tapi, dan inilah kickernya, mengapa kita perlu menetapkan nilai default hanya untuk melenyapkannya beberapa saat kemudian? Pemeriksa penugasan tugas pasti C # memeriksa untuk melihat apakah setiap bidang ditugaskan! Ini legal:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

Dan mengapa itu tidak legal? Ini adalah tipe nilai. fadalah variabel, dan sudah berisi nilai jenis yang valid Foo, jadi mari kita atur bidang, dan kita selesai, kan?

Baik. Jadi, apa masalahnya?

Bug yang Anda temukan adalah: sebagai penghematan biaya, kompiler C # tidak memuat metadata untuk bidang pribadi struct yang ada di perpustakaan yang dirujuk . Metadata itu bisa sangat besar , dan itu akan memperlambat kompiler untuk kemenangan yang sangat kecil untuk memuat semuanya ke dalam memori setiap waktu.

Dan sekarang Anda harus dapat menyimpulkan penyebab bug yang Anda temukan. Ketika kompilator memeriksa untuk melihat apakah parameter keluar ditetapkan dengan pasti, ia membandingkan jumlah bidang yang diketahui dengan jumlah bidang yang ditentukan diinisialisasi dan dalam kasus Anda hanya tahu tentang bidang publik nol karena metadata bidang pribadi tidak dimuat . Kompilator menyimpulkan "nol bidang wajib diisi, nol bidang diinisialisasi, kami baik."

Seperti yang saya katakan, bug ini telah ada selama lebih dari satu dekade dan orang-orang seperti Anda kadang-kadang menemukan kembali dan melaporkannya. Ini tidak berbahaya, dan tidak mungkin diperbaiki karena memperbaikinya hampir tidak menguntungkan tetapi biaya kinerja yang besar.

Dan tentu saja bug tersebut tidak repro untuk bidang priv dari struct yang ada dalam kode sumber dalam proyek Anda, karena jelas kompiler sudah memiliki informasi tentang bidang pribadi yang ada.

Eric Lippert
sumber
@ johnny5: Anda seharusnya tidak mendapatkan kesalahan. Lihat dotnetfiddle.net/ZEKiUk . Bisakah Anda memposting repro sederhana?
Eric Lippert
1
Terima kasih untuk biola itu karena saya mendefinisikan x dan y sebagai properti, bukan anggota
johnny 5
1
@ johnny5: Jika Anda baru saja mendefinisikan properti gaya C # 1.0 yang normal kemudian dari perspektif pemeriksa penugasan yang pasti, itu adalah metode, bukan bidang. Jika Anda mendefinisikan properti otomatis gaya C # 3.0+, kompiler tahu bahwa ada bidang pribadi yang mendukungnya; aturan untuk penugasan yang pasti untuk hal itu telah diubah selama bertahun-tahun dan saya sekarang tidak mengingat aturan yang tepat.
Eric Lippert
Jika Anda menggunakan System.TimeSpanbukan, kesalahan memang datang: error CS0269: Use of unassigned out parameter 'email'dan error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. Hanya ada satu bidang non-statis TimeSpan, yaitu _ticks. Ini internaluntuk mscorlib perakitannya. Apakah majelis ini istimewa? Sama dengan System.DateTime, dan bidangnya adalahprivate
Jeppe Stig Nielsen
@JeppeStigNielsen: Saya tidak tahu ada apa dengan itu! Jika Anda mengetahuinya, beri tahu saya.
Eric Lippert
1

Meskipun terlihat seperti bug, itu masuk akal.

'Kesalahan yang hilang' hanya muncul saat menggunakan perpustakaan kelas. Dan perpustakaan kelas mungkin telah ditulis dalam bahasa .net lain, mis. VB.Net. 'Pelacakan penugasan pasti' adalah fitur C #, bukan kerangka kerja.

Jadi secara keseluruhan saya tidak berpikir itu bug tetapi saya tidak tahu tentang pernyataan otoritatif untuk ini.

Henk Holterman
sumber
Jika Anda mengimpor rakitan ke C #, meskipun struct dapat terletak di rakitan yang ditulis dalam bahasa lain, Kode yang menggunakannya masih dalam c # jadi tidak harus menggunakan pelacakan tugas yang pasti.
johnny 5
1
Belum tentu. C # tidak akan membiarkan Anda menggunakan variabel unitial (lokal) tetapi pada saat yang sama kerangka kerja itu akan diatur ke 0 ( default(T)). Jadi tidak ada pelanggaran keamanan memori atau sesuatu yang serupa.
Henk Holterman
3
Saya bisa membuat pernyataan otoritatif seperti itu. :) Ini adalah bug yang sudah lama diketahui.
Eric Lippert
1
@EricLippert Terima kasih, saya berharap Anda akan melihat ini di beberapa titik
johnny 5