Mengapa saya tidak dapat menetapkan konstruktor default untuk struct di .NET?

261

Di .NET, tipe nilai (C # struct) tidak dapat memiliki konstruktor tanpa parameter. Menurut posting ini ini diamanatkan oleh spesifikasi CLI. Apa yang terjadi adalah bahwa untuk setiap tipe nilai konstruktor default dibuat (oleh kompiler?) Yang menginisialisasi semua anggota ke nol (atau null).

Mengapa tidak diizinkan untuk mendefinisikan konstruktor default seperti itu?

Satu penggunaan sepele adalah untuk bilangan rasional:

public struct Rational {
    private long numerator;
    private long denominator;

    public Rational(long num, long denom)
    { /* Todo: Find GCD etc. */ }

    public Rational(long num)
    {
        numerator = num;
        denominator = 1;
    }

    public Rational() // This is not allowed
    {
        numerator = 0;
        denominator = 1;
    }
}

Menggunakan versi C # saat ini, Rasional default adalah 0/0yang tidak begitu keren.

PS : Akankah parameter default membantu menyelesaikan ini untuk C # 4.0 atau akankah konstruktor default yang ditentukan CLR dipanggil?


Jon Skeet menjawab:

Untuk menggunakan contoh Anda, apa yang Anda inginkan terjadi ketika seseorang melakukannya:

 Rational[] fractions = new Rational[1000];

Haruskah itu berjalan melalui konstruktor Anda 1000 kali?

Tentu harus, itu sebabnya saya menulis konstruktor default di tempat pertama. CLR harus menggunakan konstruktor zeroing default ketika tidak ada konstruktor default eksplisit didefinisikan; dengan begitu Anda hanya membayar untuk apa yang Anda gunakan. Lalu jika saya ingin 1000 kontainer non-default Rational(dan ingin mengoptimalkan 1000 konstruksi), saya akan menggunakan List<Rational>bukan array.

Alasan ini, dalam pikiran saya, tidak cukup kuat untuk mencegah definisi konstruktor default.

Motti
sumber
3
+1 memiliki masalah yang sama sekali, akhirnya dikonversi struct ke dalam kelas.
Dirk Vollmar
4
Parameter default di C # 4 tidak dapat membantu karena Rational()memanggil ctor parameterless daripada Rational(long num=0, long denom=1).
LaTeX
6
Perhatikan bahwa dalam C # 6.0 yang datang dengan Visual Studio 2015 itu akan diizinkan untuk menulis konstruktor instance nol parameter untuk struct. Jadi new Rational()akan memanggil konstruktor jika ada, namun jika tidak ada, new Rational()akan setara dengan default(Rational). Bagaimanapun Anda didorong untuk menggunakan sintaks default(Rational)ketika Anda ingin "nilai nol" dari struct Anda (yang merupakan angka "buruk" dengan desain yang Anda usulkan Rational). Nilai default untuk tipe nilai Tselalu default(T). Jadi new Rational[1000]tidak akan pernah memanggil konstruktor struct.
Jeppe Stig Nielsen
6
Untuk mengatasi masalah khusus ini, Anda dapat menyimpan denominator - 1di dalam struct, sehingga nilai default menjadi 0/1
miniBill
3
Then if I want a container of 1000 non-default Rationals (and want to optimize away the 1000 constructions) I will use a List<Rational> rather than an array.Mengapa Anda mengharapkan array untuk memanggil konstruktor yang berbeda ke Daftar untuk struct?
mjwills

Jawaban:

197

Catatan: jawaban di bawah ini ditulis jauh sebelum C # 6, yang berencana memperkenalkan kemampuan untuk mendeklarasikan konstruktor tanpa parameter dalam struct - tetapi mereka masih tidak akan dipanggil dalam semua situasi (misalnya untuk pembuatan array) (pada akhirnya fitur ini tidak ditambahkan ke C # 6 ).


EDIT: Saya sudah mengedit jawaban di bawah ini karena wawasan Grauenwolf tentang CLR.

CLR memungkinkan tipe nilai memiliki konstruktor tanpa parameter, tetapi C # tidak. Saya percaya ini karena itu akan memperkenalkan harapan bahwa konstruktor akan dipanggil ketika tidak. Sebagai contoh, pertimbangkan ini:

MyStruct[] foo = new MyStruct[1000];

CLR mampu melakukan ini dengan sangat efisien hanya dengan mengalokasikan memori yang sesuai dan memusatkan semuanya. Jika harus menjalankan konstruktor MyStruct 1000 kali, itu akan jauh lebih efisien. (Bahkan, tidak - jika Anda melakukan memiliki konstruktor parameterless, itu tidak bisa berjalan ketika Anda membuat sebuah array, atau ketika Anda memiliki variabel contoh diinisiasi.)

Aturan dasar dalam C # adalah "nilai default untuk semua jenis tidak dapat bergantung pada inisialisasi apa pun". Sekarang mereka bisa membiarkan konstruktor tanpa parameter didefinisikan, tetapi kemudian tidak mengharuskan konstruktor untuk dieksekusi dalam semua kasus - tetapi itu akan menyebabkan lebih banyak kebingungan. (Atau setidaknya, jadi saya percaya argumennya masuk.)

EDIT: Untuk menggunakan contoh Anda, apa yang Anda inginkan terjadi ketika seseorang melakukannya:

Rational[] fractions = new Rational[1000];

Haruskah itu berjalan melalui konstruktor Anda 1000 kali?

  • Jika tidak, kami berakhir dengan 1000 rasional yang tidak valid
  • Jika ya, maka kita berpotensi menyia-nyiakan banyak pekerjaan jika kita akan mengisi array dengan nilai-nilai nyata.

EDIT: (Menjawab lebih banyak pertanyaan) Konstruktor tanpa parameter tidak dibuat oleh kompiler. Tipe nilai tidak harus memiliki konstruktor sejauh menyangkut CLR - meskipun ternyata bisa jika Anda menulisnya di IL. Ketika Anda menulis " new Guid()" dalam C # yang memancarkan IL berbeda dengan apa yang Anda dapatkan jika Anda memanggil konstruktor normal. Lihat pertanyaan SO ini untuk sedikit lebih banyak tentang aspek itu.

Saya menduga bahwa tidak ada tipe nilai dalam kerangka kerja dengan konstruktor tanpa parameter. Tidak diragukan lagi NDepend dapat memberi tahu saya jika saya memintanya dengan cukup baik ... Fakta bahwa C # melarang itu adalah petunjuk yang cukup besar bagi saya untuk berpikir itu mungkin ide yang buruk.

Jon Skeet
sumber
9
Penjelasan lebih pendek: Di C ++, struct dan kelas hanyalah dua sisi dari koin yang sama. Satu-satunya perbedaan nyata adalah yang pertama bersifat publik dan yang lainnya pribadi. Di .Net, ada perbedaan yang jauh lebih besar antara struct dan kelas, dan penting untuk memahaminya.
Joel Coehoorn
39
@ Joel: Itu tidak benar-benar menjelaskan batasan khusus ini, kan?
Jon Skeet
6
CLR memungkinkan tipe nilai memiliki konstruktor tanpa parameter. Dan ya, itu akan menjalankannya untuk setiap elemen dalam sebuah array. C # berpikir ini adalah ide yang buruk dan tidak mengizinkannya, tetapi Anda dapat menulis bahasa .NET yang mendukung.
Jonathan Allen
2
Maaf, saya agak bingung dengan yang berikut ini. ApakahRational[] fractions = new Rational[1000]; juga menyia-nyiakan banyak pekerjaan jika Rationalkelas bukan sebuah struct? Jika demikian, mengapa kelas memiliki ctor default?
Cium ketiakku
5
@ FifaEarthCup2014: Anda harus lebih spesifik tentang apa yang Anda maksud dengan "buang banyak pekerjaan". Tetapi jika demikian, itu tidak akan memanggil konstruktor 1000 kali. Jika Rationalkelas, Anda akan berakhir dengan 1000 referensi referensi kosong.
Jon Skeet
48

Str adalah tipe nilai dan tipe nilai harus memiliki nilai default segera setelah dideklarasikan.

MyClass m;
MyStruct m2;

Jika Anda mendeklarasikan dua bidang seperti di atas tanpa membuat instance, maka pecahkan debugger, makan menjadi nol tetapi m2tidak akan. Mengingat ini, konstruktor tanpa parameter tidak masuk akal, pada kenyataannya semua konstruktor pada struct tidak memberikan nilai, hal itu sendiri sudah ada hanya dengan mendeklarasikannya. Memang m2 dapat dengan senang hati digunakan dalam contoh di atas dan metodenya disebut, jika ada, dan bidang serta propertinya dimanipulasi!

pengguna42467
sumber
3
Tidak yakin mengapa seseorang menolak Anda. Tampaknya Anda adalah jawaban yang paling benar di sini.
pipTheGeek
12
Perilaku dalam C ++ adalah bahwa jika suatu tipe memiliki konstruktor default maka yang digunakan ketika objek tersebut dibuat tanpa konstruktor eksplisit. Ini bisa digunakan dalam C # untuk menginisialisasi m2 dengan konstruktor default yang mengapa jawaban ini tidak membantu.
Motti
3
onester: jika Anda tidak ingin struct memanggil konstruktor mereka sendiri ketika dideklarasikan, maka jangan mendefinisikan konstruktor default seperti itu! :) itu kata Motti
Stefan Monov
8
@Tarik. Saya tidak setuju. Sebaliknya, konstruktor tanpa parameter akan masuk akal: jika saya ingin membuat struct "Matrix" yang selalu memiliki matriks identitas sebagai nilai default, bagaimana Anda bisa melakukannya dengan cara lain?
Elo
1
Saya tidak yakin saya sepenuhnya setuju dengan "Memang m2 bisa dengan senang hati digunakan .." . Itu mungkin benar dalam C # sebelumnya tetapi merupakan kesalahan kompiler untuk mendeklarasikan struct, bukan newitu, lalu coba gunakan anggotanya
Caius Jard
18

Meskipun CLR memungkinkannya, C # tidak mengizinkan struct untuk memiliki konstruktor tanpa parameter default. Alasannya adalah bahwa, untuk tipe nilai, kompiler secara default tidak menghasilkan konstruktor default, juga tidak menghasilkan panggilan ke konstruktor default. Jadi, bahkan jika Anda mendefinisikan konstruktor default, itu tidak akan dipanggil, dan itu hanya akan membingungkan Anda.

Untuk menghindari masalah seperti itu, kompiler C # melarang definisi konstruktor default oleh pengguna. Dan karena itu tidak menghasilkan konstruktor default, Anda tidak bisa menginisialisasi bidang ketika mendefinisikannya.

Atau alasan besarnya adalah bahwa suatu struktur adalah tipe nilai dan tipe nilai diinisialisasi dengan nilai default dan konstruktor digunakan untuk inisialisasi.

Anda tidak harus instantiate struct Anda dengan newkata kunci. Alih-alih bekerja seperti int; Anda dapat langsung mengaksesnya.

Structs tidak dapat mengandung konstruktor tanpa parameter eksplisit. Anggota Struct secara otomatis diinisialisasi ke nilai default mereka.

Konstruktor (parameter-kurang) default untuk struct dapat menetapkan nilai yang berbeda dari keadaan all-zeroed yang akan menjadi perilaku tak terduga. Oleh karena itu .NET runtime melarang konstruktor default untuk sebuah struct.

Adiii
sumber
Jawaban ini sejauh ini adalah yang terbaik. Pada akhirnya inti dari pembatasan adalah untuk menghindari kejutan seperti MyStruct s;tidak memanggil konstruktor default yang Anda berikan.
talles
1
Terima kasih atas penjelasannya. Jadi itu hanya kekurangan kompiler yang harus ditingkatkan, tidak ada alasan teoritis untuk melarang konstruktor tanpa parameter (segera setelah mereka dapat dibatasi hanya untuk mengakses properti).
Elo
16

Anda dapat membuat properti statis yang menginisialisasi dan mengembalikan nomor "rasional" default:

public static Rational One => new Rational(0, 1); 

Dan gunakan seperti:

var rat = Rational.One;
AustinWBryan
sumber
24
Dalam hal ini, Rational.Zeromungkin akan sedikit membingungkan.
Kevin
13

Penjelasan singkat:

Dalam C ++, struct dan kelas hanyalah dua sisi dari koin yang sama. Satu-satunya perbedaan nyata adalah bahwa yang satu bersifat publik dan yang lainnya bersifat pribadi.

Di .NET , ada perbedaan yang jauh lebih besar antara struct dan kelas. Yang utama adalah bahwa struct menyediakan semantik tipe-nilai, sementara kelas menyediakan semantik tipe-referensi. Ketika Anda mulai memikirkan implikasi dari perubahan ini, perubahan lain mulai lebih masuk akal juga, termasuk perilaku konstruktor yang Anda gambarkan.

Joel Coehoorn
sumber
7
Anda harus menjadi sedikit lebih eksplisit tentang bagaimana ini tersirat oleh nilai vs tipe split split Saya tidak mengerti ...
Motti
Jenis nilai memiliki nilai default - mereka bukan nol, bahkan jika Anda tidak mendefinisikan konstruktor. Sementara pada pandangan pertama ini tidak menghalangi juga mendefinisikan konstruktor default, kerangka kerja menggunakan fitur ini internal untuk membuat asumsi tertentu tentang struct.
Joel Coehoorn
@annakata: Konstruktor lain mungkin berguna dalam beberapa skenario yang melibatkan Refleksi. Juga, jika obat generik pernah ditingkatkan untuk memungkinkan batasan "baru" yang diparameterisasi, akan berguna untuk memiliki struct yang dapat mematuhinya.
supercat
@annakata Saya percaya itu karena C # memiliki persyaratan kuat tertentu yang newbenar - benar harus ditulis untuk memanggil konstruktor. Dalam C ++ konstruktor dipanggil dengan cara tersembunyi, pada deklarasi atau instanciation array. Dalam C # baik semuanya adalah pointer jadi mulai dari nol, baik itu sebuah struct dan harus memulai sesuatu, tetapi ketika Anda tidak dapat menulis new... (seperti array init), itu akan melanggar aturan C # yang kuat.
v.oddou
3

Saya belum melihat yang setara dengan solusi akhir yang akan saya berikan, jadi ini dia.

gunakan offset untuk memindahkan nilai dari default 0 ke nilai apa pun yang Anda suka. di sini properti harus digunakan alih-alih langsung mengakses bidang. (mungkin dengan fitur c # 7 yang mungkin Anda lebih baik mendefinisikan bidang cakupan properti sehingga mereka tetap terlindungi dari akses langsung dalam kode.)

Solusi ini berfungsi untuk struct sederhana dengan hanya tipe nilai (tanpa tipe ref atau nullable struct).

public struct Tempo
{
    const double DefaultBpm = 120;
    private double _bpm; // this field must not be modified other than with its property.

    public double BeatsPerMinute
    {
        get => _bpm + DefaultBpm;
        set => _bpm = value - DefaultBpm;
    }
}

Ini berbeda dari jawaban ini, pendekatan ini bukan casing utama tetapi menggunakan offset yang akan bekerja untuk semua rentang.

contoh dengan enums sebagai bidang.

public struct Difficaulty
{
    Easy,
    Medium,
    Hard
}

public struct Level
{
    const Difficaulty DefaultLevel = Difficaulty.Medium;
    private Difficaulty _level; // this field must not be modified other than with its property.

    public Difficaulty Difficaulty
    {
        get => _level + DefaultLevel;
        set => _level = value - DefaultLevel;
    }
}

Seperti yang saya katakan trik ini mungkin tidak berfungsi dalam semua kasus, bahkan jika struct hanya memiliki bidang nilai, hanya Anda yang tahu jika itu berfungsi dalam kasus Anda atau tidak. hanya memeriksa. tetapi Anda mendapatkan ide umum.

M.kazem Akhgary
sumber
Ini adalah solusi yang baik untuk contoh yang saya berikan tetapi itu benar-benar hanya seharusnya menjadi contoh, pertanyaannya adalah umum.
Motti
2

Hanya kasus khusus itu. Jika Anda melihat pembilang 0 dan penyebut 0, berpura-puralah memiliki nilai yang Anda inginkan.

Jonathan Allen
sumber
5
Saya pribadi tidak akan menyukai kelas / struct saya untuk memiliki perilaku semacam ini. Gagal diam-diam (atau pulih dengan cara perkiraan dev terbaik untuk Anda) adalah jalan menuju kesalahan yang tidak tertangkap.
Boris Callens
2
+1 Ini adalah jawaban yang bagus, karena untuk tipe nilai, Anda harus memperhitungkan nilai defaultnya. Ini memungkinkan Anda "mengatur" nilai default dengan perilakunya.
IllidanS4 ingin Monica kembali
Ini persis bagaimana mereka mengimplementasikan kelas-kelas seperti Nullable<T>(misalnya int?).
Jonathan Allen
Itu ide yang sangat buruk. 0/0 harus selalu berupa pecahan yang tidak valid (NaN). Bagaimana jika seseorang memanggil di new Rational(x,y)mana x dan y adalah 0?
Mike Rosoft
Jika Anda memiliki konstruktor aktual, maka Anda dapat melempar pengecualian, mencegah 0/0 yang sebenarnya terjadi. Atau jika Anda ingin itu terjadi, Anda harus menambahkan bool tambahan untuk membedakan antara default dan 0/0.
Jonathan Allen
2

Apa yang saya gunakan adalah operator null-penggabungan (??) dikombinasikan dengan bidang dukungan seperti ini:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

  public SomeRefType MyRefVariable {
    get { return m_MyRefVariableBackingField ?? (m_MyRefVariableBackingField = new SomeRefType()); }
  }
}

Semoga ini membantu ;)

Catatan: tugas penggabungan nol saat ini adalah proposal fitur untuk C # 8.0.

Axel Samyn
sumber
1

Anda tidak dapat menentukan konstruktor default karena Anda menggunakan C #.

Structs dapat memiliki konstruktor default di .NET, meskipun saya tidak tahu bahasa tertentu yang mendukungnya.

Jonathan Allen
sumber
Dalam C #, kelas dan struct secara semantik berbeda. Str adalah tipe nilai, sedangkan kelas adalah tipe referensi.
Tom Sarduy
-1

Inilah solusi saya untuk dilema konstruktor default. Saya tahu ini adalah solusi yang terlambat, tetapi saya pikir perlu dicatat bahwa ini adalah solusi.

public struct Point2D {
    public static Point2D NULL = new Point2D(-1,-1);
    private int[] Data;

    public int X {
        get {
            return this.Data[ 0 ];
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 0 ] = value;
            }
        }
    }

    public int Z {
        get {
            return this.Data[ 1 ];
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new int[ 2 ];
            } finally {
                this.Data[ 1 ] = value;
            }
        }
    }

    public Point2D( int x , int z ) {
        this.Data = new int[ 2 ] { x , z };
    }

    public static Point2D operator +( Point2D A , Point2D B ) {
        return new Point2D( A.X + B.X , A.Z + B.Z );
    }

    public static Point2D operator -( Point2D A , Point2D B ) {
        return new Point2D( A.X - B.X , A.Z - B.Z );
    }

    public static Point2D operator *( Point2D A , int B ) {
        return new Point2D( B * A.X , B * A.Z );
    }

    public static Point2D operator *( int A , Point2D B ) {
        return new Point2D( A * B.Z , A * B.Z );
    }

    public override string ToString() {
        return string.Format( "({0},{1})" , this.X , this.Z );
    }
}

mengabaikan fakta bahwa saya memiliki struct statis yang disebut null, (Catatan: Ini hanya untuk semua kuadran positif), menggunakan get; set; di C #, Anda dapat mencoba / menangkap / akhirnya, untuk menangani kesalahan di mana tipe data tertentu tidak diinisialisasi oleh konstruktor default Point2D (). Saya kira ini sulit dipahami sebagai solusi untuk beberapa orang pada jawaban ini. Itu sebabnya saya menambahkan milik saya. Menggunakan fungsionalitas pengambil dan penyetel dalam C # akan memungkinkan Anda untuk memotong konstruktor default ini tidak masuk akal dan mencoba menangkap apa yang tidak Anda inisialisasi. Bagi saya ini berfungsi dengan baik, untuk orang lain Anda mungkin ingin menambahkan beberapa pernyataan if. Jadi, Jika Anda menginginkan pengaturan Numerator / Denominator, kode ini mungkin membantu. Saya hanya ingin menegaskan kembali bahwa solusi ini tidak terlihat bagus, mungkin bekerja lebih buruk dari sudut pandang efisiensi, tetapi, untuk seseorang yang datang dari versi C # yang lebih lama, menggunakan tipe data array memberi Anda fungsi ini. Jika Anda hanya menginginkan sesuatu yang berfungsi, coba ini:

public struct Rational {
    private long[] Data;

    public long Numerator {
        get {
            try {
                return this.Data[ 0 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 0 ];
            }
        }
        set {
            try {
                this.Data[ 0 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 0 ] = value;
            }
        }
    }

    public long Denominator {
        get {
            try {
                return this.Data[ 1 ];
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                return this.Data[ 1 ];
            }
        }
        set {
            try {
                this.Data[ 1 ] = value;
            } catch( Exception ) {
                this.Data = new long[ 2 ] { 0 , 1 };
                this.Data[ 1 ] = value;
            }
        }
    }

    public Rational( long num , long denom ) {
        this.Data = new long[ 2 ] { num , denom };
        /* Todo: Find GCD etc. */
    }

    public Rational( long num ) {
        this.Data = new long[ 2 ] { num , 1 };
        this.Numerator = num;
        this.Denominator = 1;
    }
}
G1xb17
sumber
2
Ini kode yang sangat buruk. Mengapa Anda memiliki referensi array di sebuah struct? Mengapa Anda tidak memiliki koordinat X dan Y sebagai bidang? Dan menggunakan pengecualian untuk kontrol aliran adalah ide yang buruk; Anda biasanya harus menulis kode Anda sedemikian rupa sehingga NullReferenceException tidak pernah terjadi. Jika Anda benar-benar membutuhkan ini - meskipun konstruk seperti itu akan lebih cocok untuk kelas daripada struct - maka Anda harus menggunakan inisialisasi malas. (Dan secara teknis, Anda - benar-benar tidak perlu di setiap pengaturan kecuali yang pertama dari koordinat - mengatur setiap koordinat dua kali.)
Mike Rosoft
-1
public struct Rational 
{
    private long numerator;
    private long denominator;

    public Rational(long num = 0, long denom = 1)   // This is allowed!!!
    {
        numerator   = num;
        denominator = denom;
    }
}
eMeL
sumber
5
Itu diizinkan tetapi tidak digunakan ketika tidak ada parameter yang ditentukan ideone.com/xsLloQ
Motti