Saya punya kebiasaan aneh, sepertinya ... setidaknya menurut rekan kerja saya. Kami telah mengerjakan proyek kecil bersama. Cara saya menulis kelas adalah (contoh sederhana):
[Serializable()]
public class Foo
{
public Foo()
{ }
private Bar _bar;
public Bar Bar
{
get
{
if (_bar == null)
_bar = new Bar();
return _bar;
}
set { _bar = value; }
}
}
Jadi, pada dasarnya, saya hanya menginisialisasi bidang apa pun ketika pengambil dipanggil dan bidang tersebut masih nol. Saya pikir ini akan mengurangi kelebihan dengan tidak menginisialisasi properti yang tidak digunakan di mana pun.
ETA: Alasan saya melakukan ini adalah karena kelas saya memiliki beberapa properti yang mengembalikan instance dari kelas lain, yang pada gilirannya juga memiliki properti dengan kelas yang lebih banyak, dan seterusnya. Memanggil konstruktor untuk kelas atas selanjutnya akan memanggil semua konstruktor untuk semua kelas ini, ketika mereka tidak selalu semua dibutuhkan.
Apakah ada keberatan terhadap praktik ini, selain dari preferensi pribadi?
UPDATE: Saya telah mempertimbangkan banyak pendapat yang berbeda sehubungan dengan pertanyaan ini dan saya akan berdiri dengan jawaban yang diterima Namun, saya sekarang memiliki pemahaman yang jauh lebih baik tentang konsep dan saya dapat memutuskan kapan akan menggunakannya dan kapan tidak.
Cons:
- Masalah keamanan utas
- Tidak mematuhi permintaan "setter" ketika nilai yang diberikan adalah nol
- Optimalisasi mikro
- Penanganan pengecualian harus dilakukan di konstruktor
- Perlu memeriksa null dalam kode kelas
Pro:
- Optimalisasi mikro
- Properti tidak pernah mengembalikan nol
- Tunda atau hindari memuat benda "berat"
Sebagian besar kontra tidak berlaku untuk perpustakaan saya saat ini, namun saya harus menguji untuk melihat apakah "optimasi mikro" benar-benar mengoptimalkan apa pun.
PEMBAHARUAN TERAKHIR:
Oke, saya mengubah jawaban saya. Pertanyaan awal saya adalah apakah ini kebiasaan yang baik atau tidak. Dan sekarang saya yakin tidak. Mungkin saya masih akan menggunakannya di beberapa bagian kode saya saat ini, tetapi tidak tanpa syarat dan pasti tidak setiap saat. Jadi saya akan kehilangan kebiasaan dan memikirkannya sebelum menggunakannya. Terimakasih semuanya!
sumber
Jawaban:
Apa yang Anda miliki di sini adalah - naif - implementasi "inisialisasi malas".
Jawaban singkat:
Menggunakan inisialisasi malas tanpa syarat bukanlah ide yang baik. Ini memiliki tempat tetapi harus mempertimbangkan dampak dari solusi ini.
Latar belakang dan penjelasan:
Implementasi beton:
Pertama-tama mari kita lihat sampel beton Anda dan mengapa saya menganggap penerapannya naif:
Itu melanggar Principle of Least Surprise (POLS) . Ketika nilai diberikan ke properti, diharapkan nilai ini dikembalikan. Dalam implementasi Anda ini tidak berlaku untuk
null
:foo.Bar
pada utas yang berbeda berpotensi mendapatkan dua contoh berbedaBar
dan salah satunya tanpa koneksi keFoo
instance. Setiap perubahan yang dilakukan padaBar
instance tersebut hilang secara diam-diam.Ini adalah kasus pelanggaran POLS. Ketika hanya nilai yang disimpan dari suatu properti yang diakses, diharapkan aman-utas. Meskipun Anda dapat berargumen bahwa kelas tersebut tidak aman untuk thread - termasuk pengambil properti Anda - Anda harus mendokumentasikan ini dengan benar karena itu bukan kasus normal. Lebih jauh, pengenalan masalah ini tidak perlu karena akan segera kita lihat.
Secara umum:
Sekarang saatnya untuk melihat inisialisasi malas secara umum:
Inisialisasi malas biasanya digunakan untuk menunda pembangunan objek yang membutuhkan waktu lama untuk dibangun atau yang mengambil banyak memori setelah sepenuhnya dibangun.
Itu adalah alasan yang sangat valid untuk menggunakan inisialisasi malas.
Namun, properti seperti itu biasanya tidak memiliki setter, yang menghilangkan masalah pertama yang disebutkan di atas.
Selanjutnya, implementasi thread-safe akan digunakan - seperti
Lazy<T>
- untuk menghindari masalah kedua.Bahkan ketika mempertimbangkan dua poin ini dalam penerapan properti malas, poin-poin berikut adalah masalah umum dari pola ini:
Konstruksi objek bisa gagal, menghasilkan pengecualian dari pengambil properti. Ini merupakan pelanggaran lain terhadap POLS dan karenanya harus dihindari. Bahkan bagian tentang properti di "Pedoman Desain untuk Mengembangkan Perpustakaan Kelas" secara eksplisit menyatakan bahwa pengambil properti tidak boleh melempar pengecualian:
Optimalisasi otomatis oleh kompiler terluka, yaitu prediksi inlining dan cabang. Silakan lihat jawaban Bill K untuk penjelasan terperinci.
Kesimpulan dari poin-poin ini adalah sebagai berikut:
Untuk setiap properti yang diimplementasikan dengan malas, Anda harus mempertimbangkan poin-poin ini.
Itu berarti, bahwa itu adalah keputusan per kasus dan tidak dapat diambil sebagai praktik umum terbaik.
Pola ini memiliki tempatnya, tetapi ini bukan praktik terbaik umum ketika menerapkan kelas. Seharusnya tidak digunakan tanpa syarat , karena alasan yang disebutkan di atas.
Pada bagian ini saya ingin membahas beberapa poin yang diajukan orang lain sebagai argumen untuk menggunakan inisialisasi malas tanpa syarat:
Serialisasi:
EricJ menyatakan dalam satu komentar:
Ada beberapa masalah dengan argumen ini:
Optimalisasi mikro: Argumen utama Anda adalah bahwa Anda ingin membuat objek hanya ketika seseorang benar-benar mengaksesnya. Jadi, Anda sebenarnya berbicara tentang mengoptimalkan penggunaan memori.
Saya tidak setuju dengan argumen ini karena alasan berikut:
Saya mengakui fakta bahwa kadang-kadang optimasi semacam ini dibenarkan. Tetapi bahkan dalam kasus ini inisialisasi malas sepertinya bukan solusi yang tepat. Ada dua alasan yang menentangnya:
sumber
null
kasus ini menjadi lebih kuat. :-)Ini adalah pilihan desain yang bagus. Sangat disarankan untuk kode perpustakaan atau kelas inti.
Ini disebut oleh beberapa "inisialisasi malas" atau "inisialisasi tertunda" dan umumnya dianggap oleh semua sebagai pilihan desain yang baik.
Pertama, jika Anda menginisialisasi dalam deklarasi variabel tingkat kelas atau konstruktor, maka ketika objek Anda dibangun, Anda memiliki overhead untuk menciptakan sumber daya yang mungkin tidak pernah digunakan.
Kedua, sumber daya hanya akan dibuat jika diperlukan.
Ketiga, Anda menghindari sampah mengumpulkan benda yang tidak digunakan.
Terakhir, lebih mudah untuk menangani pengecualian inisialisasi yang mungkin terjadi di properti kemudian pengecualian yang terjadi selama inisialisasi variabel tingkat kelas atau konstruktor.
Ada pengecualian untuk aturan ini.
Mengenai argumen kinerja pemeriksaan tambahan untuk inisialisasi di properti "dapatkan", itu tidak signifikan. Menginisialisasi dan membuang objek adalah hit kinerja yang lebih signifikan daripada pemeriksaan pointer nol sederhana dengan lompatan.
Pedoman Desain untuk Mengembangkan Perpustakaan Kelas di http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
Mengenai
Lazy<T>
Kelas generik
Lazy<T>
diciptakan persis seperti yang diinginkan poster, lihat Inisialisasi Lazy di http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx . Jika Anda memiliki versi .NET yang lebih lama, Anda harus menggunakan pola kode yang diilustrasikan dalam pertanyaan. Pola kode ini telah menjadi sangat umum sehingga Microsoft ingin memasukkan kelas dalam pustaka .NET terbaru untuk membuatnya lebih mudah untuk menerapkan pola tersebut. Selain itu, jika implementasi Anda membutuhkan keamanan utas, maka Anda harus menambahkannya.Tipe Data Primitif dan Kelas Sederhana
Obvioulsy, Anda tidak akan menggunakan inisialisasi malas untuk tipe data primitif atau penggunaan kelas sederhana seperti
List<string>
.Sebelum Mengomentari tentang Malas
Lazy<T>
diperkenalkan di .NET 4.0, jadi tolong jangan tambahkan komentar lain tentang kelas ini.Sebelum Mengomentari tentang Optimalisasi Mikro
Saat Anda sedang membangun perpustakaan, Anda harus mempertimbangkan semua optimasi. Misalnya, dalam kelas .NET Anda akan melihat array bit yang digunakan untuk variabel kelas Boolean di seluruh kode untuk mengurangi konsumsi memori dan fragmentasi memori, hanya untuk menyebut dua "optimasi mikro".
Mengenai Antarmuka Pengguna
Anda tidak akan menggunakan inisialisasi malas untuk kelas yang langsung digunakan oleh antarmuka pengguna. Minggu lalu saya menghabiskan sebagian besar hari menghapus pemuatan malas dari delapan koleksi yang digunakan dalam model tampilan untuk kotak kombo. Saya punya
LookupManager
yang menangani pemuatan malas dan caching koleksi yang dibutuhkan oleh elemen antarmuka pengguna."Setter"
Saya tidak pernah menggunakan properti-set ("setter") untuk properti malas yang dimuat. Karena itu, Anda tidak akan pernah mengizinkan
foo.Bar = null;
. Jika Anda perlu mengaturBar
maka saya akan membuat metode yang disebutSetBar(Bar value)
dan tidak menggunakan inisialisasi malasKoleksi
Properti pengumpulan kelas selalu diinisialisasi ketika dideklarasikan karena mereka tidak boleh nol.
Kelas Kompleks
Biarkan saya ulangi secara berbeda, Anda menggunakan inisialisasi malas untuk kelas kompleks. Yang biasanya, kelas yang dirancang dengan buruk.
akhirnya
Saya tidak pernah mengatakan untuk melakukan ini untuk semua kelas atau dalam semua kasus. Itu kebiasaan buruk.
sumber
Apakah Anda mempertimbangkan menerapkan pola tersebut menggunakan
Lazy<T>
?Selain pembuatan objek malas yang mudah, Anda mendapatkan keamanan utas saat objek diinisialisasi:
Seperti yang dikatakan orang lain, Anda malas memuat objek jika benar-benar berat sumber daya atau perlu waktu untuk memuatnya selama waktu konstruksi objek.
sumber
Lazy<T>
sekarang, dan menahan diri dari menggunakan cara yang biasa saya lakukan.Making the Lazy<T> object thread safe does not protect the lazily initialized object. If multiple threads can access the lazily initialized object, you must make its properties and methods safe for multithreaded access.
Saya pikir itu tergantung pada apa yang Anda inisialisasi. Saya mungkin tidak akan melakukannya untuk daftar karena biaya konstruksinya cukup kecil, sehingga bisa masuk konstruktor. Tetapi jika itu adalah daftar pra-populasi maka saya mungkin tidak akan sampai itu diperlukan untuk pertama kalinya.
Pada dasarnya, jika biaya konstruksi melebihi biaya melakukan pemeriksaan bersyarat pada setiap akses, maka malas membuatnya. Jika tidak, lakukan di konstruktor.
sumber
Kelemahan yang bisa saya lihat adalah jika Anda ingin bertanya apakah Bars adalah null, itu tidak akan pernah terjadi, dan Anda akan membuat daftar di sana.
sumber
null
, tetapi jangan mendapatkannya kembali. Biasanya, Anda menganggap Anda mendapatkan kembali nilai yang sama dengan yang Anda masukkan ke properti.Instansiasi / inisialisasi malas adalah pola yang sangat layak. Perlu diingat, bahwa sebagai aturan umum, konsumen API Anda tidak mengharapkan getter dan setter untuk mengambil waktu yang dapat dilihat dari POV pengguna akhir (atau gagal).
sumber
Saya hanya akan memberikan komentar pada jawaban Daniel tetapi saya jujur tidak berpikir itu cukup jauh.
Meskipun ini adalah pola yang sangat baik untuk digunakan dalam situasi tertentu (misalnya, ketika objek diinisialisasi dari database), itu kebiasaan yang mengerikan untuk masuk.
Salah satu hal terbaik tentang suatu objek adalah ia menawarkan lingkungan yang aman dan tepercaya. Kasus terbaik adalah jika Anda membuat sebanyak mungkin bidang "Final", mengisinya dengan konstruktor. Ini membuat kelas Anda cukup anti peluru. Mengizinkan ladang untuk diubah melalui setter sedikit kurang begitu, tetapi tidak mengerikan. Misalnya:
Dengan pola Anda, metode toString akan terlihat seperti ini:
Tidak hanya ini, tetapi Anda perlu cek nol di mana-mana Anda mungkin menggunakan objek itu di kelas Anda (Di luar kelas Anda aman karena cek nol di pengambil, tetapi Anda harus sebagian besar menggunakan anggota kelas Anda di dalam kelas)
Juga kelas Anda terus-menerus dalam keadaan tidak menentu - misalnya jika Anda memutuskan untuk membuat kelas itu menjadi kelas hibernasi dengan menambahkan beberapa penjelasan, bagaimana Anda melakukannya?
Jika Anda mengambil keputusan berdasarkan beberapa mikro-optomisasi tanpa persyaratan dan pengujian, itu hampir pasti merupakan keputusan yang salah. Bahkan, ada peluang yang sangat bagus bahwa pola Anda sebenarnya memperlambat sistem bahkan di bawah keadaan yang paling ideal karena pernyataan if dapat menyebabkan kegagalan prediksi cabang pada CPU yang akan memperlambat banyak hal berkali-kali lipat hanya menetapkan nilai dalam konstruktor kecuali objek yang Anda buat cukup kompleks atau berasal dari sumber data jarak jauh.
Untuk contoh masalah prediksi brance (yang Anda alami berulang kali, jarang sekali), lihat jawaban pertama untuk pertanyaan luar biasa ini: Mengapa lebih cepat memproses array yang diurutkan daripada array yang tidak disortir?
sumber
toString()
mau menelepongetName()
, tidak pakainame
langsung.Izinkan saya menambahkan satu poin lagi ke banyak poin bagus yang dibuat oleh orang lain ...
Debugger akan ( secara default ) mengevaluasi properti ketika melangkah melalui kode, yang berpotensi dapat membuat instantiate
Bar
lebih cepat daripada biasanya terjadi dengan hanya mengeksekusi kode. Dengan kata lain, tindakan debugging semata-mata mengubah pelaksanaan program.Ini mungkin atau mungkin bukan masalah (tergantung pada efek samping), tetapi sesuatu yang harus diperhatikan.
sumber
Apakah Anda yakin Foo harus menjadi instantiating apa pun?
Bagi saya sepertinya bau (walaupun tidak selalu salah ) untuk membiarkan Foo instantiate apapun. Kecuali jika Foo memiliki tujuan untuk menjadi sebuah pabrik, Foo tidak boleh membuat institusinya sendiri, tetapi malah menyuntikkannya ke konstruktornya .
Namun jika tujuan keberadaan Foo adalah untuk membuat instance dari tipe Bar, maka saya tidak melihat ada yang salah dengan melakukannya dengan malas.
sumber