Cara memigrasi pemikiran saya dari C ++ ke C #

12

Saya adalah pengembang C ++ yang berpengalaman, saya tahu bahasanya dengan sangat terperinci dan telah menggunakan beberapa fitur spesifiknya secara intensif. Juga, saya tahu prinsip-prinsip OOD dan pola desain. Saya sekarang belajar C # tetapi saya tidak bisa menghentikan perasaan tidak bisa menyingkirkan pola pikir C ++. Saya mengikat diri saya dengan keras pada kekuatan C ++ sehingga saya tidak bisa hidup tanpa beberapa fitur. Dan saya tidak dapat menemukan solusi atau penggantian yang baik untuk mereka di C #.

Praktik baik apa , pola desain , idiom yang berbeda dalam C # dari perspektif C ++ yang dapat Anda sarankan? Bagaimana cara mendapatkan desain C ++ yang sempurna tanpa terlihat bodoh di C #?

Secara khusus, saya tidak dapat menemukan cara C # -ish yang baik untuk menangani (contoh terbaru):

  • Mengontrol masa pakai sumber daya yang memerlukan pembersihan deterministik (seperti file). Ini mudah using, tetapi bagaimana menggunakannya dengan benar ketika kepemilikan sumber daya sedang ditransfer [... antara utas]? Dalam C ++ saya hanya akan menggunakan pointer bersama dan membiarkannya mengurus 'pengumpulan sampah' pada waktu yang tepat.
  • Perjuangan konstan dengan fungsi-fungsi utama untuk generik tertentu (Saya suka hal-hal seperti spesialisasi template parsial di C ++). Haruskah saya mengabaikan segala upaya untuk melakukan pemrograman generik dalam C #? Mungkin obat generik terbatas pada tujuan dan bukankah C # -ish untuk menggunakannya kecuali untuk domain masalah tertentu?
  • Fungsionalitas seperti makro. Walaupun umumnya merupakan ide yang buruk, untuk beberapa domain masalah tidak ada solusi lain (misalnya evaluasi kondisional dari pernyataan, seperti dengan log yang seharusnya hanya untuk rilis Debug). Tidak memilikinya berarti saya harus menambahkan lebih banyak if (condition) {...}boilerplate dan itu masih tidak sama dalam hal memicu efek samping.
Andrew
sumber
# 1 adalah yang sulit bagi saya, ingin tahu jawabannya sendiri. # 2 - dapatkah Anda memberikan contoh kode C ++ sederhana dan menjelaskan mengapa Anda membutuhkannya? Untuk # 3 - gunakan C#untuk menghasilkan kode lain. Anda dapat membaca CSVatau XMLatau apa yang Anda file sebagai input dan menghasilkan C#atau SQLfile. Ini bisa lebih kuat daripada menggunakan makro fungsional.
Pekerjaan
Pada topik fungsionalitas seperti makro, hal spesifik apa yang Anda coba lakukan? Apa yang saya temukan adalah bahwa biasanya ada konstruksi bahasa C # untuk menangani apa yang akan saya lakukan dengan makro di C ++. Lihat juga arahan preprocessor C #: msdn.microsoft.com/en-us/library/4y6tbswk.aspx
ravibhagw
Banyak hal yang dilakukan dengan makro di C ++ dapat dilakukan dengan refleksi di C #.
Sebastian Redl
Ini adalah salah satu perasaan kesal hewan peliharaan saya ketika seseorang berkata, "Alat yang tepat untuk pekerjaan yang tepat - pilih bahasa berdasarkan apa yang akan Anda butuhkan." Untuk program besar dalam bahasa tujuan umum, ini adalah pernyataan yang tidak berguna dan tidak membantu. Yang mengatakan, hal bahwa C ++ lebih baik daripada hampir semua bahasa lain adalah manajemen sumber daya deterministik. Apakah manajemen sumber daya deterministik merupakan fitur utama dari program Anda, atau sesuatu yang biasa Anda lakukan? Jika ini bukan fitur pengguna yang penting, Anda mungkin terlalu fokus pada hal itu.
Ben

Jawaban:

16

Dalam pengalaman saya, tidak ada buku untuk melakukan ini. Forum membantu dengan idiom dan praktik terbaik, tetapi pada akhirnya Anda harus meluangkan waktu. Berlatih dengan memecahkan sejumlah masalah berbeda dalam C #. Lihatlah apa yang sulit / canggung dan cobalah untuk membuatnya lebih sulit dan tidak canggung lain kali. Bagi saya, proses ini memakan waktu sekitar satu bulan sebelum saya "mengerti".

Hal terbesar yang menjadi fokus adalah kurangnya manajemen memori. Dalam C ++ ini memerintah setiap keputusan desain tunggal yang Anda buat, tetapi dalam C # itu kontra produktif. Dalam C #, Anda sering ingin mengabaikan kematian atau keadaan transisi lain dari objek Anda. Asosiasi semacam itu menambahkan sambungan yang tidak perlu.

Sebagian besar, fokus pada penggunaan alat baru yang paling efektif, tidak menyalahkan diri sendiri karena keterikatan pada yang lama.

Bagaimana cara mendapatkan desain C ++ yang sempurna tanpa terlihat bodoh di C #?

Dengan menyadari bahwa berbagai idiom mendorong ke arah pendekatan desain yang berbeda. Anda tidak bisa hanya mem-porting sesuatu 1: 1 antara (sebagian besar) bahasa dan mengharapkan sesuatu yang indah dan idiomatis di ujung yang lain.

Tetapi dalam menanggapi skenario tertentu:

Sumber daya yang tidak dikelola bersama - Ini adalah ide yang buruk di C ++, dan merupakan ide yang buruk di C #. Jika Anda memiliki file kemudian buka, gunakan, dan tutup. Membagikannya di antara utas hanya meminta masalah, dan jika hanya satu utas yang benar-benar menggunakannya maka buat utas tersebut untuk membuka / memiliki / menutupnya. Jika Anda benar - benar memiliki file yang berumur panjang karena suatu alasan, buat kelas untuk menyatakan hal itu dan biarkan aplikasi mengaturnya sesuai kebutuhan (tutup sebelum keluar, ikat dekat dengan semacam acara Penutupan Aplikasi, dll.)

Spesialisasi Templat Parsial - Anda kurang beruntung di sini, meskipun semakin saya menggunakan C # semakin saya menemukan penggunaan template yang saya lakukan di C ++ untuk jatuh ke YAGNI. Mengapa membuat sesuatu yang generik ketika T selalu int? Skenario lain paling baik diselesaikan di C # dengan aplikasi antarmuka yang liberal. Anda tidak perlu obat generik ketika antarmuka atau tipe delegasi dapat dengan kuat mengetik apa yang ingin Anda variasikan.

Persyaratan preprosesor - C # telah #if, gila. Lebih baik lagi, ia memiliki atribut bersyarat yang hanya akan membuang fungsi itu dari kode jika simbol kompiler tidak didefinisikan.

Telastyn
sumber
Mengenai manajemen sumber daya, di C # itu cukup umum untuk memiliki kelas manajer (yang bisa statis atau dinamis).
rwong
Mengenai sumber daya bersama: Yang dikelola mudah di C # (jika kondisi ras tidak relevan), yang tidak dikelola jauh lebih jahat.
Deduplicator
@ rwong - umum, tetapi kelas manajer masih merupakan anti-pola yang harus dihindari.
Telastyn
Sehubungan dengan manajemen memori, saya menemukan bahwa beralih ke C # membuat ini lebih sulit, terutama pada awalnya. Saya pikir Anda harus lebih sadar daripada di C ++. Dalam C ++ Anda tahu persis kapan dan di mana setiap objek dialokasikan, didemokasi ulang dan direferensikan. Di C #, ini semua tersembunyi dari Anda. Sering kali dalam C # Anda bahkan tidak menyadari bahwa referensi ke objek telah diperoleh dan memori mulai bocor. Bersenang-senang melacak kebocoran memori di C #, yang biasanya sepele untuk mencari tahu di C ++. Tentu saja, dengan pengalaman Anda mempelajari nuansa C # ini sehingga mereka cenderung menjadi kurang masalah.
Dunk
4

Sejauh yang saya bisa lihat, satu-satunya hal yang memungkinkan untuk Manajemen Seumur Hidup deterministik adalah IDisposable, yang rusak (seperti dalam: tidak ada bahasa atau jenis dukungan sistem) ketika, seperti yang Anda perhatikan, Anda perlu Mentransfer Kepemilikan sumber daya semacam itu.

Berada di kapal yang sama dengan Anda - pengembang C ++ melakukan C # atm. - kurangnya dukungan untuk memodelkan Transfer Kepemilikan (file, db-koneksi, ...) dalam tipe / tanda tangan C # sangat mengganggu bagi saya, tetapi saya harus mengakui bahwa itu bekerja cukup baik untuk "hanya" mengandalkan pada konvensi dan API API untuk mendapatkan ini dengan benar.

  • Gunakan IDisposableuntuk melakukan pembersihan deterministik jika perlu.
  • Saat Anda perlu mentransfer kepemilikan suatu IDisposable, pastikan API yang terlibat jelas tentang hal ini dan jangan gunakan usingdalam kasus ini, karena usingtidak dapat "dibatalkan".

Ya, itu tidak seanggun yang Anda bisa ungkapkan dalam sistem tipe dengan C ++ modern (pindahkan semantik, dll.), Tapi ini menyelesaikan pekerjaan, dan (luar biasa bagi saya) komunitas C # secara keseluruhan sepertinya tidak peduli terlalu banyak, AFAICT.

Martin Ba
sumber
2

Anda menyebutkan dua fitur C ++ spesifik. Saya akan membahas RAII:

Ini biasanya tidak berlaku, karena C # adalah sampah yang dikumpulkan. Konstruksi C # terdekat mungkin adalah IDisposableantarmuka, digunakan sebagai berikut:

using (FileStream fs = File.OpenRead(@"c:\path\filename.txt"))
{
   //Do stuff with the file.
} //File will be closed here.

Anda seharusnya tidak berharap untuk IDisposablesering menerapkan (dan melakukannya sedikit maju), karena tidak perlu untuk sumber daya yang dikelola. Namun, Anda harus menggunakan usingkonstruk (atau menyebut Disposediri Anda, jika usingtidak mungkin) untuk implementasi apa pun IDisposable. Bahkan jika Anda mengacaukan ini, kelas C # yang dirancang dengan baik akan mencoba untuk menangani ini untuk Anda (menggunakan finalizers). Namun, dalam bahasa yang dikumpulkan sampah, pola RAII tidak sepenuhnya berfungsi (karena pengumpulan tidak menentukan), sehingga pembersihan sumber daya Anda akan tertunda (terkadang serempak).

Brian
sumber
RAII memang masalah terbesar saya. Katakanlah saya memiliki sumber daya (katakan file) yang dibuat oleh satu utas dan diberikan kepada yang lain. Bagaimana saya bisa memastikan bahwa itu dibuang pada waktu tertentu ketika utas itu tidak membutuhkannya lagi? Tentu saja, saya bisa memanggil Buang tetapi kemudian terlihat jauh lebih jelek daripada petunjuk bersama di C ++. Tidak ada RAII di dalamnya.
Andrew
@Andrew Apa yang Anda gambarkan tampaknya menyiratkan pengalihan kepemilikan, jadi sekarang utas kedua bertanggung jawab atas Disposing()file dan mungkin harus digunakan using.
svick
@Andrew - Secara umum, Anda tidak harus melakukan itu. Alih-alih, Anda harus memberi tahu utas cara mendapatkan file dan membiarkannya memiliki kepemilikan seumur hidup.
Telastyn
1
@Telastyn Apakah ini berarti menyerahkan kepemilikan atas sumber daya yang dikelola adalah masalah yang lebih besar di C # daripada di C ++? Cukup mengejutkan.
Andrew
6
@Andrew: Biarkan saya meringkas: di C ++, Anda mendapatkan manajemen yang seragam, semi-otomatis dari semua sumber daya. Di C #, Anda mendapatkan manajemen memori yang sepenuhnya otomatis - dan manajemen yang sepenuhnya manual untuk semua hal lainnya. Tidak ada perubahan nyata, jadi satu-satunya pertanyaan Anda bukanlah "bagaimana cara memperbaikinya?", Hanya "bagaimana saya terbiasa dengan ini?"
Jerry Coffin
2

Ini adalah pertanyaan yang sangat bagus, karena saya telah melihat sejumlah pengembang C ++ menulis kode C # yang mengerikan.

Sebaiknya jangan menganggap C # sebagai "C ++ dengan sintaks yang lebih bagus". Mereka adalah bahasa berbeda yang memerlukan pendekatan berbeda dalam pemikiran Anda. C ++ memaksa Anda untuk terus berpikir tentang apa yang akan dilakukan CPU dan memori. C # tidak seperti itu. C # khusus dirancang agar Anda tidak memikirkan CPU dan memori dan sebaliknya memikirkan domain bisnis tempat Anda menulis .

Salah satu contoh dari ini yang telah saya lihat adalah bahwa banyak pengembang C ++ akan suka menggunakan untuk loop karena mereka lebih cepat daripada foreach. Ini biasanya merupakan ide yang buruk di C # karena membatasi jenis koleksi yang mungkin diulangi (dan karena itu kegunaan ulang dan fleksibilitas kode).

Saya pikir cara terbaik untuk menyesuaikan dari C ++ ke C # adalah dengan mencoba dan mendekati pengkodean dari perspektif yang berbeda. Pada awalnya ini akan sulit, karena selama bertahun-tahun Anda akan sepenuhnya terbiasa menggunakan utas "apa yang CPU dan memori lakukan" di otak Anda untuk memfilter kode yang Anda tulis. Namun dalam C #, Anda seharusnya memikirkan hubungan antara objek dalam domain bisnis. "Apa yang ingin saya lakukan" sebagai kebalikan dari "apa yang dilakukan komputer".

Jika Anda ingin melakukan sesuatu untuk semua yang ada di daftar objek, alih-alih menulis loop for pada daftar, buat metode yang mengambil IEnumerable<MyObject>dan menggunakan foreachloop.

Contoh spesifik Anda:

Mengontrol masa pakai sumber daya yang memerlukan pembersihan deterministik (seperti file). Ini mudah digunakan di tangan tetapi bagaimana menggunakannya dengan benar ketika kepemilikan sumber daya sedang ditransfer [... antara utas]? Dalam C ++ saya hanya akan menggunakan pointer bersama dan membiarkannya mengurus 'pengumpulan sampah' pada waktu yang tepat.

Anda seharusnya tidak pernah melakukan ini di C # (terutama di antara utas). Jika Anda perlu melakukan sesuatu pada suatu file, lakukan di satu tempat pada satu waktu. Tidak apa-apa (dan memang praktik yang baik) untuk menulis kelas pembungkus yang mengelola sumber daya yang tidak dikelola yang melewati antara kelas Anda, tetapi jangan mencoba dan meneruskan File antara utas dan memiliki kelas terpisah menulis / membaca / menutup / membukanya. Jangan berbagi kepemilikan sumber daya yang tidak dikelola. Gunakan Disposepola untuk menangani pembersihan.

Perjuangan konstan dengan fungsi-fungsi utama untuk generik tertentu (Saya suka hal-hal seperti spesialisasi template parsial di C ++). Haruskah saya mengabaikan segala upaya untuk melakukan pemrograman generik dalam C #? Mungkin obat generik terbatas pada tujuan dan bukankah C # -ish untuk menggunakannya kecuali untuk domain masalah tertentu?

Generik dalam C # dirancang untuk menjadi generik. Spesialisasi generik harus ditangani oleh kelas turunan. Mengapa harus List<T>berperilaku berbeda jika itu adalah List<int>atau List<string>? Semua operasi pada List<T>generik sehingga berlaku untuk apa pun List<T> . Jika Anda ingin mengubah perilaku .Addmetode pada a List<string>, maka buat kelas turunan MySpecializedStringCollection : List<string>atau kelas komposisi MySpecializedStringCollection : IList<string>yang menggunakan generik secara internal tetapi melakukan hal-hal dengan cara yang berbeda. Ini membantu Anda menghindari melanggar Prinsip Pergantian Liskov dan secara tidak sopan meniduri orang lain yang menggunakan kelas Anda.

Fungsionalitas seperti makro. Meskipun umumnya ide yang buruk, untuk beberapa domain masalah tidak ada solusi lain (misalnya evaluasi kondisional dari pernyataan, seperti dengan log yang seharusnya hanya untuk rilis Debug). Tidak memilikinya berarti saya perlu menambahkan lebih banyak (kondisi) {...} boilerplate dan itu masih tidak sama dalam hal memicu efek samping.

Seperti yang orang lain katakan, Anda dapat menggunakan perintah preprocessor untuk melakukan ini. Atribut bahkan lebih baik. Secara umum, atribut adalah cara terbaik untuk menangani hal-hal yang bukan fungsi "inti" untuk sebuah kelas.

Singkatnya, ketika menulis C #, perlu diingat bahwa Anda harus berpikir sepenuhnya tentang domain bisnis dan bukan tentang CPU dan memori. Anda dapat mengoptimalkan nanti jika perlu , tetapi kode Anda harus mencerminkan hubungan antara prinsip-prinsip bisnis yang Anda coba petakan.

Stephen
sumber
3
WRT. transfer sumber daya: "Anda tidak boleh melakukan ini" tidak memotongnya IMHO. Seringkali, desain yang sangat bagus menyarankan transfer IDisposable( bukan sumber daya mentah) dari satu kelas ke kelas lain: Lihat saja StreamWriter API di mana ini masuk akal bagi Dispose()aliran yang dilewati. Dan di situlah letak lubang dalam bahasa C # - Anda tidak dapat mengekspresikan ini dalam sistem tipe.
Martin Ba
2
"Salah satu contoh dari ini yang telah saya lihat adalah bahwa banyak pengembang C ++ akan suka menggunakan untuk loop karena mereka lebih cepat daripada foreach." Ini adalah ide yang buruk di C ++ juga. Abstraksi tanpa biaya adalah kekuatan besar C ++, dan foreach atau algoritma lainnya tidak boleh lebih lambat daripada tulisan tangan untuk loop dalam banyak kasus.
Sebastian Redl
1

Hal yang paling berbeda bagi saya adalah kecenderungan untuk segala sesuatu yang baru . Jika Anda menginginkan objek, lupa mencoba meneruskannya ke rutin dan membagikan data yang mendasarinya, tampaknya pola C # hanya untuk membuat diri Anda objek baru. Ini, secara umum, tampaknya jauh lebih dari cara C #. Pada awalnya pola ini tampak sangat tidak efisien, tetapi saya kira membuat sebagian besar objek di C # adalah operasi cepat.

Namun, ada juga kelas-kelas statis yang benar-benar mengganggu saya karena sering kali Anda mencoba membuatnya hanya untuk mengetahui Anda harus menggunakannya saja. Saya merasa tidak mudah untuk mengetahui mana yang harus digunakan.

Saya cukup senang dalam program C # untuk mengabaikannya ketika saya tidak lagi membutuhkannya, tapi ingat apa yang dikandung objek itu - umumnya Anda hanya perlu berpikir jika memiliki beberapa data 'tidak biasa', objek yang hanya terdiri dari memori akan pergi saja sendiri, orang lain harus dibuang atau Anda harus melihat cara menghancurkannya - secara manual atau dengan menggunakan blok jika tidak.

Anda akan dengan cepat mengambilnya, C # hampir tidak berbeda dari VB sehingga Anda dapat memperlakukannya sebagai alat RAD yang sama (jika itu membantu untuk memikirkan C # seperti VB.NET kemudian melakukannya, sintaksnya hanya sedikit berbeda, dan masa lalu saya menggunakan VB membuat saya menjadi pola pikir yang tepat untuk pengembangan RAD). Satu-satunya hal yang sulit adalah menentukan berapa banyak perpustakaan .net besar yang harus Anda gunakan. Google pasti teman Anda di sini!

gbjbaanb
sumber
1
Melewati objek ke rutin untuk berbagi data yang mendasarinya baik-baik saja di C #, setidaknya dari perspektif sintaksis.
Brian