Warum kann ich in C # das Mitglied einer Werttypinstanz in einer foreach-Schleife nicht ändern?

70

Ich weiß, dass Werttypen unveränderlich sein sollten, aber das ist nur ein Vorschlag, keine Regel, oder? Warum kann ich so etwas nicht tun:

struct MyStruct
{
    public string Name { get; set; }
}

 public class Program
{
    static void Main(string[] args)
    {
        MyStruct[] array = new MyStruct[] { new MyStruct { Name = "1" }, new MyStruct { Name = "2" } };
        foreach (var item in array)
        {
            item.Name = "3";
        }
        //for (int i = 0; i < array.Length; i++)
        //{
        //    array[i].Name = "3";
        //}

        Console.ReadLine();
    }
}

Die foreach-Schleife im Code wird nicht kompiliert, während die kommentierte for-Schleife einwandfrei funktioniert. Die Fehlermeldung:

Mitglieder von 'item' können nicht geändert werden, da es sich um eine 'foreach-Iterationsvariable' handelt.

Warum ist das so?

Cui Pengfei 崔鹏飞
quelle
4
+1: Gute Frage. Ich habe lange gewusst, dass eine foreachSchleifenvariable nicht geändert werden kann, aber ich habe nie wirklich gelernt, warum !
JP2-Code

Antworten:

76

Da foreach einen Enumerator verwendet und Enumeratoren die zugrunde liegende Auflistung nicht ändern können , können sie jedoch alle Objekte ändern, auf die von einem Objekt in der Auflistung verwiesen wird . Hier kommt die Semantik vom Typ Wert und Referenz ins Spiel.

Bei einem Referenztyp, dh einer Klasse, ist die gesamte Sammlung, die gespeichert wird, eine Referenz auf ein Objekt. Als solches berührt es niemals eines der Mitglieder des Objekts und könnte sich auch nicht weniger um sie kümmern. Eine Änderung am Objekt berührt die Sammlung nicht.

Andererseits speichern Werttypen ihre gesamte Struktur in der Sammlung. Sie können seine Mitglieder nicht berühren, ohne die Sammlung zu ändern und den Enumerator ungültig zu machen.

Darüber hinaus gibt der Enumerator eine Kopie des Werts in der Auflistung zurück. In einem Ref-Typ bedeutet dies nichts. Eine Kopie einer Referenz ist dieselbe Referenz, und Sie können das referenzierte Objekt beliebig ändern, wobei sich die Änderungen außerhalb des Gültigkeitsbereichs ausbreiten. Auf der anderen Seite bedeutet ein Werttyp, dass Sie nur eine Kopie des Objekts erhalten, und daher werden Änderungen an dieser Kopie niemals weitergegeben.

Kyte
quelle
ausgezeichnete Antwort. und wo kann man mehr Beweise dafür finden, dass "Werttypen ihre gesamte Struktur in der Sammlung speichern"?
Cui Pengfei 13
@CuiPengFei: Werttypen speichern ihren gesamten Wert in einer Variablen. Arrays sind nur ein Variablenblock.
Adam Robinson
Genau. Ein Werttyp behält seine gesamte Struktur in dem Container, in dem er sich befindet, sei es eine Sammlung, eine lokale Variable usw. Übrigens ist es aus diesem Grund besser, Strukturen unveränderlich zu machen: Am Ende erhalten Sie Tonnen von Kopien derselben "gleichen" Objekt und es ist schwer zu verfolgen, wer die Änderung vorgenommen hat und ob sie sich verbreiten wird und wo. Weißt du, die typischen Probleme, die mit Saiten auftreten.
Kyte
sehr einfache Erklärung
3 Regeln
Diese Erklärung ist völlig richtig, aber ich frage mich immer noch, warum Enumeratoren auf diese Weise funktionieren. Es konnten keine Enumeratoren implementiert werden, die Sammlungen von Werttypen durch Referenz manipulieren, sodass foreach effektiv funktioniert, oder foreach wurde mit der Option implementiert, anzugeben, dass die Iterationsvariable vom Referenztyp ist, um dasselbe zu erreichen. Bringt die Einschränkung der bestehenden Implementierung einen tatsächlichen Nutzen?
Neutrino
16

Es ist ein Vorschlag in dem Sinne, dass nichts Sie davon abhält, ihn zu verletzen, aber es sollte wirklich viel mehr Gewicht erhalten als "es ist ein Vorschlag". Zum Beispiel aus den Gründen, die Sie hier sehen.

Werttypen speichern den tatsächlichen Wert in einer Variablen, nicht in einer Referenz. Das bedeutet, dass Sie den Wert in Ihrem Array haben und eine Kopie dieses Werts in item, keine Referenz. Wenn Sie die Werte in ändern könnten item, würde dies nicht auf den Wert im Array übertragen, da es sich um einen völlig neuen Wert handelt. Deshalb ist es nicht erlaubt.

Wenn Sie dies tun möchten, müssen Sie das Array nach Index durchlaufen und dürfen keine temporäre Variable verwenden.

Adam Robinson
quelle
Ich habe nicht abgelehnt, aber ich glaube, der Grund, warum Sie erwähnen, warum foreach schreibgeschützt ist, ist nicht ganz richtig .
Steven Jeuris
@Steven: Der Grund, den ich erwähnt habe, ist richtig, wenn auch nicht so gründlich beschrieben wie in der verlinkten Antwort.
Adam Robinson
Alles, was Sie erklärt haben, fühlt sich richtig an, bis auf diesen einen Satz "Deshalb ist es nicht erlaubt". es fühlt sich nicht nach dem Grund an, das weiß ich nicht genau, aber es fühlt sich einfach nicht so an. und Kytes Antwort ist eher so.
Cui Pengfei 13
@ CuiPengFei: Deshalb ist es nicht erlaubt. Sie dürfen den tatsächlichen Wert der Iteratorvariablen nicht ändern. Dies tun Sie, wenn Sie eine Eigenschaft für einen Werttyp festlegen.
Adam Robinson
Ja, ich habe es gerade noch einmal gelesen. kytes und deine antwort sind praktisch gleich. danke
Cui Pengfei 13
10

Strukturen sind Werttypen.
Klassen sind Referenztypen.

ForEachKonstrukt verwendet IEnumerator von IEnumerable Datentypelementen. In diesem Fall sind Variablen in dem Sinne schreibgeschützt, dass Sie sie nicht ändern können. Da Sie einen Werttyp haben, können Sie keinen enthaltenen Wert ändern, da er denselben Speicher verwendet.

C # lang spec Abschnitt 8.8.4:

Die Iterationsvariable entspricht einer schreibgeschützten lokalen Variablen mit einem Bereich, der sich über die eingebettete Anweisung erstreckt

Um dies zu beheben, verwenden Sie die Strukturklasse insted.

class MyStruct {
    public string Name { get; set; }
}

Bearbeiten: @CuiPengFei

Wenn Sie varimpliziten Typ verwenden, ist es für den Compiler schwieriger, Ihnen zu helfen. Wenn Sie es verwenden MyStructwürden, würde es Ihnen im Falle einer Struktur sagen, dass es schreibgeschützt ist. Bei Klassen ist der Verweis auf das Element schreibgeschützt, sodass Sie nicht item = null;in die Schleife schreiben können , sondern die veränderbaren Eigenschaften ändern können.

Sie können auch verwenden (wenn Sie verwenden möchten struct):

        MyStruct[] array = new MyStruct[] { new MyStruct { Name = "1" }, new MyStruct { Name = "2" } };
        for (int index=0; index < array.Length; index++)
        {
            var item = array[index];
            item.Name = "3";
        }
Margus
quelle
danke, aber wie erklären Sie die Tatsache, dass es gut funktioniert, wenn ich MyStruct eine ChangeName-Methode hinzufüge und sie in der foreach-Schleife aufrufe?
Cui Pengfei 13
3

HINWEIS: Laut Adams Kommentar ist dies nicht die richtige Antwort / Ursache des Problems. Es ist jedoch immer noch etwas, an das man denken sollte.

Von MSDN . Sie können die Werte nicht ändern, wenn Sie den Enumerator verwenden. Dies ist im Wesentlichen das, was foreach tut.

Enumeratoren können zum Lesen der Daten in der Sammlung verwendet werden, sie können jedoch nicht zum Ändern der zugrunde liegenden Sammlung verwendet werden.

Ein Enumerator bleibt gültig, solange die Sammlung unverändert bleibt. Wenn Änderungen an der Auflistung vorgenommen werden, z. B. das Hinzufügen, Ändern oder Löschen von Elementen, wird der Enumerator unwiederbringlich ungültig und sein Verhalten ist nicht definiert.

Ian
quelle
1
Es mag zwar so aussehen, aber das ist nicht das Problem in der Frage. Sie können sicherlich die Eigenschafts- oder Feldwerte einer Instanz ändern, die über erhalten wurde foreach(das ist nicht das, worüber der MSDN-Artikel spricht), da dadurch das Mitglied der Sammlung und nicht die Sammlung selbst geändert wird. Hier geht es um die Semantik von Werttyp und Referenztyp.
Adam Robinson
In der Tat habe ich gerade Ihre Antwort gelesen und den Fehler auf meine Weise erkannt. Ich lasse das hier, da es immer noch nützlich ist.
Ian
Danke, aber ich glaube nicht, dass dies der Grund ist. weil ich versuche, eines der Elemente des Elements der Sammlung zu ändern, nicht die Sammlung selbst. und wenn ich MyStruct von einer Struktur in eine Klasse ändere, funktioniert die foreach-Schleife einfach.
Cui Pengfei 13
CuiPengFei - Werttypen vs. Referenztypen. Lesen Sie darüber in Skeets Blog.
JonH
1

Machen Sie MyStruct zu einer Klasse (anstelle von struct), und Sie können dies tun.

Adriano Carneiro
quelle
Dies ist richtig, basierend auf VT (Werttyp) vs RT (Ref-Typ), beantwortet aber die Frage nicht wirklich.
JonH
Danke, ich weiß, dass es funktionieren wird. aber ich versuche nur, den Grund herauszufinden.
Cui Pengfei 13
@ CuiPengFei: Meine Antwort erklärt, warum dies geschieht.
Adam Robinson
1

Werttypen werden nach Wert aufgerufen . Kurz gesagt, wenn Sie die Variable auswerten, wird eine Kopie davon erstellt. Selbst wenn dies zulässig wäre, würden Sie eine Kopie bearbeiten und nicht die ursprüngliche Instanz von MyStruct.

foreachist schreibgeschützt in C # . Für Referenztypen (Klassen) ändert sich nicht viel, da nur die Referenz schreibgeschützt ist, sodass Sie weiterhin Folgendes tun können:

    MyClass[] array = new MyClass[] {
        new MyClass { Name = "1" },
        new MyClass { Name = "2" }
    };
    foreach ( var item in array )
    {
        item.Name = "3";
    }

Bei Werttypen (struct) ist das gesamte Objekt jedoch schreibgeschützt, was zu dem führt, was Sie erleben. Sie können das Objekt im foreach nicht anpassen.

Steven Jeuris
quelle
Danke, aber ich denke nicht, dass Ihre Erklärung zu 100% richtig ist. denn wenn ich MyStruct zur ChangeName-Methode hinzufüge und sie in der foreach-Schleife aufrufe, funktioniert es einwandfrei.
Cui Pengfei 13
@CuiPengFei: Es wird kompiliert, aber wenn Sie die ursprünglichen Instanzen anschließend überprüfen, werden Sie feststellen, dass sie sich nicht geändert haben. (Aufgrund des "Call by Value" -Verhaltens von
Werttypen
Oh ja, Entschuldigung für den unvorsichtigen Kommentar.
Cui Pengfei 13