Warum kann ich in .NET keinen Standardkonstruktor für eine Struktur definieren?


261

In .NET kann ein Werttyp (C # struct) keinen Konstruktor ohne Parameter haben. Laut diesem Beitrag ist dies in der CLI-Spezifikation vorgeschrieben. Was passiert ist, dass für jeden Werttyp ein Standardkonstruktor erstellt wird (vom Compiler?), Der alle Mitglieder auf Null (oder null) initialisiert .

Warum ist es nicht erlaubt, einen solchen Standardkonstruktor zu definieren?

Eine triviale Verwendung sind rationale Zahlen:

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;
    }
}

Bei Verwendung der aktuellen Version von C # ist ein Standard-Rational 0/0nicht so cool.

PS : Helfen Standardparameter bei der Lösung dieses Problems für C # 4.0 oder wird der CLR-definierte Standardkonstruktor aufgerufen?


Jon Skeet antwortete:

Um Ihr Beispiel zu verwenden: Was möchten Sie tun, wenn jemand Folgendes tut:

 Rational[] fractions = new Rational[1000];

Sollte es 1000 Mal durch Ihren Konstruktor laufen?

Sicher sollte es, deshalb habe ich zuerst den Standardkonstruktor geschrieben. Die CLR sollte den Standard-Nullungskonstruktor verwenden, wenn kein expliziter Standardkonstruktor definiert ist. Auf diese Weise zahlen Sie nur für das, was Sie verwenden. Wenn ich dann einen Container mit 1000 nicht standardmäßigen Rationals möchte (und die 1000 Konstruktionen optimieren möchte), verwende ich List<Rational>eher ein als ein Array.

Dieser Grund ist meiner Meinung nach nicht stark genug, um die Definition eines Standardkonstruktors zu verhindern.


3
+1 hatte einmal ein ähnliches Problem und konvertierte schließlich die Struktur in eine Klasse.
Dirk Vollmar

4
Die Standardparameter in C # 4 können nicht helfen, da Rational()der parameterlose ctor anstelle des aufgerufen wird Rational(long num=0, long denom=1).
LaTeX

6
Beachten Sie, dass in C # 6.0, das mit Visual Studio 2015 geliefert wird, Instanzkonstruktoren mit Nullparametern für Strukturen geschrieben werden dürfen. So new Rational()wird der Konstruktor aufrufen , wenn es vorhanden ist , aber wenn es nicht existiert new Rational()wird äquivalent default(Rational). In jedem Fall werden Sie aufgefordert, die Syntax zu verwenden, default(Rational)wenn Sie den "Nullwert" Ihrer Struktur wünschen (was eine "schlechte" Zahl bei Ihrem vorgeschlagenen Design von ist Rational). Der Standardwert für einen Werttyp Tist immer default(T). So new Rational[1000]wird nie invoke struct Konstrukteure.
Jeppe Stig Nielsen

6
Um dieses spezielle Problem zu lösen, können Sie es denominator - 1in der Struktur speichern , sodass der Standardwert 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.Warum sollte ein Array einen anderen Konstruktor für eine Liste für eine Struktur aufrufen?
mjwills

Antworten:


197

Hinweis: Die folgende Antwort wurde lange vor C # 6 geschrieben, das die Möglichkeit einführen soll, parameterlose Konstruktoren in Strukturen zu deklarieren. Sie werden jedoch nicht in allen Situationen (z. B. zur Array-Erstellung) (am Ende ) aufgerufen Diese Funktion wurde nicht zu C # 6 hinzugefügt .


BEARBEITEN: Ich habe die Antwort unten aufgrund von Grauenwolfs Einblick in die CLR bearbeitet.

Die CLR ermöglicht es Werttypen, parameterlose Konstruktoren zu haben, C # jedoch nicht. Ich glaube, das liegt daran, dass dadurch die Erwartung entsteht, dass der Konstruktor aufgerufen wird, wenn dies nicht der Fall ist. Betrachten Sie zum Beispiel Folgendes:

MyStruct[] foo = new MyStruct[1000];

Die CLR kann dies sehr effizient tun, indem sie den entsprechenden Speicher zuweist und alles auf Null setzt. Wenn der MyStruct-Konstruktor 1000 Mal ausgeführt werden müsste, wäre dies viel weniger effizient. (In der Tat tut es nicht - wenn Sie tun einen parameterlosen Konstruktor haben, ist es nicht laufen lassen , wenn Sie ein Array zu erstellen, oder wenn Sie eine nicht initialisierte Instanz - Variable.)

Die Grundregel in C # lautet "Der Standardwert für einen Typ kann sich nicht auf eine Initialisierung stützen". Jetzt sind sie konnten erlaubt parameterlos Bauer definiert werden, stellen jedoch dann nicht erforderlich , dass Konstruktor in allen Fällen ausgeführt werden - aber das würde zu mehr Verwirrung geführt haben. (Oder zumindest, also glaube ich, dass das Argument geht.)

BEARBEITEN: Um Ihr Beispiel zu verwenden, was möchten Sie tun, wenn jemand Folgendes tat:

Rational[] fractions = new Rational[1000];

Sollte es 1000 Mal durch Ihren Konstruktor laufen?

  • Wenn nicht, erhalten wir 1000 ungültige Rationals
  • Wenn dies der Fall ist, haben wir möglicherweise eine Menge Arbeit verschwendet, wenn wir das Array mit echten Werten füllen möchten.

EDIT: (Beantwortet ein bisschen mehr der Frage) Der parameterlose Konstruktor wird nicht vom Compiler erstellt. Werttypen müssen für die CLR keine Konstruktoren haben - obwohl sich herausstellt, dass dies möglich ist, wenn Sie sie in IL schreiben. Wenn Sie " new Guid()" in C # schreiben , wird eine andere IL ausgegeben als beim Aufruf eines normalen Konstruktors. In dieser SO-Frage finden Sie weitere Informationen zu diesem Aspekt.

Ich vermute, dass es im Parameter mit parameterlosen Konstruktoren keine Werttypen gibt. Zweifellos könnte NDepend mir sagen, ob ich es nett genug gefragt habe ... Die Tatsache, dass C # es verbietet, ist ein Hinweis, der groß genug ist, um zu glauben, dass es wahrscheinlich eine schlechte Idee ist.


9
Kürzere Erklärung: In C ++ waren Struktur und Klasse nur zwei Seiten derselben Medaille. Der einzige wirkliche Unterschied ist, dass einer standardmäßig öffentlich und der andere privat war. In .Net gibt es einen viel größeren Unterschied zwischen einer Struktur und einer Klasse, und es ist wichtig, ihn zu verstehen.
Joel Coehoorn

38
@ Joel: Das erklärt diese Einschränkung allerdings nicht wirklich, oder?
Jon Skeet

6
Die CLR erlaubt es Werttypen, parameterlose Konstruktoren zu haben. Und ja, es wird für jedes einzelne Element in einem Array ausgeführt. C # hält dies für eine schlechte Idee und lässt sie nicht zu, aber Sie könnten eine .NET-Sprache schreiben, die dies tut.
Jonathan Allen

2
Entschuldigung, ich bin etwas verwirrt mit Folgendem. Hat Rational[] fractions = new Rational[1000];auch eine Last von Arbeit verschwenden , wenn Rationaleine Klasse anstelle einer Struktur ist? Wenn ja, warum haben Klassen einen Standard-Ctor?
Küssen Sie meine Achselhöhle

5
@ FifaEarthCup2014: Sie müssten genauer sagen, was Sie unter "Verschwendung einer Menge Arbeit" verstehen. In beiden Fällen wird der Konstruktor jedoch nicht 1000 Mal aufgerufen. Wenn Rationales sich um eine Klasse handelt, erhalten Sie ein Array mit 1000 Nullreferenzen.
Jon Skeet

48

Eine Struktur ist ein Werttyp und ein Werttyp muss einen Standardwert haben, sobald er deklariert wird.

MyClass m;
MyStruct m2;

Wenn Sie zwei Felder wie oben deklarieren, ohne sie zu instanziieren, brechen Sie den Debugger, mist null, aber m2nicht. In Anbetracht dessen würde ein parameterloser Konstruktor keinen Sinn ergeben. Tatsächlich weist jeder Konstruktor einer Struktur nur Werte zu. Das Ding selbst existiert bereits, indem es nur deklariert wird. Tatsächlich könnte m2 im obigen Beispiel sehr gerne verwendet werden und seine Methoden, falls vorhanden, aufgerufen und seine Felder und Eigenschaften manipuliert werden!


3
Ich bin mir nicht sicher, warum dich jemand abgewählt hat. Sie scheinen hier die richtigste Antwort zu sein.
pipTheGeek

12
Das Verhalten in C ++ ist, dass wenn ein Typ einen Standardkonstruktor hat, dieser verwendet wird, wenn ein solches Objekt ohne einen expliziten Konstruktor erstellt wird. Dies könnte in C # verwendet worden sein, um m2 mit dem Standardkonstruktor zu initialisieren, weshalb diese Antwort nicht hilfreich ist.
Motti

3
onester: Wenn Sie nicht möchten, dass die Strukturen bei der Deklaration ihren eigenen Konstruktor aufrufen, definieren Sie keinen solchen Standardkonstruktor! :) das ist
Mottis

8
@ Tarik. Ich stimme nicht zu. Im Gegenteil, ein parameterloser Konstruktor wäre durchaus sinnvoll: Wenn ich eine "Matrix" -Struktur erstellen möchte, die immer eine Identitätsmatrix als Standardwert hat, wie können Sie dies auf andere Weise tun?
Elo

1
Ich bin mir nicht sicher, ob ich dem "In der Tat könnte m2 sehr gerne verwendet werden .." voll und ganz zustimme . Es mag in einem früheren C # wahr gewesen sein, aber es ist ein Compilerfehler, eine Struktur zu deklarieren, nicht newIt, und dann zu versuchen, ihre Mitglieder zu verwenden
Caius Jard

18

Obwohl die CLR dies zulässt, erlaubt C # nicht, dass Strukturen einen standardmäßigen parameterlosen Konstruktor haben. Der Grund dafür ist, dass Compiler für einen Werttyp standardmäßig weder einen Standardkonstruktor generieren noch einen Aufruf des Standardkonstruktors generieren. Selbst wenn Sie einen Standardkonstruktor definiert haben, wird dieser nicht aufgerufen, und das verwirrt Sie nur.

Um solche Probleme zu vermeiden, lässt der C # -Compiler die Definition eines Standardkonstruktors durch den Benutzer nicht zu. Und da kein Standardkonstruktor generiert wird, können Sie beim Definieren keine Felder initialisieren.

Oder der große Grund ist, dass eine Struktur ein Werttyp ist und Werttypen durch einen Standardwert initialisiert werden und der Konstruktor für die Initialisierung verwendet wird.

Sie müssen Ihre Struktur nicht mit dem newSchlüsselwort instanziieren . Es funktioniert stattdessen wie ein Int; Sie können direkt darauf zugreifen.

Strukturen dürfen keine expliziten parameterlosen Konstruktoren enthalten. Strukturelemente werden automatisch auf ihre Standardwerte initialisiert.

Ein Standardkonstruktor (ohne Parameter) für eine Struktur kann andere Werte als den Status "Null" festlegen, was ein unerwartetes Verhalten wäre. Die .NET-Laufzeit verbietet daher Standardkonstruktoren für eine Struktur.


Diese Antwort ist bei weitem die beste. Am Ende geht es darum, Überraschungen zu vermeiden, MyStruct s;indem Sie nicht den von Ihnen bereitgestellten Standardkonstruktor aufrufen.
Talles

1
Vielen Dank für die Erklärung. Es ist also nur ein Compiler-Mangel, der verbessert werden müsste. Es gibt keinen theoretisch guten Grund, parameterlose Konstruktoren zu verbieten (sobald sie nur auf Zugriffseigenschaften beschränkt werden könnten).
Elo

16

Sie können eine statische Eigenschaft erstellen, die eine standardmäßige "rationale" Nummer initialisiert und zurückgibt:

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

Und benutze es wie:

var rat = Rational.One;

24
In diesem Fall Rational.Zerokönnte etwas weniger verwirrend sein.
Kevin

13

Kürzere Erklärung:

In C ++ waren Struktur und Klasse nur zwei Seiten derselben Medaille. Der einzige wirkliche Unterschied besteht darin, dass einer standardmäßig öffentlich und der andere privat war.

In .NET gibt es einen viel größeren Unterschied zwischen einer Struktur und einer Klasse. Die Hauptsache ist, dass struct eine Semantik vom Werttyp bereitstellt, während class eine Semantik vom Referenztyp bereitstellt. Wenn Sie über die Auswirkungen dieser Änderung nachdenken, sind auch andere Änderungen sinnvoller, einschließlich des von Ihnen beschriebenen Konstruktorverhaltens.


7
Sie müssen etwas expliziter darüber sein, wie dies durch die Aufteilung von Wert und Referenztyp impliziert wird. Ich verstehe es nicht ...
Motti

Werttypen haben einen Standardwert - sie sind nicht null, auch wenn Sie keinen Konstruktor definieren. Während dies auf den ersten Blick nicht ausschließt, auch einen Standardkonstruktor zu definieren, verwendet das Framework diese Funktion intern, um bestimmte Annahmen über Strukturen zu treffen.
Joel Coehoorn

@annakata: Andere Konstruktoren sind wahrscheinlich in einigen Szenarien mit Reflection nützlich. Wenn Generika jemals erweitert würden, um eine parametrisierte "neue" Einschränkung zu ermöglichen, wäre es nützlich, Strukturen zu haben, die diesen entsprechen könnten.
Supercat

@annakata Ich glaube, das liegt daran, dass C # eine besonders starke Anforderung hat, die newwirklich geschrieben werden muss, um einen Konstruktor aufzurufen. In C ++ werden Konstruktoren auf versteckte Weise aufgerufen, bei der Deklaration oder Instanziierung von Arrays. In C # ist entweder alles ein Zeiger, also fangen Sie bei null an, oder es ist eine Struktur und muss bei etwas beginnen, aber wenn Sie nicht schreiben können new... (wie Array-Init), würde dies eine starke C # -Regel brechen.
v.oddou

3

Ich habe kein Äquivalent zu einer späten Lösung gesehen, die ich geben werde, also hier ist es.

Verwenden Sie Offsets, um Werte von Standard 0 in einen beliebigen Wert zu verschieben. Hier müssen Eigenschaften verwendet werden, anstatt direkt auf Felder zuzugreifen. (Möglicherweise definieren Sie mit der möglichen Funktion c # 7 besser Felder mit Eigenschaftsbereich, damit sie nicht direkt im Code aufgerufen werden können.)

Diese Lösung funktioniert für einfache Strukturen mit nur Werttypen (kein Ref-Typ oder nullfähige Struktur).

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;
    }
}

Dies ist anders als diese Antwort, dieser Ansatz nicht besonders Gehäuse ist aber die Verwendung eines Offset , die für alle Bereiche arbeiten.

Beispiel mit Aufzählungen als Feld.

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;
    }
}

Wie gesagt, dieser Trick funktioniert möglicherweise nicht in allen Fällen, auch wenn struct nur Wertefelder enthält. Nur Sie wissen, ob er in Ihrem Fall funktioniert oder nicht. einfach untersuchen. aber Sie bekommen die allgemeine Idee.


Dies ist eine gute Lösung für das Beispiel, das ich gegeben habe, aber es sollte eigentlich nur ein Beispiel sein, die Frage ist allgemein.
Motti

2

Nur Sonderfall. Wenn Sie einen Zähler von 0 und einen Nenner von 0 sehen, tun Sie so, als hätte er die Werte, die Sie wirklich wollen.


5
Ich persönlich möchte nicht, dass meine Klassen / Strukturen sich so verhalten. Wenn Sie stillschweigend versagen (oder sich so erholen, wie es der Entwickler für Sie am besten vermutet), ist dies der Weg zu nicht erfassten Fehlern.
Boris Callens

2
+1 Dies ist eine gute Antwort, da Sie für Werttypen deren Standardwert berücksichtigen müssen. Auf diese Weise können Sie den Standardwert mit seinem Verhalten "festlegen".
IllidanS4 will Monica

Genau so implementieren sie Klassen wie Nullable<T>(zB int?).
Jonathan Allen

Das ist eine sehr schlechte Idee. 0/0 sollte immer eine ungültige Fraktion (NaN) sein. Was ist, wenn jemand anruft, new Rational(x,y)wo x und y zufällig 0 sind?
Mike Rosoft

Wenn Sie einen tatsächlichen Konstruktor haben, können Sie eine Ausnahme auslösen, um zu verhindern, dass eine echte 0/0 auftritt. Oder wenn Sie möchten, dass dies geschieht, müssen Sie einen zusätzlichen Bool hinzufügen, um zwischen Standard und 0/0 zu unterscheiden.
Jonathan Allen

2

Was ich benutze, ist der Null-Koaleszenz-Operator (??) in Kombination mit einem Hintergrundfeld wie diesem:

public struct SomeStruct {
  private SomeRefType m_MyRefVariableBackingField;

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

Hoffe das hilft ;)

Hinweis: Die Null-Koaleszenzzuweisung ist derzeit ein Funktionsvorschlag für C # 8.0.


1

Sie können keinen Standardkonstruktor definieren, da Sie C # verwenden.

Strukturen können Standardkonstruktoren in .NET haben, obwohl ich keine bestimmte Sprache kenne, die dies unterstützt.


In C # unterscheiden sich Klassen und Strukturen semantisch. Eine Struktur ist ein Werttyp, während eine Klasse ein Referenztyp ist.
Tom Sarduy

-1

Hier ist meine Lösung für das Dilemma ohne Standardkonstruktor. Ich weiß, dass dies eine späte Lösung ist, aber ich denke, es ist erwähnenswert, dass dies eine Lösung ist.

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 );
    }
}

Ignorieren der Tatsache, dass ich eine statische Struktur namens null habe (Hinweis: Dies gilt nur für alle positiven Quadranten), mit get; set; In C # können Sie try / catch / finally ausführen, um Fehler zu beheben, bei denen ein bestimmter Datentyp nicht vom Standardkonstruktor Point2D () initialisiert wird. Ich denke, dies ist eine schwer fassbare Lösung für einige Leute bei dieser Antwort. Das ist hauptsächlich der Grund, warum ich meine hinzufüge. Wenn Sie die Getter- und Setter-Funktionalität in C # verwenden, können Sie diesen unsinnigen Standardkonstruktor umgehen und versuchen, das zu initialisieren, was Sie nicht initialisiert haben. Für mich funktioniert das gut, für jemand anderen möchten Sie vielleicht einige if-Anweisungen hinzufügen. In dem Fall, in dem Sie ein Numerator / Nenner-Setup wünschen, kann dieser Code hilfreich sein. Ich möchte nur wiederholen, dass diese Lösung nicht gut aussieht, unter Effizienzgesichtspunkten wahrscheinlich noch schlechter funktioniert, aber Für jemanden, der aus einer älteren Version von C # stammt, bietet die Verwendung von Array-Datentypen diese Funktionalität. Wenn Sie nur etwas möchten, das funktioniert, versuchen Sie Folgendes:

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;
    }
}

2
Das ist sehr schlechter Code. Warum haben Sie eine Array-Referenz in einer Struktur? Warum haben Sie nicht einfach die X- und Y-Koordinaten als Felder? Die Verwendung von Ausnahmen für die Flusskontrolle ist eine schlechte Idee. Sie sollten Ihren Code im Allgemeinen so schreiben, dass keine NullReferenceException auftritt. Wenn Sie dies wirklich benötigen - obwohl ein solches Konstrukt eher für eine Klasse als für eine Struktur geeignet wäre -, sollten Sie die verzögerte Initialisierung verwenden. (Und technisch gesehen setzen Sie - völlig unnötig in jeder bis auf die erste Einstellung einer Koordinate - jede Koordinate zweimal ein.)
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;
    }
}

5
Es ist erlaubt, aber es wird nicht verwendet, wenn keine Parameter angegeben sind. Ideone.com/xsLloQ
Motti
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.