Mengapa variabel lokal memerlukan inisialisasi, tetapi bidang tidak?

140

Jika saya membuat bool dalam kelas saya, hanya sesuatu seperti bool check, itu default ke false.

Ketika saya membuat bool yang sama dalam metode saya, bool check(bukan dalam kelas), saya mendapatkan kesalahan "penggunaan pemeriksaan variabel lokal yang belum ditetapkan". Mengapa?

nachime
sumber
Komentar bukan untuk diskusi panjang; percakapan ini telah dipindahkan ke obrolan .
Martijn Pieters
14
Pertanyaannya tidak jelas. Apakah "karena spesifikasi mengatakan demikian" menjadi jawaban yang dapat diterima?
Eric Lippert
4
Karena itulah yang dilakukan di Jawa ketika mereka menyalinnya. : P
Alvin Thompson

Jawaban:

177

Jawaban Yuval dan David pada dasarnya benar; menyimpulkan:

  • Penggunaan variabel lokal yang tidak ditetapkan kemungkinan merupakan bug, dan ini dapat dideteksi oleh kompiler dengan biaya rendah.
  • Penggunaan bidang atau elemen larik yang tidak ditetapkan cenderung lebih kecil bugnya, dan lebih sulit untuk mendeteksi kondisi di kompiler. Oleh karena itu kompiler tidak berusaha mendeteksi penggunaan variabel yang tidak diinisialisasi untuk bidang, dan sebagai gantinya bergantung pada inisialisasi ke nilai default untuk membuat perilaku program menjadi deterministik.

Seorang komentator untuk jawaban David bertanya mengapa tidak mungkin mendeteksi penggunaan bidang yang tidak ditetapkan melalui analisis statis; inilah poin yang ingin saya kembangkan dalam jawaban ini.

Pertama, untuk variabel apa pun, lokal atau tidak, dalam praktiknya tidak mungkin untuk menentukan dengan tepat apakah suatu variabel ditetapkan atau tidak ditetapkan. Mempertimbangkan:

bool x;
if (M()) x = true;
Console.WriteLine(x);

Pertanyaan "apakah x ditugaskan?" setara dengan "apakah M () mengembalikan true?" Sekarang, misalkan M () mengembalikan true jika Teorema Terakhir Fermat berlaku untuk semua bilangan bulat kurang dari sebelas gajillion, dan salah jika sebaliknya. Untuk menentukan apakah x sudah ditentukan, kompiler pada dasarnya harus menghasilkan bukti dari Teorema Terakhir Fermat. Kompilernya tidak sepintar itu.

Jadi apa yang dilakukan oleh kompiler untuk penduduk lokal adalah mengimplementasikan algoritma yang cepat , dan melebih - lebihkan ketika lokal tidak ditentukan secara pasti. Artinya, ia memiliki beberapa positif palsu, di mana dikatakan "Saya tidak dapat membuktikan bahwa lokal ini ditugaskan" meskipun Anda dan saya tahu itu. Sebagai contoh:

bool x;
if (N() * 0 == 0) x = true;
Console.WriteLine(x);

Misalkan N () mengembalikan bilangan bulat. Anda dan saya tahu bahwa N () * 0 akan menjadi 0, tetapi kompiler tidak tahu itu. (Catatan: kompiler C # 2.0 memang tahu itu, tapi saya menghapus optimasi itu, karena spesifikasinya tidak mengatakan bahwa kompiler tahu itu.)

Baiklah, jadi apa yang kita ketahui sejauh ini? Sangat tidak praktis bagi penduduk setempat untuk mendapatkan jawaban yang tepat, tetapi kami dapat memperkirakan terlalu tinggi tentang tidak-ditugaskan-murah dan mendapatkan hasil yang cukup bagus yang salah kaprah di sisi "membuat Anda memperbaiki program yang tidak jelas". Itu bagus. Mengapa tidak melakukan hal yang sama untuk bidang? Artinya, membuat pemeriksa tugas yang pasti yang menaksir terlalu murah?

Nah, berapa banyak cara yang ada untuk lokal diinisialisasi? Itu dapat ditugaskan dalam teks metode. Itu dapat ditugaskan dalam lambda dalam teks metode; bahwa lambda mungkin tidak pernah dipanggil, jadi tugas-tugas itu tidak relevan. Atau dapat diteruskan sebagai "keluar" ke metode lain, pada titik mana kita dapat menganggap itu ditugaskan ketika metode kembali secara normal. Itu adalah poin yang sangat jelas di mana lokal ditugaskan, dan mereka ada di sana dalam metode yang sama dengan yang dinyatakan lokal . Menentukan penugasan yang pasti untuk penduduk setempat hanya membutuhkan analisis lokal . Metode cenderung pendek - jauh lebih sedikit dari sejuta baris kode dalam suatu metode - dan karenanya menganalisis keseluruhan metode cukup cepat.

Sekarang bagaimana dengan bidang? Bidang dapat diinisialisasi dalam konstruktor tentu saja. Atau inisialisasi bidang. Atau konstruktor dapat memanggil metode contoh yang menginisialisasi bidang. Atau konstruktor dapat memanggil metode virtual yang menginisialisasi bidang. Atau konstruktor dapat memanggil metode di kelas lain , yang mungkin di perpustakaan , yang menginisialisasi bidang. Bidang statis dapat diinisialisasi dalam konstruktor statis. Bidang statis dapat diinisialisasi oleh konstruktor statis lainnya .

Pada dasarnya, penginisialisasi untuk bidang bisa berada di mana saja di seluruh program , termasuk di dalam metode virtual yang akan dideklarasikan di perpustakaan yang belum ditulis :

// Library written by BarCorp
public abstract class Bar
{
    // Derived class is responsible for initializing x.
    protected int x;
    protected abstract void InitializeX(); 
    public void M() 
    { 
       InitializeX();
       Console.WriteLine(x); 
    }
}

Apakah ada kesalahan saat mengkompilasi pustaka ini? Jika ya, bagaimana cara BarCorp memperbaiki bug? Dengan menetapkan nilai default ke x? Tapi itu yang sudah dilakukan kompiler.

Misalkan perpustakaan ini legal. Jika FooCorp menulis

public class Foo : Bar
{
    protected override void InitializeX() { } 
}

apakah itu sebuah kesalahan? Bagaimana kompiler seharusnya mengetahui hal itu? Satu-satunya cara adalah dengan melakukan analisis seluruh program yang melacak inisialisasi statis setiap bidang pada setiap jalur yang mungkin melalui program , termasuk jalur yang melibatkan pilihan metode virtual saat runtime . Masalah ini bisa sangat sulit ; itu bisa melibatkan pelaksanaan simulasi jutaan jalur kontrol. Menganalisis aliran kontrol lokal membutuhkan mikrodetik dan tergantung pada ukuran metode. Menganalisis arus kendali global dapat memakan waktu berjam-jam karena itu tergantung pada kompleksitas setiap metode dalam program dan semua perpustakaan .

Jadi mengapa tidak melakukan analisis yang lebih murah yang tidak perlu menganalisis keseluruhan program, dan hanya melebih-lebihkan bahkan lebih parah? Nah, usulkan algoritma yang berfungsi yang tidak membuatnya terlalu sulit untuk menulis program yang benar yang benar-benar dikompilasi, dan tim desain dapat mempertimbangkannya. Saya tidak tahu ada algoritma seperti itu.

Sekarang, komentator menyarankan "mengharuskan konstruktor menginisialisasi semua bidang". Itu bukan ide yang buruk. Bahkan, itu adalah ide yang tidak buruk bahwa C # sudah memiliki fitur itu untuk struct . Dibutuhkan konstruktor struct untuk menetapkan semua bidang pada saat ctor kembali secara normal; konstruktor default menginisialisasi semua bidang ke nilai default.

Bagaimana dengan kelas? Nah, bagaimana Anda tahu bahwa konstruktor telah menginisialisasi bidang ? Ctor dapat memanggil metode virtual untuk menginisialisasi bidang, dan sekarang kita kembali pada posisi yang sama seperti sebelumnya. Structs tidak memiliki kelas turunan; kelas mungkin. Apakah perpustakaan yang berisi kelas abstrak diperlukan untuk berisi konstruktor yang menginisialisasi semua bidangnya? Bagaimana kelas abstrak tahu nilai apa yang harus diinisialisasi bidang?

John menyarankan hanya melarang metode panggilan dalam aktor sebelum kolom diinisialisasi. Jadi, kesimpulannya, opsi kami adalah:

  • Jadikan idiom pemrograman yang umum, aman, dan sering digunakan ilegal.
  • Lakukan analisis seluruh program yang mahal yang membuat kompilasi membutuhkan waktu berjam-jam untuk mencari bug yang mungkin tidak ada.
  • Mengandalkan inisialisasi otomatis ke nilai default.

Tim desain memilih opsi ketiga.

Eric Lippert
sumber
1
Jawaban yang bagus, seperti biasa. Saya punya pertanyaan: Mengapa tidak secara otomatis memberikan nilai default ke variabel lokal juga? Dengan kata lain, mengapa tidak membuat bool x;setara dengan bool x = false; bahkan di dalam suatu metode ?
durron597
8
@ durron597: Karena pengalaman menunjukkan bahwa lupa untuk menetapkan nilai ke lokal mungkin adalah bug. Jika mungkin bug dan murah dan mudah dideteksi, maka ada insentif yang baik untuk membuat perilaku itu ilegal atau peringatan.
Eric Lippert
27

Ketika saya membuat bool yang sama dalam metode saya, bool check (bukan dalam kelas), saya mendapatkan kesalahan "penggunaan pemeriksaan variabel lokal yang belum ditetapkan". Mengapa?

Karena kompiler sedang mencoba untuk mencegah Anda membuat kesalahan.

Apakah menginisialisasi variabel Anda untuk falsemengubah apa pun di jalur eksekusi khusus ini? Mungkin tidak, mengingat default(bool)itu salah, tetapi memaksa Anda untuk menyadari bahwa ini sedang terjadi. Lingkungan .NET mencegah Anda mengakses "memori sampah", karena akan menginisialisasi nilai apa pun ke default. Tapi tetap saja, bayangkan ini adalah tipe referensi, dan Anda akan meneruskan nilai (nol) yang tidak diinisialisasi ke metode yang mengharapkan non-null, dan mendapatkan NRE saat runtime. Kompiler hanya berusaha mencegahnya, menerima kenyataan bahwa ini kadang-kadang menghasilkan bool b = falsepernyataan.

Eric Lippert berbicara tentang ini di sebuah posting blog :

Alasan mengapa kami ingin menjadikan ini ilegal bukanlah, seperti yang diyakini banyak orang, karena variabel lokal akan diinisialisasi ke sampah dan kami ingin melindungi Anda dari sampah. Kami memang secara otomatis menginisialisasi penduduk lokal ke nilai default mereka. (Meskipun bahasa pemrograman C dan C ++ tidak, dan dengan senang hati akan memungkinkan Anda untuk membaca sampah dari lokal yang tidak diinisialisasi.) Sebaliknya, itu karena keberadaan jalur kode seperti itu mungkin bug, dan kami ingin melempar Anda ke dalam lubang kualitas; Anda harus bekerja keras untuk menulis bug itu.

Mengapa ini tidak berlaku untuk bidang kelas? Yah, saya berasumsi garis harus ditarik di suatu tempat, dan inisialisasi variabel lokal jauh lebih mudah untuk didiagnosis dan diperbaiki, dibandingkan dengan bidang kelas. Kompilator dapat melakukan ini, tetapi pikirkan semua kemungkinan pemeriksaan yang perlu dilakukan (di mana beberapa di antaranya tidak tergantung pada kode kelas itu sendiri) untuk mengevaluasi apakah setiap bidang dalam suatu kelas diinisialisasi. Saya bukan perancang kompiler, tetapi saya yakin ini pasti akan lebih sulit karena ada banyak case yang diperhitungkan, dan harus dilakukan tepat waktu juga. Untuk setiap fitur yang Anda rancang, tulis, uji, dan gunakan, dan nilai penerapan ini sebagai lawan dari upaya yang dilakukan akan menjadi tidak layak dan rumit.

Yuval Itzchakov
sumber
"bayangkan ini adalah tipe referensi, dan Anda akan meneruskan objek yang tidak diinisialisasi ini ke metode yang mengharapkan yang diinisialisasi" Apakah maksud Anda: "bayangkan ini adalah tipe referensi dan Anda melewati default (nol) alih-alih referensi dari suatu obyek"?
Deduplicator
@Dupuplikator Ya. Metode yang mengharapkan nilai bukan nol. Mengedit bagian itu. Semoga ini lebih jelas sekarang.
Yuval Itzchakov
Saya tidak berpikir itu karena garis yang ditarik. Setiap kelas mengandaikan untuk memiliki konstruktor, setidaknya konstruktor default. Jadi, ketika Anda tetap dengan konstruktor default Anda akan mendapatkan nilai default (transparan transparan). Saat mendefinisikan konstruktor, Anda diharapkan atau seharusnya tahu apa yang Anda lakukan di dalamnya dan bidang apa yang ingin Anda inisialisasi dengan cara apa termasuk pengetahuan tentang nilai default.
Peter
Sebaliknya: Bidang dalam metode dapat dengan mendeklarasikan dan menetapkan nilai pada jalur eksekusi yang berbeda. Mungkin ada pengecualian yang mudah diawasi sampai Anda melihat dokumentasi kerangka kerja yang dapat Anda gunakan atau bahkan di bagian lain dari kode yang mungkin tidak Anda pertahankan. Ini dapat memperkenalkan jalur eksekusi yang sangat kompleks. Oleh karena itu kompiler memberi petunjuk.
Peter
@ Peter Saya tidak terlalu mengerti komentar kedua Anda. Mengenai yang pertama, tidak ada persyaratan untuk menginisialisasi bidang apa pun di dalam konstruktor. Ini adalah praktik umum . Pekerjaan kompiler bukan untuk menegakkan praktik seperti itu. Anda tidak dapat mengandalkan implementasi konstruktor yang berjalan dan berkata "baiklah, semua bidang baik untuk dijalankan". Eric banyak menguraikan dalam jawabannya tentang cara seseorang dapat menginisialisasi bidang kelas, dan menunjukkan bagaimana akan memakan waktu yang sangat lama untuk menghitung semua cara logis inisialisasi.
Yuval Itzchakov
25

Mengapa variabel lokal memerlukan inisialisasi, tetapi bidang tidak?

Jawaban singkatnya adalah bahwa kode yang mengakses variabel lokal yang tidak diinisialisasi dapat dideteksi oleh kompiler dengan cara yang dapat diandalkan, menggunakan analisis statis. Padahal ini bukan kasus bidang. Jadi kompiler memberlakukan kasus pertama, tetapi bukan yang kedua.

Mengapa variabel lokal memerlukan inisialisasi?

Ini tidak lebih dari keputusan desain bahasa C #, seperti yang dijelaskan oleh Eric Lippert . CLR dan lingkungan .NET tidak memerlukannya. VB.NET, misalnya, akan mengkompilasi dengan baik dengan variabel lokal yang tidak diinisialisasi, dan pada kenyataannya CLR menginisialisasi semua variabel yang tidak diinisialisasi ke nilai default.

Hal yang sama dapat terjadi dengan C #, tetapi desainer bahasa memilih untuk tidak melakukannya. Alasannya adalah bahwa variabel yang diinisialisasi adalah sumber bug yang sangat besar dan dengan mandat inisialisasi, kompiler membantu mengurangi kesalahan yang tidak disengaja.

Mengapa bidang tidak memerlukan inisialisasi?

Jadi mengapa inisialisasi eksplisit wajib ini tidak terjadi dengan bidang dalam kelas? Hanya karena inisialisasi eksplisit dapat terjadi selama konstruksi, melalui properti yang dipanggil oleh penginisialisasi objek, atau bahkan dengan metode yang dipanggil lama setelah acara. Kompiler tidak dapat menggunakan analisis statis untuk menentukan apakah setiap jalur yang mungkin melalui kode mengarah ke variabel yang diinisialisasi secara eksplisit sebelum kita. Melakukan kesalahan akan mengganggu, karena pengembang dapat dibiarkan dengan kode yang valid yang tidak dapat dikompilasi. Jadi C # tidak memberlakukan sama sekali dan CLR dibiarkan untuk secara otomatis menginisialisasi bidang ke nilai default jika tidak ditetapkan secara eksplisit.

Bagaimana dengan jenis koleksi?

Penegakan C # terhadap inisialisasi variabel lokal terbatas, yang sering menarik pengembang keluar. Pertimbangkan empat baris kode berikut:

string str;
var len1 = str.Length;
var array = new string[10];
var len2 = array[0].Length;

Baris kedua kode tidak akan dikompilasi, karena mencoba membaca variabel string yang tidak diinisialisasi. Baris keempat mengkompilasi kode baik-baik saja, seperti yang arraytelah diinisialisasi, tetapi hanya dengan nilai default. Karena nilai default sebuah string adalah nol, kami mendapatkan pengecualian pada saat run-time. Siapa pun yang menghabiskan waktu di sini di Stack Overflow akan tahu bahwa inkonsistensi inisialisasi eksplisit / implisit ini mengarah ke banyak sekali "Mengapa saya mendapatkan" referensi objek yang tidak disetel ke instance objek "kesalahan?" pertanyaan.

David Arno
sumber
"Kompilator tidak dapat menggunakan analisis statis untuk menentukan apakah setiap jalur yang mungkin melalui kode mengarah ke variabel yang diinisialisasi secara eksplisit sebelum kita." Saya tidak yakin ini benar. Bisakah Anda memposting contoh program yang tahan terhadap analisis statis?
John Kugelman
@ JohnKugelman, pertimbangkan kasus sederhana public interface I1 { string str {get;set;} }dan metode int f(I1 value) { return value.str.Length; }. Jika ini ada di pustaka, kompiler tidak bisa tahu apa yang akan ditautkan dengan pustaka tersebut, sehingga apakah setkehendak telah dipanggil sebelum get, Bidang dukungan mungkin tidak diinisialisasi secara eksplisit, tetapi harus mengkompilasi kode tersebut.
David Arno
Itu benar, tapi saya tidak akan mengharapkan kesalahan dihasilkan saat kompilasi f. Ini akan dihasilkan saat kompilasi konstruktor. Jika Anda meninggalkan konstruktor dengan bidang yang mungkin tidak diinisialisasi, itu akan menjadi kesalahan. Mungkin juga harus ada pembatasan dalam memanggil metode kelas dan getter sebelum semua bidang diinisialisasi.
John Kugelman
@ JohnKugelman: Saya akan memposting jawaban yang membahas masalah yang Anda ajukan.
Eric Lippert
4
Itu tidak adil. Kami mencoba untuk memiliki perselisihan di sini!
John Kugelman
10

Jawaban yang bagus di atas, tapi saya pikir saya akan memposting jawaban yang jauh lebih sederhana / lebih pendek bagi orang-orang yang malas membaca yang panjang (seperti saya).

Kelas

class Foo {
    private string Boo;
    public Foo() { /** bla bla bla **/ }
    public string DoSomething() { return Boo; }
}

Properti Boomungkin atau mungkin belum diinisialisasi dalam konstruktor. Jadi ketika menemukan return Boo;itu tidak menganggap bahwa itu sudah diinisialisasi. Itu hanya menekan kesalahan.

Fungsi

public string Foo() {
   string Boo;
   return Boo; // triggers error
}

The { }karakter menentukan ruang lingkup blok kode. Kompilator berjalan di cabang-cabang { }blok ini melacak hal-hal. Dapat dengan mudah mengatakan bahwa Booitu tidak diinisialisasi. Kesalahan kemudian dipicu.

Mengapa ada kesalahan?

Kesalahan diperkenalkan untuk mengurangi jumlah baris kode yang diperlukan untuk membuat kode sumber aman. Tanpa kesalahan di atas akan terlihat seperti ini.

public string Foo() {
   string Boo;
   /* bla bla bla */
   if(Boo == null) {
      return "";
   }
   return Boo;
}

Dari manual:

Kompiler C # tidak mengizinkan penggunaan variabel yang tidak diinisialisasi. Jika kompiler mendeteksi penggunaan variabel yang mungkin belum diinisialisasi, itu menghasilkan kesalahan kompiler CS0165. Untuk informasi lebih lanjut, lihat Fields (Panduan Pemrograman C #). Perhatikan bahwa kesalahan ini dihasilkan ketika kompiler menemukan konstruk yang mungkin mengakibatkan penggunaan variabel yang tidak ditetapkan, bahkan jika kode khusus Anda tidak. Ini menghindari perlunya aturan yang terlalu rumit untuk penugasan yang pasti.

Referensi: https://msdn.microsoft.com/en-us/library/4y7h161d.aspx

Reactgular
sumber