Mengapa loop sementara (benar) di konstruktor sebenarnya buruk?

47

Meskipun pertanyaan umum ruang lingkup saya agak C # karena saya sadar bahwa bahasa seperti C ++ memiliki semantik yang berbeda mengenai eksekusi konstruktor, manajemen memori, perilaku tidak terdefinisi, dll.

Seseorang bertanya kepada saya pertanyaan menarik yang bagi saya tidak mudah dijawab.

Mengapa (atau itu sama sekali?) Dianggap sebagai desain yang buruk untuk membiarkan konstruktor kelas memulai loop yang tidak pernah berakhir (yaitu loop game)?

Ada beberapa konsep yang dirusak oleh ini:

  • seperti prinsip yang paling mengejutkan, pengguna tidak mengharapkan konstruktor berperilaku seperti ini.
  • Tes unit lebih sulit karena Anda tidak dapat membuat kelas ini atau menyuntikkannya karena tidak pernah keluar dari loop.
  • Akhir dari loop (akhir permainan) kemudian secara konseptual adalah waktu di mana konstruktor selesai, yang juga aneh.
  • Secara teknis kelas seperti itu tidak memiliki anggota publik kecuali konstruktor, yang membuatnya lebih sulit untuk dipahami (terutama untuk bahasa di mana tidak ada implementasi tersedia)

Dan kemudian ada masalah teknis:

  • Konstruktor sebenarnya tidak pernah selesai, jadi apa yang terjadi dengan GC di sini? Apakah objek ini sudah ada dalam Gen 0?
  • Berasal dari kelas semacam itu tidak mungkin atau paling tidak sangat rumit karena fakta bahwa konstruktor dasar tidak pernah kembali

Apakah ada sesuatu yang lebih buruk atau licik dengan pendekatan semacam itu?

Samuel
sumber
61
Mengapa ini bagus? Jika Anda hanya memindahkan loop utama Anda ke suatu metode (refactoring sangat sederhana) maka pengguna dapat menulis kode yang tidak mengejutkan seperti ini:var g = new Game {...}; g.MainLoop();
Brandin
61
Pertanyaan Anda sudah menyatakan 6 alasan untuk tidak menggunakannya, dan saya ingin melihat satu saja yang mendukungnya. Anda juga bisa bertanya mengapa itu ide buruk untuk menempatkan sebuah while(true)loop dalam setter properti: new Game().RunNow = true?
Groo
38
Analogi ekstrem: mengapa kita mengatakan bahwa apa yang dilakukan Hitler salah? Kecuali untuk diskriminasi rasial, mulai Perang Dunia II, menewaskan jutaan orang, kekerasan [dll dll karena 50+ alasan lain] ia tidak melakukan kesalahan. Tentu jika Anda menghapus daftar alasan mengapa ada sesuatu yang salah, Anda dapat menyimpulkan bahwa hal itu baik, untuk apa pun secara harfiah.
Bakuriu
4
Sehubungan dengan pengumpulan sampah (GC) (masalah teknis Anda), tidak akan ada yang istimewa tentang itu. Contoh ada sebelum kode konstruktor benar-benar dijalankan. Dalam hal ini instance masih dapat dijangkau, sehingga tidak dapat diklaim oleh GC. Jika GC masuk, instance ini akan bertahan, dan pada setiap kemunculan pengaturan GC, objek akan dipromosikan ke generasi berikutnya sesuai dengan aturan yang biasa. Apakah GC terjadi atau tidak, tergantung pada apakah (cukup) objek baru sedang dibuat atau tidak.
Jeppe Stig Nielsen
8
Saya akan melangkah lebih jauh, dan mengatakan sementara (benar) buruk, terlepas dari mana itu digunakan. Ini menyiratkan bahwa tidak ada cara yang jelas dan bersih untuk menghentikan loop. Dalam contoh Anda, bukankah loop harus berhenti saat game keluar, atau kehilangan fokus?
Jon Anderson

Jawaban:

250

Apa tujuan seorang konstruktor? Ini mengembalikan objek yang baru dibangun. Apa yang dilakukan infinite loop? Tidak pernah kembali. Bagaimana konstruktor mengembalikan objek yang baru dibangun jika tidak kembali sama sekali? Tidak bisa.

Ergo, sebuah infinite loop memutus kontrak fundamental konstruktor: untuk membangun sesuatu.

Jörg W Mittag
sumber
11
Mungkin mereka bermaksud menempatkan sementara (benar) dengan istirahat di tengah? Berisiko, tapi sah.
John Dvorak
2
Saya setuju, ini benar - setidaknya. dari sudut pandang akademis. Hal yang sama berlaku untuk setiap fungsi yang mengembalikan sesuatu.
Doc Brown
61
Bayangkan tanah miskin yang menciptakan sub-kelas dari kelas tersebut ...
Newtopian
5
@ JanDvorak: Yah, itu pertanyaan yang berbeda. OP secara eksplisit menentukan "tidak pernah berakhir" dan menyebutkan sebuah loop acara.
Jörg W Mittag
4
@ JörgWMittag yah, kalau begitu, ya, jangan letakkan itu di konstruktor.
John Dvorak
45

Apakah ada sesuatu yang lebih buruk atau licik dengan pendekatan semacam itu?

Ya tentu saja. Itu tidak perlu, tidak terduga, tidak berguna, tidak penting. Ini melanggar konsep desain kelas modern (kohesi, kopling). Itu melanggar kontrak metode (konstruktor memiliki pekerjaan yang ditentukan dan bukan hanya beberapa metode acak). Hal ini tentu saja tidak dipelihara dengan baik, programmer masa depan akan menghabiskan banyak waktu untuk mencoba memahami apa yang sedang terjadi, dan mencoba menebak alasan mengapa hal itu dilakukan dengan cara itu.

Tidak ada yang merupakan "bug" dalam arti kode Anda tidak berfungsi. Tetapi kemungkinan akan menimbulkan biaya sekunder yang besar (relatif terhadap biaya penulisan kode pada awalnya) dalam jangka panjang dengan membuat kode lebih sulit untuk dipertahankan (yaitu, sulit untuk menambahkan tes, sulit untuk digunakan kembali, sulit untuk debug, sulit untuk memperpanjang dll .).

Banyak / kebanyakan peningkatan modern dalam metode pengembangan perangkat lunak dilakukan secara khusus untuk membuat proses penulisan / pengujian / debugging / pemeliharaan perangkat lunak menjadi lebih mudah. Semua ini dielakkan dengan hal-hal seperti ini, di mana kode ditempatkan secara acak karena "berfungsi".

Sayangnya, Anda secara teratur akan bertemu programmer yang sama sekali tidak mengetahui semua ini. Berhasil, itu saja.

Untuk menyelesaikan dengan analogi (bahasa pemrograman lain, di sini masalahnya adalah menghitung 2+2):

$sum_a = `bash -c "echo $((2+2)) 2>/dev/null"`;   # calculate
chomp $sum_a;                         # remove trailing \n
$sum_a = $sum_a + 0;                  # force it to be a number in case some non-digit characters managed to sneak in

$sum_b = 2+2;

Apa yang salah dengan pendekatan pertama? Ia mengembalikan 4 setelah waktu yang cukup singkat; itu benar. Keberatan (bersama dengan semua alasan yang biasa diberikan pengembang untuk membantahnya) adalah:

  • Sulit dibaca (tapi hei, programmer yang baik dapat membacanya dengan mudah, dan kami selalu melakukannya seperti ini; jika Anda mau, saya bisa mengubahnya menjadi sebuah metode!)
  • Lebih lambat (tapi ini bukan di tempat kritis waktu sehingga kita bisa mengabaikan itu, dan selain itu, yang terbaik adalah hanya mengoptimalkan bila diperlukan!)
  • Menggunakan lebih banyak sumber daya (proses baru, lebih banyak RAM, dll. - tapi selain itu, server kami lebih dari cukup cepat)
  • Memperkenalkan dependensi bash(tetapi tidak akan pernah berjalan di Windows, Mac atau Android)
  • Dan semua alasan lain yang disebutkan di atas.
AnoE
sumber
5
"GC mungkin dalam kondisi penguncian yang luar biasa saat menjalankan konstruktor" - mengapa? Pengalokasi mengalokasikan terlebih dahulu, kemudian meluncurkan konstruktor sebagai metode biasa. Tidak ada yang spesial tentang itu, kan? Dan pengalokasi harus mengizinkan alokasi baru (dan ini dapat memicu situasi OOM) di dalam konstruktor.
John Dvorak
1
@JanDvorak, Hanya ingin menunjukkan bahwa mungkin ada beberapa perilaku khusus "di suatu tempat" saat berada dalam konstruktor, tetapi Anda benar, contoh yang saya pilih tidak realistis. Dihapus.
AnoE
Saya sangat suka penjelasan Anda dan ingin menandai kedua balasan sebagai jawaban (paling tidak memiliki suara positif). Analoginya bagus dan jejak pemikiran serta penjelasan biaya sekunder Anda juga, tetapi saya menganggapnya sebagai persyaratan tambahan untuk kode (sama dengan argumen saya). Keadaan saat ini adalah untuk menguji kode, oleh karena itu testability dll adalah persyaratan de-facto. Tapi pernyataan @ JörgWMittag fundamental contract of a constructor: to construct somethingtepat.
Samuel
Analogi ini tidak begitu baik. Jika saya melihat ini, saya akan berharap untuk melihat komentar di suatu tempat tentang mengapa Anda menggunakan Bash untuk melakukan perhitungan, dan tergantung pada alasan Anda itu mungkin benar-benar baik-baik saja. Hal yang sama tidak akan berfungsi untuk OP, karena pada dasarnya Anda harus memberikan permintaan maaf dalam komentar di mana pun Anda menggunakan konstruktor "// Catatan: membangun objek ini memulai loop acara yang tidak pernah kembali".
Brandin
4
"Sayangnya, Anda secara teratur akan bertemu programmer yang sama sekali tidak mengetahui semua ini. Berhasil, itu saja." Sedih tapi sangat benar. Saya pikir saya dapat berbicara untuk kita semua ketika saya mengatakan satu-satunya beban substansial dalam kehidupan profesional kita adalah, yah, orang-orang itu.
Lightness Races with Monica
8

Anda memberikan cukup alasan dalam pertanyaan Anda untuk mengesampingkan pendekatan ini tetapi pertanyaan sebenarnya di sini adalah "Apakah ada sesuatu yang lebih jelas buruk atau licik dengan pendekatan seperti itu?"

Namun yang pertama saya di sini adalah bahwa ini tidak ada gunanya. Jika konstruktor Anda tidak pernah selesai, tidak ada bagian lain dari program yang bisa mendapatkan referensi ke objek yang dibangun jadi apa logika di balik meletakkannya di konstruktor alih-alih metode biasa. Kemudian terpikir oleh saya bahwa satu-satunya perbedaan adalah bahwa di loop Anda, Anda dapat memungkinkan referensi untuk sebagian dibangun thismelarikan diri dari loop. Meskipun ini tidak sepenuhnya terbatas pada situasi ini, dijamin bahwa jika Anda mengizinkan thisuntuk melarikan diri, referensi tersebut akan selalu menunjuk ke objek yang tidak sepenuhnya dibangun.

Saya tidak tahu apakah semantik di sekitar situasi seperti ini didefinisikan dengan baik dalam C # tapi saya berpendapat itu tidak masalah karena itu bukan sesuatu yang kebanyakan pengembang ingin coba selidiki.

JimmyJames
sumber
Saya akan berasumsi bahwa konstruktor dijalankan setelah objek dibuat sehingga dalam bahasa waras bocor ini harus perilaku yang didefinisikan dengan baik.
mroman
@mroman: mungkin ok bocor thisdi konstruktor - tetapi hanya jika Anda berada di konstruktor dari kelas yang paling diturunkan. Jika Anda memiliki dua kelas, Adan Bdengan B : A, jika konstruktor Aakan bocor this, anggota Bmasih akan diinisialisasi (dan metode virtual panggilan atau gips untuk Bdapat mendatangkan malapetaka berdasarkan itu).
hoffmale
1
Saya kira dalam C ++ itu bisa gila, ya. Dalam bahasa lain anggota menerima nilai default sebelum mereka diberi nilai aktualnya sehingga walaupun bodoh untuk benar-benar menulis kode seperti itu perilaku masih didefinisikan dengan baik. Jika Anda memanggil metode yang menggunakan anggota ini, Anda mungkin mengalami NullPointerExceptions atau perhitungan yang salah (karena angka-angka default 0) atau hal-hal seperti itu tetapi secara teknis menentukan apa yang terjadi.
mroman
@mroman Bisakah Anda menemukan referensi yang pasti untuk CLR yang akan menunjukkan hal itu?
JimmyJames
Baik Anda dapat menetapkan nilai anggota sebelum konstruktor dijalankan sehingga objek ada dalam keadaan valid dengan anggota menetapkan nilai atau nilai default yang ditetapkan kepada mereka sebelum konstruktor dijalankan. Anda tidak memerlukan konstruktor untuk menginisialisasi anggota. Biarkan saya melihat apakah saya dapat menemukannya di suatu tempat di dokumen.
mroman
1

+1 Paling tidak heran, tetapi ini adalah konsep yang sulit untuk diartikulasikan ke pengembang baru. Pada tingkat pragmatis, sulit untuk men-debug pengecualian yang muncul di dalam konstruktor, jika objek gagal menginisialisasi itu tidak akan ada bagi Anda untuk memeriksa keadaan atau log dari luar konstruktor itu.

Jika Anda merasa perlu melakukan semacam pola kode ini, silakan gunakan metode statis di kelas sebagai gantinya.

Konstruktor ada untuk memberikan logika inisialisasi ketika sebuah objek diturunkan dari definisi kelas. Anda membangun instance objek ini karena itu sebagai wadah yang merangkum serangkaian properti dan fungsionalitas yang ingin Anda panggil dari sisa logika aplikasi Anda.

Jika Anda tidak berniat menggunakan objek yang Anda "buat", lalu apa gunanya instantiasi objek itu? Memiliki loop sementara (benar) di konstruktor secara efektif berarti Anda tidak pernah bermaksud untuk menyelesaikannya ...

C # adalah bahasa berorientasi objek yang sangat kaya dengan banyak konstruksi dan paradigma yang berbeda untuk Anda jelajahi, ketahui alat Anda dan kapan menggunakannya, intinya:

Dalam C # jangan jalankan logika extended atau tidak pernah berakhir dalam konstruktor karena ... ada alternatif yang lebih baik

Chris Schaller
sumber
1
tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam 9 jawaban sebelumnya
nyamuk
1
Hai, saya harus mencobanya :) Saya pikir penting untuk menyoroti bahwa sementara tidak ada aturan yang melarang hal ini, Anda tidak boleh berdasarkan fakta bahwa ada cara yang lebih sederhana. Sebagian besar tanggapan lain berfokus pada teknis mengapa itu buruk, tetapi itu sulit untuk dijual kepada seorang pengembang baru yang mengatakan "tetapi ini mengkompilasi, jadi bisakah saya membiarkannya seperti itu"
Chris Schaller
0

Tidak ada yang buruk tentang hal itu. Namun, yang penting adalah pertanyaan mengapa Anda memilih untuk menggunakan konstruk ini. Apa yang Anda dapatkan dengan melakukan ini?

Pertama, saya tidak akan berbicara tentang penggunaan while(true)konstruktor. Sama sekali tidak ada yang salah dengan menggunakan sintaks dalam konstruktor. Namun, konsep "memulai permainan di konstruktor" adalah konsep yang dapat menyebabkan beberapa masalah.

Sangat umum dalam situasi ini untuk memiliki konstruktor cukup membangun objek, dan memiliki fungsi yang disebut infinite loop. Ini memberi pengguna kode Anda lebih fleksibel. Sebagai contoh dari berbagai masalah yang dapat muncul adalah Anda membatasi penggunaan objek Anda. Jika seseorang ingin memiliki objek anggota yang merupakan "objek permainan" di kelas mereka, mereka harus siap untuk menjalankan seluruh loop permainan di konstruktor mereka karena mereka harus membangun objek. Ini mungkin menyebabkan penyiksaan kode untuk mengatasi hal ini. Dalam kasus umum, Anda tidak dapat mengalokasikan sebelumnya objek game Anda, dan kemudian menjalankannya nanti. Untuk beberapa program itu bukan masalah, tetapi ada kelas program di mana Anda ingin dapat mengalokasikan semuanya di muka dan kemudian memanggil lingkaran permainan.

Salah satu aturan praktis dalam pemrograman adalah menggunakan alat paling sederhana untuk pekerjaan itu. Ini bukan aturan yang keras dan cepat, tapi itu sangat berguna. Jika pemanggilan fungsi sudah cukup, mengapa repot membangun objek kelas? Jika saya melihat alat rumit yang lebih kuat digunakan, saya menganggap pengembang punya alasan untuk itu, dan saya akan mulai mengeksplorasi jenis trik aneh yang mungkin Anda lakukan.

Ada kasus di mana ini mungkin masuk akal. Mungkin ada kasus di mana itu benar-benar intuitif bagi Anda untuk memiliki loop permainan di konstruktor. Dalam kasus itu, Anda menjalankannya! Misalnya, loop game Anda dapat disematkan dalam program yang jauh lebih besar yang membangun kelas untuk mewujudkan beberapa data. Jika Anda harus menjalankan loop game untuk menghasilkan data itu, mungkin sangat masuk akal untuk menjalankan loop game dalam konstruktor. Perlakukan saja ini sebagai kasus khusus: valid untuk melakukan ini karena program menyeluruh membuatnya intuitif bagi pengembang berikutnya untuk memahami apa yang Anda lakukan dan mengapa.

Cort Ammon
sumber
Jika membangun objek Anda memerlukan loop game, setidaknya masukkan dalam metode pabrik sebagai gantinya. GameTestResults testResults = runGameTests(tests, configuration);bukan GameTestResults testResults = new GameTestResults(tests, configuration);.
user253751
@immibis Ya, itu benar, kecuali untuk kasus-kasus di mana itu tidak masuk akal. Jika Anda bekerja dengan sistem berbasis refleksi yang membuat objek Anda untuk Anda, Anda mungkin tidak memiliki pilihan untuk membuat metode pabrik.
Cort Ammon
0

Kesan saya adalah bahwa beberapa konsep tercampur dan menghasilkan pertanyaan yang tidak terlalu baik.

Konstruktor suatu kelas dapat memulai utas baru yang akan "tidak pernah" berakhir (meskipun saya lebih suka menggunakan while(_KeepRunning)lebih while(true)karena saya dapat mengatur anggota boolean ke false di suatu tempat).

Anda dapat mengekstrak metode membuat dan memulai utas ke dalam fungsinya sendiri, untuk memisahkan konstruksi objek dan awal pekerjaannya - saya lebih suka cara ini karena saya mendapatkan kontrol yang lebih baik atas akses ke sumber daya.

Anda juga dapat melihat "Pola Objek Aktif". Pada akhirnya, saya kira pertanyaannya adalah kapan memulai thread dari "Objek Aktif", dan preferensi saya adalah de-coupling dari konstruksi dan mulai untuk kontrol yang lebih baik.

Bernhard Hiller
sumber
0

Objek Anda mengingatkan saya untuk memulai Tugas . Tugas objek memiliki konstruktor, tetapi ada juga sekelompok metode pabrik statis seperti: Task.Run, Task.Startdan Task.Factory.StartNew.

Ini sangat mirip dengan apa yang Anda coba lakukan, dan kemungkinan ini adalah 'konvensi'. Masalah utama yang tampaknya dimiliki adalah penggunaan konstruktor ini mengejutkan mereka. @ JörgWMittag mengatakan itu melanggar kontrak mendasar , yang saya kira berarti Jorg sangat terkejut. Saya setuju dan tidak ada lagi yang bisa ditambahkan.

Namun, saya ingin menyarankan bahwa OP mencoba metode pabrik statis. Kejutannya lenyap dan tidak ada kontrak fundamental yang dipertaruhkan di sini. Orang-orang terbiasa metode pabrik statis melakukan hal-hal khusus, dan mereka dapat dinamai sesuai.

Anda dapat memberikan konstruktor yang memungkinkan kontrol berbutir halus (seperti @Brandin menyarankan, sesuatu seperti var g = new Game {...}; g.MainLoop();, yang mengakomodasi pengguna yang tidak ingin segera memulai permainan, dan mungkin ingin meneruskannya terlebih dahulu. Dan Anda dapat menulis sesuatu seperti var runningGame = Game.StartNew();memulai) segera mudah.

Nathan Cooper
sumber
Ini berarti Jörg pada dasarnya terkejut, yang mengatakan kejutan seperti itu adalah rasa sakit pada dasarnya.
Steve Jessop
0

Mengapa loop sementara (benar) di konstruktor sebenarnya buruk?

Itu tidak buruk sama sekali.

Mengapa (atau itu sama sekali?) Dianggap sebagai desain yang buruk untuk membiarkan konstruktor kelas memulai loop yang tidak pernah berakhir (yaitu loop game)?

Mengapa Anda ingin melakukan ini? Serius. Kelas itu memiliki fungsi tunggal: konstruktor. Jika semua yang Anda inginkan adalah fungsi tunggal, itu sebabnya kami memiliki fungsi, bukan konstruktor.

Anda menyebutkan beberapa alasan yang jelas menentangnya sendiri, tetapi Anda mengabaikan alasan yang paling besar:

Ada pilihan yang lebih baik dan lebih sederhana untuk mencapai hal yang sama.

Jika ada 2 pilihan dan satu lebih baik, tidak masalah seberapa banyak lebih baik, Anda memilih yang lebih baik. Kebetulan, itu sebabnya kami tidak hampir tidak pernah menggunakan GoTo.

Peter
sumber
-1

Tujuan konstruktor adalah untuk, membuat objek Anda. Sebelum konstruktor selesai, pada prinsipnya tidak aman untuk menggunakan metode apa pun dari objek itu. Tentu saja, kita dapat memanggil metode objek dalam konstruktor, tetapi kemudian setiap kali kita melakukannya, kita harus memastikan bahwa hal itu berlaku untuk objek dalam keadaan setengah baken saat ini.

Dengan secara tidak langsung memasukkan semua logika ke dalam konstruktor, Anda menambahkan lebih banyak beban seperti ini ke diri Anda sendiri. Ini menunjukkan bahwa Anda tidak merancang perbedaan antara inisialisasi dan penggunaan yang cukup baik.

Hagen von Eitzen
sumber
1
ini tampaknya tidak menawarkan sesuatu yang substansial atas poin yang dibuat dan dijelaskan dalam 8 jawaban sebelumnya
nyamuk