out-Parameter vom Typ struct müssen nicht zugewiesen werden

9

Ich habe ein bizarres Verhalten in meinem Code festgestellt, als ich während der Codeüberprüfung versehentlich eine Zeile in einer Funktion auskommentierte. Es war sehr schwer zu reproduzieren, aber ich werde hier ein ähnliches Beispiel zeigen.

Ich habe diese Testklasse:

public class Test
{
    public void GetOut(out EmailAddress email)
    {
        try
        {
            Foo(email);
        }
        catch
        {
        }
    }

    public void Foo(EmailAddress email)
    {
    }
}

Es gibt keine Zuordnung zu E-Mail in der, GetOutdie normalerweise einen Fehler auslösen würde:

Der out-Parameter 'email' muss zugewiesen werden, bevor die Steuerung die aktuelle Methode verlässt

Befindet sich EmailAddress jedoch in einer Struktur in einer separaten Assembly, wird kein Fehler erstellt und alles wird ordnungsgemäß kompiliert.

public struct EmailAddress
{
    #region Constructors

    public EmailAddress(string email)
        : this(email, string.Empty)
    {
    }

    public EmailAddress(string email, string name)
    {
        this.Email = email;
        this.Name = name;
    }

    #endregion

    #region Properties

    public string Email { get; private set; }
    public string Name { get; private set; }

    #endregion
}

Warum erzwingt der Compiler nicht, dass E-Mail zugewiesen werden muss? Warum wird dieser Code kompiliert, wenn die Struktur in einer separaten Assembly erstellt wird, aber nicht kompiliert, wenn die Struktur in der vorhandenen Assembly definiert ist?

Johnny 5
quelle
2
Wenn Sie eine Klasse verwenden, müssen Sie eine Instanz des Objekts neu erstellen. Es ist nicht für Strukturen erforderlich. docs.microsoft.com/en-us/dotnet/csharp/programming-guide/… (Suche nach diesem Text auf dieser Seite speziell: Im Gegensatz zu Klassen können Strukturen ohne Verwendung des neuen Operatos instanziiert werden)
Dortimer
1
Sobald Ihre Hundestruktur eine Variable erhält, wird sie nicht kompiliert :)
André Sanson
In diesem Beispiel ist mit struct Dog{}alles in Ordnung.
Henk Holterman
2
@ johnny5 Dann zeige das Beispiel.
André Sanson
1
OK, das ist interessant. Wiedergabe mit einer Core 3 Console-App und einer Standardklassenbibliothek.
Henk Holterman

Antworten:

12

TLDR: Dies ist ein bekannter Fehler von langer Dauer. Ich habe 2010 zum ersten Mal darüber geschrieben:

https://blogs.msdn.microsoft.com/ericlippert/2010/01/18/a-definite-assignment-anomaly/

Es ist harmlos und Sie können es ignorieren und sich selbst gratulieren, dass Sie einen etwas obskuren Fehler gefunden haben.

Warum erzwingt der Compiler nicht, dass Emaildies definitiv zugewiesen werden muss?

Oh, in gewisser Weise. Es hat nur eine falsche Vorstellung davon, welche Bedingung impliziert, dass die Variable definitiv zugewiesen ist, wie wir sehen werden.

Warum wird dieser Code kompiliert, wenn die Struktur in einer separaten Assembly erstellt wird, aber nicht kompiliert, wenn die Struktur in der vorhandenen Assembly definiert ist?

Das ist der Kern des Fehlers. Der Fehler ist eine Folge der Überschneidung, wie der C # -Compiler bestimmte Zuweisungsprüfungen für Strukturen durchführt und wie der Compiler Metadaten aus Bibliotheken lädt.

Bedenken Sie:

struct Foo 
{ 
  public int x; 
  public int y; 
}
// Yes, public fields are bad, but this is just 
// to illustrate the situation.
void M(out Foo f)
{

OK, an diesem Punkt, was wissen wir? fist ein Alias ​​für eine Variable vom Typ Foo, daher wurde der Speicher bereits zugewiesen und befindet sich definitiv zumindest in dem Zustand, in dem er aus dem Speicherzuweiser stammt. Wenn der Aufrufer einen Wert in die Variable eingefügt hat, ist dieser Wert vorhanden.

Was brauchen wir? Wir fordern, dass fdiese definitiv an jedem Punkt zugewiesen wird, an dem die Kontrolle Mnormal abläuft. Sie würden also etwas erwarten wie:

void M(out Foo f)
{
  f = new Foo();
}

welche setzt f.xund f.yauf ihre Standardwerte. Aber was ist damit?

void M(out Foo f)
{
  f = new Foo();
  f.x = 123;
  f.y = 456;
}

Das sollte auch gut gehen. Aber, und hier ist der Kicker, warum müssen wir die Standardwerte nur zuweisen, um sie einen Moment später wegzublasen? Der definitive Zuweisungsprüfer von C # prüft, ob jedes Feld zugewiesen ist! Das ist legal:

void M(out Foo f)
{
  f.x = 123;
  f.y = 456;
}

Und warum sollte das nicht legal sein? Es ist ein Werttyp. fist eine Variable und enthält bereits einen gültigen Wert vom Typ. FooSetzen wir also einfach die Felder, und wir sind fertig, oder?

Recht. Also, was ist der Fehler?

Der Fehler, den Sie entdeckt haben, ist: Aus Kostengründen lädt der C # -Compiler die Metadaten für private Felder von Strukturen, die sich in referenzierten Bibliotheken befinden, nicht . Diese Metadaten können sehr groß sein und den Compiler für sehr wenig Gewinn verlangsamen, um jedes Mal alles in den Speicher zu laden.

Und jetzt sollten Sie in der Lage sein, die Ursache des gefundenen Fehlers abzuleiten. Wenn der Compiler prüft, ob der out-Parameter definitiv zugewiesen ist, vergleicht er die Anzahl der bekannten Felder mit der Anzahl der Felder, die definitiv initialisiert wurden, und in Ihrem Fall kennt er nur die null öffentlichen Felder, da die Metadaten der privaten Felder nicht geladen wurden . Der Compiler kommt zu dem Schluss: "Null Felder erforderlich, Null Felder initialisiert, wir sind gut."

Wie ich bereits sagte, gibt es diesen Fehler seit mehr als einem Jahrzehnt und Leute wie Sie entdecken ihn gelegentlich wieder und melden ihn. Es ist harmlos und es ist unwahrscheinlich, dass es repariert wird, da das Reparieren fast keinen Nutzen bringt, aber hohe Leistungskosten verursacht.

Und natürlich wird der Fehler nicht für private Felder von Strukturen angezeigt, die sich in Ihrem Projekt im Quellcode befinden, da der Compiler offensichtlich bereits Informationen über die privaten Felder zur Hand hat.

Eric Lippert
quelle
@ Johnny5: Du solltest keine Fehler bekommen. Siehe dotnetfiddle.net/ZEKiUk . Kannst du einen einfachen Repro posten?
Eric Lippert
1
Vielen Dank für die Geige, weil ich x und y als Eigenschaften anstelle von Mitgliedern definiert habe
Johnny 5.
1
@ johnny5: Wenn Sie gerade eine normale C # 1.0-Stileigenschaft definiert haben, ist dies aus Sicht des bestimmten Zuweisungsprüfers eine Methode, kein Feld. Wenn Sie eine automatische Eigenschaft im C # 3.0+ -Stil definiert haben, weiß der Compiler, dass ein privates Feld dahinter steht. Die Regeln für die endgültige Zuordnung dieses Dings wurden im Laufe der Jahre angepasst, und ich erinnere mich jetzt nicht an die genauen Regeln.
Eric Lippert
Wenn Sie System.TimeSpanstattdessen a verwenden, treten folgende Fehler auf: error CS0269: Use of unassigned out parameter 'email'und error CS0177: The out parameter 'email' must be assigned to before control leaves the current method. Es gibt nur ein nicht statisches Feld von TimeSpannämlich _ticks. Es ist internalzu seiner Montage mscorlib. Ist diese Baugruppe etwas Besonderes? Gleiches System.DateTimegilt für und sein Feld istprivate
Jeppe Stig Nielsen
@ JeppeStigNielsen: Ich weiß nicht, was damit los ist! Wenn Sie es herausfinden, lassen Sie es mich bitte wissen.
Eric Lippert
1

Obwohl es wie ein Fehler aussieht, macht es doch Sinn.

Der 'fehlende Fehler' wird nur bei Verwendung einer Klassenbibliothek angezeigt. Und eine Klassenbibliothek wurde möglicherweise in einer anderen .net-Sprache geschrieben, z. B. VB.Net. Die 'definitive Zuweisungsverfolgung' ist eine Funktion von C #, nicht des Frameworks.

Im Großen und Ganzen denke ich nicht, dass es ein Fehler ist, aber ich weiß nichts über eine maßgebliche Aussage dafür.

Henk Holterman
quelle
Wenn Sie eine Assembly in C # importieren, obwohl die Struktur in einer Assembly liegen könnte, die in einer anderen Sprache geschrieben ist, befindet sich der Code, der sie verwendet, immer noch in c #, sodass keine eindeutige Zuweisungsverfolgung erforderlich sein sollte.
Johnny 5
1
Nicht unbedingt. Mit C # können Sie keine unitialisierte (lokale) Variable verwenden, aber gleichzeitig garantiert das Framework, dass sie auf 0 ( default(T)) gesetzt wird. Es liegt also kein Verstoß gegen die Speichersicherheit oder ähnliches vor.
Henk Holterman
3
Ich kann eine so maßgebliche Erklärung abgeben. :) Es ist ein seit langem bekannter Fehler.
Eric Lippert
1
@ EricLippert Danke, ich hatte gehofft, dass Sie dies irgendwann sehen würden
Johnny 5