C # String Referenztyp?

163

Ich weiß, dass "string" in C # ein Referenztyp ist. Dies ist auf MSDN. Dieser Code funktioniert jedoch nicht wie gewünscht:

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(string test)
    {
        test = "after passing";
    }
}

Die Ausgabe sollte "vor dem Übergeben" "nach dem Übergeben" sein, da ich die Zeichenfolge als Parameter übergebe und es sich um einen Referenztyp handelt. Die zweite Ausgabeanweisung sollte erkennen, dass sich der Text in der TestI-Methode geändert hat. Ich erhalte jedoch "vor dem Übergeben" "vor dem Übergeben", was den Anschein erweckt, dass es nach Wert und nicht nach Referenz übergeben wird. Ich verstehe, dass Strings unveränderlich sind, aber ich sehe nicht, wie das erklären würde, was hier vor sich geht. Was vermisse ich? Vielen Dank.


quelle
Siehe den Artikel von Jon unten. Das von Ihnen erwähnte Verhalten kann auch durch C ++ - Zeiger reproduziert werden.
Sesh
Sehr schöne Erklärung auch in MSDN .
Dimi_Pel

Antworten:

211

Der Verweis auf die Zeichenfolge wird als Wert übergeben. Es gibt einen großen Unterschied zwischen der Übergabe einer Referenz als Wert und der Übergabe eines Objekts als Referenz. Es ist bedauerlich, dass in beiden Fällen das Wort "Referenz" verwendet wird.

Wenn Sie tun die Zeichenfolge Verweis übergeben durch Verweis, wird es wie erwartet:

using System;

class Test
{
    public static void Main()
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(ref test);
        Console.WriteLine(test);
    }

    public static void TestI(ref string test)
    {
        test = "after passing";
    }
}

Jetzt müssen Sie unterscheiden, ob Sie Änderungen an dem Objekt vornehmen, auf das sich eine Referenz bezieht, oder ob Sie eine Variable (z. B. einen Parameter) ändern, damit sie sich auf ein anderes Objekt bezieht. Wir können keine Änderungen an einem String vornehmen, da Strings unveränderlich sind, aber wir können dies StringBuilderstattdessen mit einem demonstrieren :

using System;
using System.Text;

class Test
{
    public static void Main()
    {
        StringBuilder test = new StringBuilder();
        Console.WriteLine(test);
        TestI(test);
        Console.WriteLine(test);
    }

    public static void TestI(StringBuilder test)
    {
        // Note that we're not changing the value
        // of the "test" parameter - we're changing
        // the data in the object it's referring to
        test.Append("changing");
    }
}

Weitere Informationen finden Sie in meinem Artikel zur Parameterübergabe .

Jon Skeet
quelle
2
stimme zu, möchte nur klarstellen, dass die Verwendung des ref-Modifikators auch für Nichtreferenztypen funktioniert, dh beide sind ziemlich getrennte Konzepte.
Eglasius
2
@ Jon Skeet liebte die Nebenbemerkung in Ihrem Artikel. Sie sollten referenceddas als Ihre Antwort haben
Nithish Inpursuit Ofhappiness
36

Wenn wir die Frage beantworten müssen: String ist ein Referenztyp und verhält sich wie eine Referenz. Wir übergeben einen Parameter, der einen Verweis enthält, nicht den tatsächlichen String. Das Problem liegt in der Funktion:

public static void TestI(string test)
{
    test = "after passing";
}

Der Parameter testenthält einen Verweis auf die Zeichenfolge, ist jedoch eine Kopie. Wir haben zwei Variablen, die auf die Zeichenfolge zeigen. Und da alle Operationen mit Zeichenfolgen tatsächlich ein neues Objekt erstellen, erstellen wir unsere lokale Kopie so, dass sie auf die neue Zeichenfolge verweist. Die ursprüngliche testVariable wird jedoch nicht geändert.

Die vorgeschlagenen Lösungen für refdie Funktionsdeklaration und den Aufruf funktionieren, da wir den Wert der testVariablen nicht übergeben, sondern nur einen Verweis darauf übergeben. Änderungen innerhalb der Funktion spiegeln somit die ursprüngliche Variable wider.

Ich möchte am Ende wiederholen: String ist ein Referenztyp, aber da er unveränderlich ist, erstellt die Zeile test = "after passing";tatsächlich ein neues Objekt und unsere Kopie der Variablen testwird so geändert, dass sie auf den neuen String zeigt.

Martin Dimitrov
quelle
25

Wie andere angegeben haben, ist der StringTyp in .NET unveränderlich und seine Referenz wird als Wert übergeben.

Sobald diese Zeile im Originalcode ausgeführt wird:

test = "after passing";

dann beziehttest sich nicht mehr auf das ursprüngliche Objekt. Wir haben ein neues String Objekt erstellt und testdieses Objekt auf dem verwalteten Heap referenziert.

Ich habe das Gefühl, dass viele Leute hier stolpern, da es keinen sichtbaren formalen Konstruktor gibt, der sie daran erinnert. In diesem Fall geschieht dies hinter den Kulissen, da der StringTyp Sprachunterstützung bei seiner Konstruktion bietet.

Aus diesem Grund ist die Änderung von testaußerhalb des Bereichs der TestI(string)Methode nicht sichtbar - wir haben die Referenz als Wert übergeben und jetzt hat sich dieser Wert geändert! Wenn die StringReferenz jedoch als Referenz übergeben wurde, wird sie bei Änderung der Referenz außerhalb des Anwendungsbereichs der TestI(string)Methode angezeigt.

In diesem Fall wird entweder das Schlüsselwort ref oder out benötigt. Ich bin der Meinung, dass das outKeyword für diese spezielle Situation möglicherweise etwas besser geeignet ist.

class Program
{
    static void Main(string[] args)
    {
        string test = "before passing";
        Console.WriteLine(test);
        TestI(out test);
        Console.WriteLine(test);
        Console.ReadLine();
    }

    public static void TestI(out string test)
    {
        test = "after passing";
    }
}
Derek W.
quelle
ref = außerhalb der Funktion initialisiert, out = innerhalb der Funktion initialisiert oder mit anderen Worten; ref ist bidirektional, out ist out-only. Also sollte sicher ref verwendet werden.
Paul Zahra
@PaulZahra: outmuss innerhalb der Methode zugewiesen werden, damit der Code kompiliert werden kann. refhat keine solche Anforderung. Auch outParameter werden außerhalb der Methode initialisiert - der Code in dieser Antwort ist ein Gegenbeispiel.
Derek W
Sollte klarstellen - outParameter können außerhalb der Methode initialisiert werden, müssen aber nicht. In diesem Fall möchten wir den outParameter initialisieren , um einen Punkt über die Art des stringTyps in .NET zu demonstrieren .
Derek W
9

Tatsächlich wäre es für jedes Objekt dasselbe gewesen, dh ein Referenztyp zu sein und als Referenz zu übergeben, sind zwei verschiedene Dinge in c #.

Dies würde funktionieren, aber das gilt unabhängig vom Typ:

public static void TestI(ref string test)

Auch wenn es sich bei String um einen Referenztyp handelt, handelt es sich auch um einen speziellen. Es ist so konzipiert, dass es unveränderlich ist, sodass alle Methoden die Instanz nicht ändern (sie geben eine neue zurück). Es enthält auch einige zusätzliche Dinge für die Leistung.

eglasius
quelle
7

Hier ist eine gute Möglichkeit, über den Unterschied zwischen Werttypen, Wertübergabe, Referenztypen und Referenzübergabe nachzudenken:

Eine Variable ist ein Container.

Eine Variable vom Typ Wert enthält eine Instanz. Eine Variable vom Referenztyp enthält einen Zeiger auf eine Instanz, die an einer anderen Stelle gespeichert ist.

Durch Ändern einer Variablen vom Typ Wert wird die darin enthaltene Instanz mutiert. Durch Ändern einer Referenzvariablen wird die Instanz mutiert, auf die sie verweist.

Separate Referenztypvariablen können auf dieselbe Instanz verweisen. Daher kann dieselbe Instanz über jede Variable mutiert werden, die darauf verweist.

Ein übergebenes Wertargument ist ein neuer Container mit einer neuen Kopie des Inhalts. Ein als Referenz übergebenes Argument ist der ursprüngliche Container mit seinem ursprünglichen Inhalt.

Wenn ein Argument vom Werttyp als Wert übergeben wird: Die Neuzuweisung des Inhalts des Arguments hat keine Auswirkungen außerhalb des Gültigkeitsbereichs, da der Container eindeutig ist. Das Ändern des Arguments hat keine Auswirkungen außerhalb des Gültigkeitsbereichs, da die Instanz eine unabhängige Kopie ist.

Wenn ein Argument vom Referenztyp als Wert übergeben wird: Die Neuzuweisung des Inhalts des Arguments hat keine Auswirkungen außerhalb des Gültigkeitsbereichs, da der Container eindeutig ist. Das Ändern des Inhalts des Arguments wirkt sich auf den externen Bereich aus, da der kopierte Zeiger auf eine gemeinsam genutzte Instanz verweist.

Wenn ein Argument als Referenz übergeben wird: Die Neuzuweisung des Inhalts des Arguments wirkt sich auf den externen Bereich aus, da der Container gemeinsam genutzt wird. Das Ändern des Inhalts des Arguments wirkt sich auf den externen Bereich aus, da der Inhalt gemeinsam genutzt wird.

Abschließend:

Eine Zeichenfolgenvariable ist eine Referenzvariable. Daher enthält es einen Zeiger auf eine Instanz, die an einer anderen Stelle gespeichert ist. Bei der Übergabe als Wert wird der Zeiger kopiert. Das Ändern eines Zeichenfolgenarguments sollte sich daher auf die gemeinsam genutzte Instanz auswirken. Eine Zeichenfolgeninstanz hat jedoch keine veränderlichen Eigenschaften, sodass ein Zeichenfolgenargument ohnehin nicht geändert werden kann. Bei der Übergabe als Referenz wird der Container des Zeigers gemeinsam genutzt, sodass die Neuzuweisung weiterhin Auswirkungen auf den externen Bereich hat.

Bryan
quelle
6

" Ein Bild sagt mehr als tausend Worte ".

Ich habe hier ein einfaches Beispiel, es ähnelt Ihrem Fall.

string s1 = "abc";
string s2 = s1;
s1 = "def";
Console.WriteLine(s2);
// Output: abc

Das ist, was passiert ist:

Geben Sie hier die Bildbeschreibung ein

  • Zeile 1 und 2: s1und s2Variablen verweisen auf dasselbe "abc"Zeichenfolgenobjekt.
  • Zeile 3: Da Zeichenfolgen unveränderlich sind , "abc"ändert sich das Zeichenfolgenobjekt nicht selbst (zu "def"), sondern es "def"wird stattdessen ein neues Zeichenfolgenobjekt erstellt, auf das dann s1verwiesen wird.
  • Zeile 4: s2Verweist immer noch auf ein "abc"Zeichenfolgenobjekt, das ist also die Ausgabe.
Messi
quelle
5

Die obigen Antworten sind hilfreich. Ich möchte nur ein Beispiel hinzufügen, das meiner Meinung nach deutlich zeigt, was passiert, wenn wir Parameter ohne das Schlüsselwort ref übergeben, auch wenn dieser Parameter ein Referenztyp ist:

MyClass c = new MyClass(); c.MyProperty = "foo";

CNull(c); // only a copy of the reference is sent 
Console.WriteLine(c.MyProperty); // still foo, we only made the copy null
CPropertyChange(c); 
Console.WriteLine(c.MyProperty); // bar


private void CNull(MyClass c2)
        {          
            c2 = null;
        }
private void CPropertyChange(MyClass c2) 
        {
            c2.MyProperty = "bar"; // c2 is a copy, but it refers to the same object that c does (on heap) and modified property would appear on c.MyProperty as well.
        }
BornToCode
quelle
1
Diese Erklärung hat bei mir am besten funktioniert. Im Grunde übergeben wir also alles als Wert, obwohl die Variable selbst entweder ein Wert oder ein Referenztyp ist, es sei denn, wir verwenden das Schlüsselwort ref (oder out). Bei unserer täglichen Codierung spielt dies keine Rolle, da wir unsere Objekte in einer Methode, in der sie übergeben wurden, im Allgemeinen nicht auf null oder auf eine andere Instanz setzen, sondern ihre Eigenschaften festlegen oder ihre Methoden aufrufen. Im Fall von "string" wird die Einstellung immer auf eine neue Instanz festgelegt, aber die Neuerstellung ist nicht sichtbar, und dies führt zu einer falschen Interpretation des ungeübten Auges. Korrigieren Sie mich, wenn Sie falsch liegen.
О Г И І І
3

Für neugierige Köpfe und um das Gespräch zu vervollständigen: Ja, String ist ein Referenztyp :

unsafe
{
     string a = "Test";
     string b = a;
     fixed (char* p = a)
     {
          p[0] = 'B';
     }
     Console.WriteLine(a); // output: "Best"
     Console.WriteLine(b); // output: "Best"
}

Beachten Sie jedoch, dass diese Änderung nur in einem unsicheren Block funktioniert ! weil Strings unveränderlich sind (von MSDN):

Der Inhalt eines Zeichenfolgenobjekts kann nach dem Erstellen des Objekts nicht mehr geändert werden, obwohl die Syntax den Anschein erweckt, als könnten Sie dies tun. Wenn Sie beispielsweise diesen Code schreiben, erstellt der Compiler tatsächlich ein neues Zeichenfolgenobjekt, das die neue Zeichenfolge enthält, und dieses neue Objekt wird b zugewiesen. Die Zeichenfolge "h" ist dann für die Speicherbereinigung berechtigt.

string b = "h";  
b += "ello";  

Und denken Sie daran:

Obwohl die Zeichenfolge ein Referenztyp ist, werden die Gleichheitsoperatoren ( ==und !=) definiert, um die Werte von Zeichenfolgenobjekten und nicht von Referenzen zu vergleichen.

NY
quelle
0

Ich glaube, Ihr Code ist analog zu dem folgenden, und Sie hätten nicht erwarten dürfen, dass sich der Wert aus demselben Grund geändert hat, aus dem er hier nicht vorhanden wäre:

 public static void Main()
 {
     StringWrapper testVariable = new StringWrapper("before passing");
     Console.WriteLine(testVariable);
     TestI(testVariable);
     Console.WriteLine(testVariable);
 }

 public static void TestI(StringWrapper testParameter)
 {
     testParameter = new StringWrapper("after passing");

     // this will change the object that testParameter is pointing/referring
     // to but it doesn't change testVariable unless you use a reference
     // parameter as indicated in other answers
 }
Dave Cousineau
quelle
-1

Versuchen:


public static void TestI(ref string test)
    {
        test = "after passing";
    }
Marius Kjeldahl
quelle
3
Ihre Antwort sollte mehr als nur Code enthalten. Es sollte auch eine Erklärung enthalten, warum es funktioniert.
Charles Caldwell