Comment les chaînes sont-elles transmises dans .NET?

121

Lorsque je passe un stringà une fonction, un pointeur vers le contenu de la chaîne est-il passé ou la chaîne entière est-elle transmise à la fonction sur la pile comme le structserait un?

Cole Johnson
la source

Réponses:

278

Une référence est passée; cependant, il n'est pas techniquement passé par référence. Il s'agit d'une distinction subtile mais très importante. Considérez le code suivant:

void DoSomething(string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomething(strMain);
    Console.WriteLine(strMain); // What gets printed?
}

Il y a trois choses que vous devez savoir pour comprendre ce qui se passe ici:

  1. Les chaînes sont des types de référence en C #.
  2. Ils sont également immuables, donc chaque fois que vous faites quelque chose qui semble changer la chaîne, vous ne l'êtes pas. Une chaîne complètement nouvelle est créée, la référence est pointée dessus et l'ancienne est jetée.
  3. Même si les chaînes sont des types de référence, elles strMainne sont pas passées par référence. C'est un type de référence, mais la référence elle-même est passée par valeur . Chaque fois que vous passez un paramètre sans le refmot - clé (sans compter les outparamètres), vous avez transmis quelque chose par valeur.

Cela doit donc signifier que vous ... passez une référence par valeur. Puisqu'il s'agit d'un type de référence, seule la référence a été copiée sur la pile. Mais qu'est ce que ça veut dire?

Passer des types de référence par valeur: vous le faites déjà

Les variables C # sont des types référence ou des types valeur . Les paramètres C # sont passés par référence ou par valeur . La terminologie est un problème ici; cela ressemble à la même chose, mais ce n'est pas le cas.

Si vous passez un paramètre de type ANY et que vous n'utilisez pas le refmot - clé, vous l'avez passé par valeur. Si vous l'avez passé par valeur, ce que vous avez vraiment transmis était une copie. Mais si le paramètre était un type de référence, alors la chose que vous avez copiée était la référence, pas celle vers laquelle elle pointait.

Voici la première ligne de la Mainméthode:

string strMain = "main";

Nous avons créé deux choses sur cette ligne: une chaîne avec la valeur mainstockée quelque part en mémoire, et une variable de référence appelée strMainpointant vers elle.

DoSomething(strMain);

Maintenant, nous passons cette référence à DoSomething. Nous l'avons passé par valeur, ce qui signifie que nous en avons fait une copie. C'est un type de référence, ce qui signifie que nous avons copié la référence, pas la chaîne elle-même. Nous avons maintenant deux références qui pointent chacune vers la même valeur en mémoire.

À l'intérieur de l'appelé

Voici le top de la DoSomethingméthode:

void DoSomething(string strLocal)

Pas de refmot-clé, donc strLocalet strMainsont deux références différentes pointant sur la même valeur. Si nous réaffectons strLocal...

strLocal = "local";   

... nous n'avons pas changé la valeur stockée; nous avons pris la référence appelée strLocalet l' avons dirigée vers une toute nouvelle chaîne. Qu'arrive-t-il strMainlorsque nous faisons cela? Rien. Il pointe toujours vers l'ancienne chaîne.

string strMain = "main";    // Store a string, create a reference to it
DoSomething(strMain);       // Reference gets copied, copy gets re-pointed
Console.WriteLine(strMain); // The original string is still "main" 

Immutabilité

Changeons le scénario une seconde. Imaginez que nous ne travaillons pas avec des chaînes, mais avec un type de référence mutable, comme une classe que vous avez créée.

class MutableThing
{
    public int ChangeMe { get; set; }
}

Si vous suivez la référence objLocalà l'objet vers lequel il pointe, vous pouvez modifier ses propriétés:

void DoSomething(MutableThing objLocal)
{
     objLocal.ChangeMe = 0;
} 

Il n'y en a encore qu'un MutableThingen mémoire, et la référence copiée et la référence d'origine pointent toujours vers elle. Les propriétés de MutableThinglui-même ont changé :

void Main()
{
    var objMain = new MutableThing();
    objMain.ChangeMe = 5; 
    Console.WriteLine(objMain.ChangeMe); // it's 5 on objMain

    DoSomething(objMain);                // now it's 0 on objLocal
    Console.WriteLine(objMain.ChangeMe); // it's also 0 on objMain   
}

Ah, mais les cordes sont immuables! Il n'y a aucune ChangeMepropriété à définir. Vous ne pouvez pas faire strLocal[3] = 'H'en C # comme vous pourriez le faire avec un chartableau de style C ; vous devez plutôt construire une toute nouvelle chaîne. La seule façon de changer strLocalest de pointer la référence sur une autre chaîne, et cela signifie que rien de ce que vous faites ne strLocalpeut affecter strMain. La valeur est immuable et la référence est une copie.

Passer une référence par référence

Pour prouver qu'il y a une différence, voici ce qui se passe lorsque vous passez une référence par référence:

void DoSomethingByReference(ref string strLocal)
{
    strLocal = "local";
}
void Main()
{
    string strMain = "main";
    DoSomethingByReference(ref strMain);
    Console.WriteLine(strMain);          // Prints "local"
}

Cette fois, la chaîne dans est Mainvraiment modifiée parce que vous avez passé la référence sans la copier sur la pile.

Ainsi, même si les chaînes sont des types de référence, les passer par valeur signifie que tout ce qui se passe dans l'appelé n'affectera pas la chaîne de l'appelant. Mais comme ce sont des types de référence, vous n'avez pas à copier la chaîne entière en mémoire lorsque vous souhaitez la transmettre.

Autres ressources:

Justin Morgan
la source
3
@TheLight - Désolé, mais vous vous trompez ici lorsque vous dites: "Un type de référence est passé par référence par défaut." Par défaut, tous les paramètres sont passés par valeur, mais avec les types référence, cela signifie que la référence est passée par valeur. Vous confondez des types de référence avec des paramètres de référence, ce qui est compréhensible car c'est une distinction très déroutante. Consultez la section Passer des types de référence par valeur ici. Votre article lié est tout à fait correct, mais il soutient en fait mon point.
Justin Morgan
1
@JustinMorgan Ne pas faire apparaître un fil de commentaire mort, mais je pense que le commentaire de TheLight a du sens si vous pensez en C. En C, les données ne sont qu'un bloc de mémoire. Une référence est un pointeur vers ce bloc de mémoire. Si vous passez tout le bloc de mémoire à une fonction, cela s'appelle "passer par valeur". Si vous passez le pointeur, cela s'appelle "passage par référence". En C #, il n'y a pas de notion de passage dans tout le bloc de mémoire, donc ils ont redéfini "passer par valeur" pour signifier passer le pointeur. Cela semble faux, mais un pointeur n'est qu'un bloc de mémoire aussi! Pour moi, la terminologie est assez arbitraire
rliu
@roliu - Le problème est que nous ne travaillons pas en C, et C # est extrêmement différent malgré son nom et sa syntaxe similaires. D'une part, les références ne sont pas les mêmes que les pointeurs , et penser à eux de cette façon peut conduire à des pièges. Le plus gros problème, cependant, est que "passer par référence" a une signification très spécifique en C #, nécessitant le refmot clé. Pour prouver que le passage par référence fait une différence, regardez cette démo: rextester.com/WKBG5978
Justin Morgan
1
@JustinMorgan Je suis d'accord pour dire que mélanger la terminologie C et C # est mauvais, mais, bien que j'aie apprécié le post de Lippert, je ne suis pas d'accord pour dire que penser aux références comme des pointeurs brouille particulièrement quoi que ce soit ici. Le billet de blog décrit comment penser une référence comme un pointeur lui donne trop de pouvoir. Je suis conscient que le refmot - clé a une utilité, j'essayais juste d'expliquer pourquoi on pourrait penser que passer un type de référence par valeur en C # ressemble à la notion "traditionnelle" (c'est-à-dire C) de passer par référence (et de passer un type de référence par référence en C # ressemble plus à passer une référence à une référence par valeur).
rliu
2
Vous avez raison, mais je pense que @roliu faisait référence à la façon dont une fonction telle que Foo(string bar)pourrait être considéré comme Foo(char* bar)alors Foo(ref string bar)serait Foo(char** bar)(ou Foo(char*& bar)ou Foo(string& bar)en C ++). Bien sûr, ce n'est pas ainsi que vous devriez y penser tous les jours, mais cela m'a en fait aidé à comprendre enfin ce qui se passe sous le capot.
Cole Johnson
23

Les chaînes en C # sont des objets de référence immuables. Cela signifie que les références à celles-ci sont transmises (par valeur) et qu'une fois qu'une chaîne est créée, vous ne pouvez pas la modifier. Les méthodes qui produisent des versions modifiées de la chaîne (sous-chaînes, versions découpées, etc.) créent des copies modifiées de la chaîne d'origine.

dasblinkenlight
la source
10

Les chaînes sont des cas particuliers. Chaque instance est immuable. Lorsque vous modifiez la valeur d'une chaîne, vous allouez une nouvelle chaîne en mémoire.

Ainsi, seule la référence est passée à votre fonction, mais lorsque la chaîne est modifiée, elle devient une nouvelle instance et ne modifie pas l'ancienne instance.

Énigmativité
la source
4
Les chaînes ne sont pas un cas particulier à cet égard. Il est très facile de créer des objets immuables qui pourraient avoir la même sémantique. (Autrement dit, une instance d'un type qui n'expose pas une méthode pour la muter ...)
Les chaînes sont des cas particuliers - ce sont en fait des types de référence immuables qui semblent mutables en ce sens qu'ils se comportent comme des types valeur.
Enigmativity
1
@Enigmativity Par cette logique, alors Uri(class) et Guid(struct) sont également des cas spéciaux. Je ne vois pas comment se System.Stringcomporte comme un "type valeur", pas plus que d'autres types immuables ... d'origines de classe ou de structure.
3
@pst - Les chaînes ont une sémantique de création spéciale - contrairement à Uri& Guid- vous pouvez simplement affecter une valeur littérale de chaîne à une variable de chaîne. La chaîne semble être modifiable, comme une intréaffectation, mais elle crée un objet implicitement - pas de newmot clé.
Enigmativity
3
La chaîne est un cas particulier, mais qui n'a aucun rapport avec cette question. Le type de valeur, le type de référence, quel que soit le type agira tous de la même manière dans cette question.
Kirk Broadhurst