web-dev-qa-db-fra.com

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

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

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 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 été modifié dans la méthode TestI. Cependant, j'obtiens "avant de passer" "avant de passer" ce qui donne l'impression que c'est passé par valeur et non par réf. Je comprends que les chaînes sont immuables, mais je ne vois pas comment cela pourrait expliquer ce qui se passe ici. Qu'est-ce que je rate? Merci.

154
CriosR

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

Si vous faites passez la référence de chaîne par référence, cela fonctionnera comme vous vous attendez:

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 référencé par une référence et modifier une variable (un paramètre, par exemple) pour lui permettre de faire référence à un autre objet. Nous ne pouvons pas modifier une chaîne car les chaînes sont immuables, mais nous pouvons le démontrer avec un StringBuilder à la place:

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 paramétrage pour plus de détails.

203
Jon Skeet

Si nous devons répondre à la question: String est un type de référence et se comporte comme une référence. Nous passons 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 test contient 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 toutes les opérations avec des chaînes créent un nouvel objet, nous faisons en sorte que notre copie locale pointe vers la nouvelle chaîne. Mais la variable originale test n'est pas modifiée.

Les solutions suggérées pour mettre ref dans la déclaration de fonction et dans le travail d’invocation, car nous ne transmettrons pas la valeur de la variable test, mais ne transmettons qu’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 il est immuable, la ligne test = "after passing"; crée réellement un nouvel objet et la copie sortante de la variable test est modifiée pour pointer vers la nouvelle chaîne.

30
Martin Dimitrov

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

Dans le code d'origine, dès que cette ligne est exécutée:

test = "after passing";

alors test n'est plus se référant à l'objet d'origine. Nous avons créé un nouvel objet String et assigné test à cet objet sur le segment de mémoire géré.

Je pense que beaucoup de gens sont pris au piège ici car il n'y a aucun constructeur formel visible pour leur rappeler. Dans ce cas, cela se passe dans les coulisses puisque le type String prend en charge le langage dans sa construction.

Par conséquent, c’est pourquoi la modification apportée à test n’est pas visible en dehors de la portée de la méthode TestI(string) - nous avons passé la référence par valeur et maintenant cette valeur a changé! Mais si la référence String a été passée par référence, alors, lorsque la référence a été modifiée, nous la verrons en dehors du champ d'application de la méthode TestI(string).

Le mot clé ref ou out est requis dans ce cas. Je pense que le mot-clé out 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";
    }
}
23
Derek W

En fait, c’était la même chose pour tout objet, c’est-à-dire qu’il s’agissait d’un type de référence et que l’on passait par référence étaient deux choses différentes en c #.

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

public static void TestI(ref string test)

De plus, la chaîne est un type de référence, c'est aussi un type spécial. Il est conçu pour être immuable, afin que toutes ses méthodes ne modifient pas l'instance (elles en renvoient une nouvelle). Il comporte également des éléments supplémentaires pour la performance.

9
eglasius

Voici un bon moyen de réfléchir à la différence entre les types de valeur, passage par valeur, types de référence et 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 sur une instance stockée ailleurs.

La modification d'une variable de type valeur modifie 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 pointant 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 transmis valeur par valeur: La réaffectation 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é valeur par valeur: La réaffectation 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 l'étendue externe, car le pointeur copié pointe vers une instance partagée.

Lorsqu'un argument est passé par référence: La réaffectation 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 de chaîne est une variable de type référence. Par conséquent, il contient un pointeur sur une instance stockée ailleurs. Lorsqu'il est transmis valeur par valeur, son pointeur est copié. La modification d'un argument de chaîne doit donc affecter l'instance partagée. Cependant, une instance de chaîne n'a pas de propriétés mutables, un argument de chaîne ne peut donc pas être modifié. Une fois passé par référence, le conteneur du pointeur est partagé. Par conséquent, la réaffectation affectera toujours l'étendue externe.

6
Bryan

" 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é:

enter image description here

  • Lignes 1 et 2: s1 et s2 variables de référence à la même "abc" objet chaîne.
  • Ligne 3: Parce que les chaînes sont immuables , donc le "abc" L'objet string ne se modifie pas lui-même (to "def"), mais un nouveau "def" Un objet chaîne est créé à la place, puis s1 références à celui-ci.
  • Ligne 4: s2 fait toujours référence à "abc" objet string, voilà donc la sortie.
4
Messi

Les réponses ci-dessus sont utiles, je voudrais juste ajouter un exemple qui, à mon avis, montre clairement ce qui se passe lorsque nous passons 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.
        }
4
BornToCode

Pour les esprits curieux et pour terminer 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 cette modification ne fonctionne que dans un bloc non sécurisé! car Les chaînes sont immuables (De MSDN):

Le contenu d'un objet chaîne ne peut plus ê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 la récupération de place.

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.

2
Naser Yousefi

Je crois que votre code est analogue à celui-ci et vous ne devriez pas vous attendre à ce que la valeur change pour la même raison, elle ne serait 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
 }
0
Dave Cousineau