Type de référence de chaîne C #?

164

Je sais que "string" en C # est un type de référence. C'est sur MSDN. Cependant, ce code ne fonctionne pas comme il se doit alors:

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

La sortie doit être "avant de passer" "après avoir passé" puisque je passe la chaîne en tant que paramètre et qu'il s'agit d'un type de référence, la deuxième instruction de sortie doit reconnaître que le texte a changé dans la méthode TestI. Cependant, j'obtiens "avant de passer" "avant de passer" faisant croire qu'il est passé par valeur et non par réf. Je comprends que les chaînes sont immuables, mais je ne vois pas comment cela expliquerait ce qui se passe ici. Qu'est-ce que je rate? Merci.


la source
Voir l'article référencé par Jon ci-dessous. Le comportement que vous mentionnez peut également être reproduit par des pointeurs C ++.
Sesh
Très belle explication dans MSDN également.
Dimi_Pel

Réponses:

211

La référence à la chaîne est passée par valeur. Il y a une grande différence entre passer une référence par valeur et passer un objet par référence. Il est malheureux que le mot «référence» soit utilisé dans les deux cas.

Si vous faites passer la référence de chaîne par référence, il fonctionnera comme prévu:

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

Vous devez maintenant faire la distinction entre apporter des modifications à l'objet auquel une référence fait référence et apporter une modification à une variable (telle qu'un paramètre) pour la laisser faire référence à un objet différent. Nous ne pouvons pas apporter de modifications à une chaîne car les chaînes sont immuables, mais nous pouvons le démontrer avec un à la StringBuilderplace:

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

Voir mon article sur le passage de paramètres pour plus de détails.

Jon Skeet
la source
2
d'accord, je veux juste préciser que l'utilisation du modificateur ref fonctionne également pour les types sans référence, c'est-à-dire que les deux sont des concepts assez séparés.
eglasius
2
@Jon Skeet a adoré la note de côté dans votre article. Vous devriez avoir referencedcela comme réponse
Nithish Inpursuit Ofhappiness
36

Si nous devons répondre à la question: String est un type de référence et se comporte comme une référence. Nous transmettons un paramètre qui contient une référence à, pas la chaîne réelle. Le problème est dans la fonction:

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

Le paramètre testcontient une référence à la chaîne, mais il s'agit d'une copie. Nous avons deux variables pointant vers la chaîne. Et comme toute opération avec des chaînes crée en fait un nouvel objet, nous faisons notre copie locale pour qu'elle pointe vers la nouvelle chaîne. Mais la testvariable d' origine n'est pas modifiée.

Les solutions suggérées à mettre refdans la déclaration de fonction et dans l'invocation fonctionnent car nous ne passerons pas la valeur de la testvariable mais nous lui passerons juste une référence. Ainsi, tout changement à l'intérieur de la fonction reflétera la variable d'origine.

Je veux répéter à la fin: String est un type de référence, mais comme son immuable, la ligne test = "after passing";crée en fait un nouvel objet et notre copie de la variable testest modifiée pour pointer vers la nouvelle chaîne.

Martin Dimitrov
la source
25

Comme d'autres l'ont indiqué, le Stringtype dans .NET est immuable et sa référence est passée par valeur.

Dans le code d'origine, dès que cette ligne s'exécute:

test = "after passing";

alors testne fait plus référence à l'objet original. Nous avons créé un nouvel String objet et assigné testà référencer cet objet sur le tas géré.

Je sens que beaucoup de gens se font trébucher ici car il n'y a pas de constructeur formel visible pour leur rappeler. Dans ce cas, cela se passe dans les coulisses car le Stringtype prend en charge le langage dans la façon dont il est construit.

C'est pourquoi le changement de testn'est pas visible en dehors de la portée de la TestI(string)méthode - nous avons passé la référence par valeur et maintenant cette valeur a changé! Mais si la Stringréférence a été passée par référence, alors lorsque la référence a changé, nous la verrons en dehors de la portée de la TestI(string)méthode.

Le mot clé ref ou out est nécessaire dans ce cas. Je pense que le outmot-clé pourrait être légèrement mieux adapté à cette situation particulière.

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
la source
ref = fonction externe initialisée, out = initialisée à l'intérieur de la fonction, ou en d'autres termes; ref est bidirectionnel, out est out-only. Alors sûrement ref devrait être utilisé.
Paul Zahra
@PaulZahra: outdoit être affecté dans la méthode pour que le code soit compilé. refn'a pas une telle exigence. Les outparamètres sont également initialisés en dehors de la méthode - le code de cette réponse est un contre-exemple.
Derek W le
Doit clarifier - les outparamètres peuvent être initialisés en dehors de la méthode, mais ce n'est pas obligatoire. Dans ce cas, nous souhaitons initialiser le outparamètre pour illustrer un point sur la nature du stringtype dans .NET.
Derek W le
9

En fait, cela aurait été la même chose pour n'importe quel objet, c'est-à-dire qu'être un type de référence et passer par référence sont 2 choses différentes en c #.

Cela fonctionnerait, mais cela s'applique quel que soit le type:

public static void TestI(ref string test)

Aussi à propos de la chaîne étant un type de référence, c'est aussi un type spécial. Il est conçu pour être immuable, donc toutes ses méthodes ne modifieront pas l'instance (elles en renvoient une nouvelle). Il contient également des éléments supplémentaires pour la performance.

Eglasius
la source
7

Voici un bon moyen de réfléchir à la différence entre les types de valeur, le passage par valeur, les types de référence et le passage par référence:

Une variable est un conteneur.

Une variable de type valeur contient une instance. Une variable de type référence contient un pointeur vers une instance stockée ailleurs.

La modification d'une variable de type valeur mute l'instance qu'elle contient. La modification d'une variable de type référence mute l'instance vers laquelle elle pointe.

Des variables de type référence distinctes peuvent pointer vers la même instance. Par conséquent, la même instance peut être mutée via n'importe quelle variable qui pointe vers elle.

Un argument passé par valeur est un nouveau conteneur avec une nouvelle copie du contenu. Un argument passé par référence est le conteneur d'origine avec son contenu d'origine.

Lorsqu'un argument de type valeur est passé par valeur: la réattribution du contenu de l'argument n'a aucun effet en dehors de la portée, car le conteneur est unique. La modification de l'argument n'a aucun effet en dehors de la portée, car l'instance est une copie indépendante.

Lorsqu'un argument de type référence est passé par valeur: la réattribution du contenu de l'argument n'a aucun effet en dehors de la portée, car le conteneur est unique. La modification du contenu de l'argument affecte la portée externe, car le pointeur copié pointe vers une instance partagée.

Lorsqu'un argument est passé par référence: la réattribution du contenu de l'argument affecte la portée externe, car le conteneur est partagé. La modification du contenu de l'argument affecte la portée externe, car le contenu est partagé.

En conclusion:

Une variable chaîne est une variable de type référence. Par conséquent, il contient un pointeur vers une instance stockée ailleurs. Lorsqu'il est passé par valeur, son pointeur est copié, donc la modification d'un argument de chaîne devrait affecter l'instance partagée. Cependant, une instance de chaîne n'a pas de propriétés mutables, donc un argument de chaîne ne peut pas être modifié de toute façon. Lorsqu'il est passé par référence, le conteneur du pointeur est partagé, donc la réaffectation affectera toujours la portée externe.

Bryan
la source
6

" Une image vaut mille mots ".

J'ai un exemple simple ici, il est similaire à votre cas.

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

C'est ce qui s'est passé:

entrez la description de l'image ici

  • Ligne 1 et 2: s1et les s2variables font référence au même "abc"objet string.
  • Ligne 3: étant donné que les chaînes sont immuables , l' "abc"objet chaîne ne se modifie pas lui-même (à "def"), mais un nouvel "def"objet chaîne est créé à la place et y fait s1référence.
  • Ligne 4: fait s2toujours référence à l' "abc"objet string, c'est donc la sortie.
Messi
la source
5

Les réponses ci-dessus sont utiles, je voudrais simplement ajouter un exemple qui, à mon avis, montre clairement ce qui se passe lorsque nous transmettons un paramètre sans le mot clé ref, même lorsque ce paramètre est un type de référence:

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
la source
1
Cette explication a fonctionné pour moi le mieux. Donc, fondamentalement, nous passons tout par valeur malgré le fait que la variable elle-même est soit une valeur, soit un type de référence, sauf si nous utilisons le mot-clé ref (ou out). Ce n'est pas important dans notre codage quotidien car nous ne définissons généralement pas nos objets sur null ou sur une instance différente dans une méthode où ils ont été transmis, nous définissons plutôt leurs propriétés ou appelons leurs méthodes. Dans le cas de "string", le paramétrer sur une nouvelle instance se produit tout le temps mais la nouvelle mise en place n'est pas visible et cela donne une fausse interprétation à l'œil non averti. Corrigez-moi si vous avez tort.
Ε Г И І И О
3

Pour les esprits curieux et pour compléter la conversation: Oui, String est un type de référence :

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

Mais notez que ce changement ne fonctionne que dans un bloc non sécurisé ! car les chaînes sont immuables (à partir de MSDN):

Le contenu d'un objet chaîne ne peut pas être modifié après la création de l'objet, bien que la syntaxe donne l'impression que vous pouvez le faire. Par exemple, lorsque vous écrivez ce code, le compilateur crée en fait un nouvel objet chaîne pour contenir la nouvelle séquence de caractères, et ce nouvel objet est affecté à b. La chaîne "h" est alors éligible pour le garbage collection.

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

Et gardez à l'esprit que:

Bien que la chaîne soit un type de référence, les opérateurs d'égalité ( ==et !=) sont définis pour comparer les valeurs des objets chaîne et non des références.

New York
la source
0

Je pense que votre code est analogue à ce qui suit, et vous n'auriez pas dû vous attendre à ce que la valeur ait changé pour la même raison qu'elle ne le ferait pas ici:

 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
la source
-1

Essayer:


public static void TestI(ref string test)
    {
        test = "after passing";
    }
Marius Kjeldahl
la source
3
Votre réponse doit contenir plus que du code. Il devrait également contenir une explication des raisons pour lesquelles cela fonctionne.
Charles Caldwell