Validasi parameter konstruktor dalam C # - Praktik terbaik

34

Apa praktik terbaik untuk validasi parameter konstruktor?

Misalkan sedikit C #:

public class MyClass
{
    public MyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            throw new ArgumentException("Text cannot be empty");

        // continue with normal construction
    }
}

Apakah bisa diterima untuk melemparkan pengecualian?

Alternatif yang saya temui adalah pra-validasi, sebelum membuat contoh:

public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
        {
            MessageBox.Show("Text cannot be empty");
            return null;
        }
        else
        {
            return new MyClass(text);
        }
    }
}
MPelletier
sumber
10
"dapat diterima untuk melempar pengecualian?" Apa alternatifnya?
S.Lott
10
@ S.Lott: Jangan lakukan apa pun. Tetapkan nilai default. Cetak mesasge ke konsol / logfile. Membunyikan bel. Kedipkan cahaya.
FrustratedWithFormsDesigner
3
@FrustratedWithFormsDesigner: "Jangan lakukan apa pun?" Bagaimana kendala "teks tidak kosong" dipenuhi? "Tetapkan nilai default"? Bagaimana nilai argumen teks akan digunakan? Gagasan lain baik-baik saja, tetapi keduanya akan mengarah ke objek dalam keadaan yang melanggar batasan pada kelas ini.
S.Lott
1
@ S.Lott Karena takut mengulangi sendiri, saya terbuka terhadap kemungkinan ada beberapa pendekatan lain, yang saya tidak tahu. Saya percaya saya tidak semua tahu, yang saya tahu pasti.
MPelletier
3
@ MPellier, memvalidasi parameter sebelumnya akan berakhir dengan melanggar KERING. (Jangan Ulangi Diri Anda Sendiri) Karena Anda harus menyalin pra-validasi ke kode apa pun yang mencoba membuat instance kelas ini.
CaffGeek

Jawaban:

26

Saya cenderung melakukan semua validasi saya di konstruktor. Ini adalah suatu keharusan karena saya hampir selalu membuat objek yang tidak berubah. Untuk kasus spesifik Anda, saya pikir ini dapat diterima.

if (string.IsNullOrEmpty(text))
    throw new ArgumentException("message", "text");

Jika Anda menggunakan .NET 4 Anda dapat melakukan ini. Tentu saja ini tergantung pada apakah Anda menganggap string yang hanya berisi ruang putih tidak valid.

if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("message", "text");
Kekacauan Kekacauan
sumber
Saya tidak yakin "kasus spesifik" -nya cukup spesifik untuk memberikan saran spesifik tersebut. Kami tidak tahu apakah masuk akal dalam konteks ini untuk melemparkan pengecualian atau membiarkan objek itu ada.
FrustratedWithFormsDesigner
@FrustratedWithFormsDesigner - Saya berasumsi Anda memilih saya untuk ini? Tentu saja Anda benar bahwa saya memperkirakan tetapi jelas objek teoritis ini harus tidak valid jika teks input kosong. Apakah Anda benar-benar ingin menelepon Inituntuk menerima semacam kode kesalahan ketika pengecualian akan berfungsi juga dan memaksa objek Anda untuk selalu mempertahankan status yang valid?
ChaosPandion
2
Saya cenderung membagi kasus pertama menjadi dua pengecualian terpisah: satu yang menguji nullity dan melempar ArgumentNullExceptiondan lainnya yang menguji kosong dan melempar ArgumentException. Memungkinkan penelepon lebih pilih-pilih jika mereka ingin dalam penanganan pengecualiannya.
Jesse C. Slicer
1
@Jesse - Saya sudah menggunakan teknik itu tapi saya masih di pagar tentang kegunaannya secara keseluruhan.
ChaosPandion
3
+1 untuk objek yang tidak berubah! Saya juga menggunakannya sedapat mungkin (saya pribadi menemukan hampir selalu).
22

Banyak orang menyatakan bahwa konstruktor tidak boleh melempar pengecualian. KyleG di halaman ini , misalnya, melakukan hal itu. Jujur, saya tidak bisa memikirkan alasan mengapa tidak.

Dalam C ++, melempar pengecualian dari konstruktor adalah ide yang buruk, karena meninggalkan Anda dengan memori yang dialokasikan yang berisi objek tidak diinisialisasi yang Anda tidak punya referensi (mis. Itu adalah kebocoran memori klasik). Mungkin dari situlah stigma tersebut berasal - sekelompok pengembang C ++ sekolah lama setengah-setengah mengembangkan pembelajaran C # mereka dan hanya menerapkan apa yang mereka ketahui dari C ++ ke dalamnya. Sebaliknya, di Objective-C Apple memisahkan langkah alokasi dari langkah inisialisasi, sehingga konstruktor dalam bahasa itu dapat membuang pengecualian.

C # tidak dapat membocorkan memori dari panggilan yang gagal ke konstruktor. Bahkan beberapa kelas dalam kerangka NET. Akan melempar pengecualian di konstruktor mereka.

Semut
sumber
Anda akan mengacak-acak beberapa bulu dengan Anda komentar C ++. :)
ChaosPandion
5
Ehh, C ++ adalah bahasa yang bagus, tapi salah satu yang paling membuat saya kesal adalah orang yang belajar satu bahasa dengan baik dan kemudian menulis seperti itu setiap saat . C # bukan C ++.
Semut
10
Masih berbahaya untuk membuang pengecualian dari ctor di C # karena ctor mungkin telah mengalokasikan sumber daya yang tidak dikelola yang kemudian tidak akan pernah dibuang. Dan finalizer perlu ditulis untuk menjadi defensif terhadap skenario di mana objek yang dibangun tidak lengkap diselesaikan. Tapi skenario ini relatif jarang. Artikel yang akan datang tentang C # Dev Center akan membahas skenario ini dan cara membuat kode pertahanan di sekitarnya, jadi perhatikan ruang itu untuk perinciannya.
Eric Lippert
6
eh? melempar pengecualian dari ctor adalah satu-satunya hal yang dapat Anda lakukan di c ++ jika Anda tidak dapat membangun objek, dan tidak ada alasan ini harus menyebabkan kebocoran memori (meskipun jelas itu terserah).
jk.
@ jk: Hmm, kamu benar! Meskipun tampaknya alternatifnya adalah untuk menandai objek yang buruk sebagai "zombie", yang tampaknya STL tidak menggantikan pengecualian: yosefk.com/c++fqa/exceptions.html#fqa-17.2 Solusi Objective-C jauh lebih rapi.
Semut
12

Melempar pengecualian IFF kelas tidak dapat dimasukkan ke dalam keadaan yang konsisten sehubungan dengan penggunaan semantiknya. Kalau tidak, jangan. JANGAN PERNAH membiarkan objek ada dalam keadaan tidak konsisten. Ini termasuk tidak menyediakan konstruktor lengkap (seperti memiliki konstruktor kosong + inisialisasi () sebelum objek Anda benar-benar dibangun) ... HANYA KATAKAN TIDAK!

Dalam keadaan darurat, semua orang melakukannya. Saya melakukannya kemarin untuk benda yang sangat sempit digunakan dalam lingkup yang ketat. Suatu hari nanti, saya atau orang lain mungkin akan membayar harga untuk slip itu dalam praktik.

Saya harus mencatat bahwa dengan "konstruktor" maksud saya hal yang klien panggil untuk membangun objek. Itu bisa dengan mudah menjadi sesuatu selain konstruk yang sebenarnya dengan nama "Konstruktor". Misalnya, sesuatu seperti ini di C ++ tidak akan melanggar prinsip IMNSHO:

struct funky_object
{
  ...
private:
  funky_object();
  bool initialize(std::string);

  friend boost::optional<funky_object> build_funky(std::string);
};
boost::optional<funky_object> build_funky(std::string str)
{
  funky_object fo;
  if (fo.initialize(str)) return fo;
  return boost::optional<funky_object>();
}

Karena satu-satunya cara untuk membuat funky_objectadalah dengan memanggil build_funkyprinsip tidak pernah membiarkan objek yang tidak valid tetap ada meskipun "Pembuat" yang sebenarnya tidak menyelesaikan pekerjaan.

Itu banyak pekerjaan ekstra meskipun untuk keuntungan yang dipertanyakan (bahkan mungkin kerugian). Saya masih lebih suka rute pengecualian.

Edward Strange
sumber
9

Dalam hal ini, saya akan menggunakan metode pabrik. Pada dasarnya, atur kelas Anda hanya memiliki konstruktor pribadi dan memiliki metode pabrik yang mengembalikan turunan objek Anda. Jika parameter awal tidak valid, kembalikan saja nol dan minta kode panggilan memutuskan apa yang harus dilakukan.

public class MyClass
{
    private MyClass(string text)
    {
        //normal construction
    }

    public static MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            return null;
        else
            return new MyClass(text);
    }
}
public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        var cls = MyClass.MakeMyClass(text);
        if(cls == null)
             //show messagebox or throw exception
        return cls;
    }
}

Jangan pernah melempar pengecualian kecuali kondisinya luar biasa. Saya berpikir bahwa dalam kasus ini, nilai kosong dapat diteruskan dengan mudah. Jika itu masalahnya, menggunakan pola ini akan menghindari pengecualian dan hukuman kinerja yang mereka tanggung sambil menjaga agar status MyClass tetap berlaku.

Donn Relacion
sumber
1
Saya tidak melihat keuntungannya. Mengapa menguji hasil pabrik untuk null lebih disukai daripada memiliki try-catch di suatu tempat yang dapat menangkap pengecualian dari konstruktor? Pendekatan Anda tampaknya "mengabaikan pengecualian, kembali ke memeriksa secara eksplisit nilai pengembalian dari metode untuk kesalahan". Kami telah lama menerima bahwa pengecualian umumnya lebih baik daripada harus secara eksplisit menguji setiap metode pengembalian nilai untuk kesalahan, sehingga saran Anda tampaknya mencurigakan. Saya ingin melihat beberapa alasan mengapa menurut Anda aturan umum tidak berlaku di sini.
DW
kami tidak pernah menerima bahwa pengecualian lebih baik dari kode yang dikembalikan, mereka dapat menjadi kinerja yang luar biasa jika dilemparkan terlalu sering. Namun, melemparkan pengecualian dari konstruktor akan membocorkan memori, bahkan di .NET, jika Anda telah mengalokasikan sesuatu yang tidak dikelola. Anda harus melakukan banyak pekerjaan di finaliser Anda untuk menebus kasus ini. Bagaimanapun, pabrik adalah ide yang bagus di sini, dan TBH seharusnya mengeluarkan pengecualian daripada mengembalikan nol. Dengan asumsi Anda membuat hanya beberapa kelas 'buruk', maka membuat lemparan pabrik lebih disukai.
gbjbaanb
2
  • Konstruktor tidak boleh memiliki efek samping.
    • Apa pun selain inisialisasi bidang pribadi harus dilihat sebagai efek samping.
    • Seorang konstruktor dengan efek samping menghancurkan Single-Responsibilty-Principle (SRP) dan berjalan berlawanan dengan semangat pemrograman berorientasi objek (OOP).
  • Konstruktor harus ringan dan tidak boleh gagal.
    • Sebagai contoh, saya selalu ngeri ketika saya melihat blok try-catch di dalam sebuah konstruktor. Konstruktor tidak boleh melempar pengecualian atau kesalahan log.

Seseorang dapat dengan wajar mempertanyakan pedoman ini dan berkata, "Tapi saya tidak mengikuti aturan ini, dan kode saya berfungsi dengan baik!" Untuk itu, saya akan menjawab, "Yah, itu mungkin benar, sampai tidak."

  • Pengecualian dan kesalahan di dalam konstruktor sangat tidak terduga. Kecuali mereka diperintahkan untuk melakukannya, programmer masa depan tidak akan cenderung untuk mengelilingi panggilan konstruktor ini dengan kode defensif.
  • Jika ada yang gagal dalam produksi, jejak tumpukan yang dihasilkan mungkin sulit diurai. Bagian atas tumpukan jejak mungkin menunjuk ke panggilan konstruktor, tetapi banyak hal terjadi di konstruktor, dan itu mungkin tidak menunjuk ke LOC aktual yang gagal.
    • Saya telah mem-parsing banyak jejak .NET stack di mana ini terjadi.
Jim G.
sumber
0

Itu tergantung apa yang dilakukan MyClass. Jika MyClass sebenarnya adalah kelas repositori data, dan teks parameter adalah string koneksi, maka praktik terbaik adalah melempar ArgumentException. Namun, jika MyClass adalah kelas StringBuilder (misalnya), maka Anda bisa membiarkannya kosong.

Jadi itu tergantung pada seberapa penting parameter teks untuk metode - apakah objek masuk akal dengan nilai nol atau kosong?

Steven Striga
sumber
2
Saya pikir pertanyaannya menyiratkan bahwa kelas sebenarnya membutuhkan string agar tidak kosong. Itu tidak terjadi dalam contoh StringBuilder Anda.
Sergio Acosta
Maka jawabannya harus ya - bisa diterima untuk melemparkan pengecualian. Untuk menghindari keharusan mencoba / menangkap kesalahan ini, maka Anda bisa menggunakan pola pabrik untuk menangani pembuatan objek untuk Anda, yang akan mencakup case string kosong.
Steven Striga
0

Preferensi saya adalah menetapkan default, tetapi saya tahu Java memiliki pustaka "Apache Commons" yang melakukan hal seperti ini, dan saya pikir itu ide yang cukup bagus juga. Saya tidak melihat masalah dengan melempar pengecualian jika nilai yang tidak valid akan menempatkan objek dalam keadaan tidak dapat digunakan; string bukan contoh yang baik tetapi bagaimana jika itu untuk DI orang miskin? Anda tidak dapat beroperasi jika nilai nulldilewatkan di tempat, katakanlah, ICustomerRepositoryantarmuka. Dalam situasi seperti ini, melemparkan pengecualian adalah cara yang benar dalam menangani berbagai hal, menurut saya.

Wayne Molina
sumber
-1

Ketika saya dulu bekerja di c ++, saya tidak menggunakan untuk melempar pengecualian pada konstruktor karena masalah kebocoran memori yang dapat menyebabkannya, saya telah mempelajarinya dengan cara yang sulit. Siapa pun yang bekerja dengan c ++ tahu betapa sulit dan bocornya memori bermasalah.

Tetapi jika Anda berada di c # / Java, maka Anda tidak memiliki masalah ini, karena pemulung akan mengumpulkan memori. Jika Anda menggunakan C #, saya pikir lebih baik menggunakan properti untuk cara yang bagus dan konsisten untuk memastikan bahwa batasan data dijamin.

public class MyClass
{
    private string _text;
    public string Text 
    {
        get
        {
            return _text;
        } 
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Text cannot be empty");
            _text = value;
        } 
    }

    public MyClass(string text)
    {
        Text = text;
        // continue with normal construction
    }
}
ajitdh
sumber
-3

Melempar pengecualian akan sangat menyusahkan bagi pengguna kelas Anda. Jika validasi adalah masalah maka saya biasanya membuat membuat objek proses 2 langkah.

1 - Buat instance

2 - Panggil metode Inisialisasi dengan hasil kembali.

ATAU

Panggil metode / fungsi yang menciptakan kelas untuk Anda yang dapat melakukan validasi.

ATAU

Memiliki bendera isValid, yang tidak terlalu saya sukai tetapi beberapa orang melakukannya.

Dunk
sumber
3
@Dunk, itu salah.
CaffGeek
4
@Chad, itu pendapat Anda, tapi pendapat saya tidak salah. Itu hanya berbeda dari milikmu. Saya menemukan kode try-catch jauh lebih sulit untuk dibaca dan saya telah melihat jauh lebih banyak kesalahan dibuat ketika orang menggunakan try-catch untuk penanganan kesalahan sepele daripada metode yang lebih tradisional. Itu pengalaman saya dan itu tidak salah. Itu adalah apa adanya.
Dunk
5
TITIK adalah bahwa setelah saya memanggil konstruktor, jika itu tidak membuat karena input tidak valid, itu adalah PENGECUALIAN. Data aplikasi sekarang dalam keadaan tidak konsisten / tidak valid jika saya hanya membuat objek dan menetapkan panji mengatakan isValid = false. Harus menguji setelah kelas dibuat jika valid itu mengerikan, desain sangat rawan kesalahan. Dan, Anda bilang, punya konstruktor, lalu panggil inisialisasi ... Bagaimana jika saya tidak memanggil inisialisasi? Lalu bagaimana? Dan bagaimana jika Inisialisasi mendapatkan data yang buruk, dapatkah saya melemparkan pengecualian sekarang? Kelas Anda seharusnya tidak sulit untuk digunakan.
CaffGeek
5
@Dunk, jika Anda menemukan pengecualian yang sulit digunakan, Anda salah menggunakannya. Sederhananya, input buruk diberikan kepada konstruktor. Itu pengecualian. Aplikasi seharusnya tidak terus berjalan kecuali jika sudah diperbaiki. Periode. Jika ya, Anda berakhir dengan masalah yang lebih buruk, data buruk. Jika Anda tidak tahu bagaimana menangani pengecualian, Anda tidak, Anda membiarkannya menggelembungkan tumpukan panggilan sampai dapat ditangani, bahkan jika itu berarti hanya login, dan menghentikan eksekusi. Sederhananya, Anda tidak perlu menangani pengecualian, atau mengujinya. Mereka hanya bekerja.
CaffGeek
3
Maaf, ini terlalu anti-pola untuk dipertimbangkan.
MPelletier