Тип посилання рядка C #?


164

Я знаю, що "рядок" в C # - це еталонний тип. Це на MSDN. Однак цей код не працює як слід:

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

Вихід повинен бути "до передачі" "після передачі", оскільки я передаю рядок як параметр, і він є еталонним типом, другий вивідний висновок повинен визнати, що текст змінився в методі TestI. Однак я отримую "перед проходженням" "перед тим, як пройти", роблячи здається, що він передається за значенням, а не за посиланням. Я розумію, що рядки незмінні, але я не бачу, як це могло б пояснити, що тут відбувається. Що я пропускаю? Дякую.


Дивіться статтю, на яку посилається Джон, нижче. Поведінка, яку ви згадуєте, також може бути відтворена вказівниками C ++.
Сеш

Дуже приємне пояснення в MSDN також.
Dimi_Pel

Відповіді:


211

Посилання на рядок передається за значенням. Існує велика різниця між передачею посилання за значенням і передачею об'єкта за посиланням. Прикро, що слово "довідка" вживається в обох випадках.

Якщо ви робите передати посилання рядки по посиланню, він буде працювати , як ви очікуєте:

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

Тепер вам потрібно розрізнити внесення змін до об'єкта, на які посилається посилання, та внесення змін до змінної (наприклад, параметра), щоб вона мала відношення до іншого об'єкта. Ми не можемо внести зміни до рядка, оскільки рядки незмінні, але ми можемо продемонструвати це StringBuilderзамість цього:

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

Дивіться мою статтю про проходження параметрів для отримання більш детальної інформації.


2
погодьтеся, просто хочу зрозуміти, що використання модифікатора ref також працює для нереференційних типів, тобто обидва є досить окремими поняттями.
eglasius

2
@Jon Skeet полюбив сіденот у вашій статті. Ви повинні це referencedзробити як свою відповідь
Nithish Inpursuit Of Щастя

36

Якщо нам доведеться відповісти на запитання: String - це еталонний тип, і він поводиться як еталон. Ми передаємо параметр, який містить посилання, а не фактичну рядок. Проблема полягає у функції:

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

Параметр testмістить посилання на рядок, але це копія. У нас є дві змінні, що вказують на рядок. І оскільки будь-які операції зі струнами насправді створюють новий об’єкт, ми робимо нашу локальну копію, щоб вказати на новий рядок. Але початкова testзмінна не змінюється.

Пропоновані рішення розмістити refв декларації функції та в роботі виклику, оскільки ми не передамо значення testзмінної, а передамо лише посилання на неї. Таким чином, будь-які зміни всередині функції відображатимуть початкову змінну.

Я хочу повторити наприкінці: Рядок - це еталонний тип, але оскільки його незмінний рядок test = "after passing";фактично створює новий об’єкт, і наша копія змінної testзмінюється, щоб вказувати на новий рядок.


25

Як заявили інші, Stringтип .NET є незмінним, і його посилання передається за значенням.

У вихідному коді, як тільки ця лінія виконує:

test = "after passing";

то testбільше не посилається на оригінальний об’єкт. Ми створили новий String об’єкт і призначили testйого посилати на керовану купу.

Я відчуваю, що багато людей тут стикаються, оскільки немає видимого формального конструктора, який би нагадував їм. У цьому випадку це відбувається за кадром, оскільки Stringтип має мовну підтримку в тому, як він побудований.

Отже, ось чому зміна testне видно за межі TestI(string)методу - ми передали посилання за значенням і тепер це значення змінилося! Але якщо Stringпосилання було передане посиланням, то коли посилання зміниться, ми побачимо її поза межами TestI(string)методу.

Або вих або з ключового слова необхідні в даному випадку. Я думаю, що outключове слово може бути трохи краще підходить для цієї конкретної ситуації.

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

ref = ініціалізована зовнішня функція, out = ініціалізована всередині функції, або іншими словами; ref є двостороннім, out-out-only. Тож неодмінно слід використовувати реф.
Пол Захра

@PaulZahra: outпотрібно призначити в методі для збору коду. refтакої вимоги не має. Також outпараметри ініціалізуються поза методом - код у цій відповіді є контрприкладом.
Дерек Ш

Слід уточнити - outпараметри можна ініціалізувати поза методом, але не обов’язково. У цьому випадку ми хочемо ініціалізувати outпараметр, щоб продемонструвати точку про природу stringтипу в .NET.
Дерек Ш

9

Насправді це було б однаково для будь-якого об'єкта з цього питання, тобто те, що тип посилання та перехід посилання - це дві різні речі в c #.

Це буде працювати, але це застосовується незалежно від типу:

public static void TestI(ref string test)

Також про те, що рядок є еталонним типом, він також є спеціальним. Він розроблений так, щоб бути незмінним, тому всі його методи не змінюватимуть примірник (вони повертають новий). У ньому також є додаткові речі для продуктивності.


7

Ось хороший спосіб подумати про різницю між типами значень, типами передачі за значенням, типами посилань та посиланням:

Змінна - контейнер.

Змінна типу типу містить примірник. Змінна типу посилання містить вказівник на екземпляр, який зберігається в іншому місці.

Змінення змінної типу значення мутує екземпляр, який вона містить. Змінення змінної типу посилання мутує екземпляр, на який він вказує.

Окремі змінні типу опорного типу можуть вказувати на один і той же екземпляр. Тому той самий екземпляр можна мутувати за допомогою будь-якої змінної, яка вказує на нього.

Аргумент переданого значення - це новий контейнер з новою копією вмісту. Аргумент, що передається через посилання - це оригінальний контейнер з його оригінальним вмістом.

Коли аргумент типу значення передається за значенням: Переназначення вмісту аргументу не має ефекту поза сферою, оскільки контейнер унікальний. Змінення аргументу не має ефекту поза сферою, оскільки екземпляр є незалежною копією.

Коли аргумент опорного типу передається за значенням: Перепризначення вмісту аргументу не має ефекту поза сферою, оскільки контейнер унікальний. Змінення вмісту аргументу впливає на зовнішню область, оскільки скопійований покажчик вказує на спільний екземпляр.

Коли будь-який аргумент передається через посилання: Перепризначення вмісту аргументу впливає на зовнішню область застосування, оскільки контейнер є спільним. Змінення вмісту аргументу впливає на зовнішню область застосування, оскільки вміст є спільним.

На закінчення:

Строкова змінна - це змінна типу опорного типу. Тому він містить вказівник на екземпляр, який зберігається в іншому місці. Коли передається значення, його вказівник копіюється, тому зміна аргументу рядка має впливати на спільний екземпляр. Однак, рядковий екземпляр не має змінних властивостей, тому аргумент рядка не можна змінювати. При передачі посилання контейнер вказівника розділяється, тому переназначення все одно вплине на зовнішню область застосування.


6

" Картина варта тисячі слів ".

У мене тут простий приклад, він схожий на ваш випадок.

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

Ось що сталося:

введіть тут опис зображення

  • Рядок 1 і 2: s1і s2змінні посилаються на один і той же "abc"рядковий об'єкт.
  • Рядок 3: Оскільки рядки незмінні , тому "abc"об'єкт рядка не змінює себе (на "def"), а створює новий "def"об'єкт рядка, а потім s1посилається на нього.
  • Рядок 4: s2як і раніше посилання на "abc"рядовий об'єкт, тож це вихід.

5

Наведені вище відповіді корисні, я просто хочу додати приклад, який, на мою думку, наочно демонструє те, що відбувається, коли ми передаємо параметр без ключового слова ref, навіть коли цей параметр є еталонним типом:

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.
        }

1
Це пояснення працювало для мене найкраще. Таким чином, ми в основному передаємо все за значенням, незважаючи на те, що сама змінна є або значенням, або типом посилання, якщо ми не використовуємо ключове слово ref (або out). Це не є помітним для нашого щоденного кодування, оскільки ми зазвичай не встановлюємо об'єкти на нуль або інший екземпляр всередині методу, куди вони передані, а ми встановлюємо їх властивості або називаємо їхні методи. У випадку "string", встановлення його на новий екземпляр відбувається постійно, але новелювання не видно, що дає помилкову інтерпретацію нетренованому оці. Виправте мене, якщо не так.
Ε Г І І І О

3

Для допитливих розумів і для завершення розмови: Так, String - це тип посилання :

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

Але зауважте, що ця зміна працює лише в небезпечному блоці! тому що рядки незмінні (від MSDN):

Вміст рядкового об'єкта неможливо змінити після створення об'єкта, хоча синтаксис робить його таким, як якщо б ви могли це зробити. Наприклад, коли ви пишете цей код, компілятор фактично створює новий рядковий об'єкт для вміщення нової послідовності символів, і цей новий об'єкт присвоюється b. Потім рядок "h" є придатним для збору сміття.

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

І майте на увазі, що:

Хоча рядок є еталонним типом, оператори рівності ( ==і !=) визначені для порівняння значень об'єктів рядків, а не посилань.


0

Я вважаю, що ваш код аналогічний наступному, і ви не повинні були очікувати, що значення зміниться з тієї ж причини, що не було б тут:

 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
 }

-1

Спробуйте:


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

3
Ваша відповідь повинна містити більше, ніж просто код. Він також повинен містити пояснення, чому це працює.
Чарльз Колдвелл
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.