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 Ofhappiness
36

Если мы должны ответить на вопрос: String является ссылочным типом и ведет себя как ссылка. Мы передаем параметр, который содержит ссылку, а не фактическую строку. Проблема в функции:

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

Параметр testсодержит ссылку на строку, но это копия. У нас есть две переменные, указывающие на строку. И поскольку любые операции со строками фактически создают новый объект, мы делаем нашу локальную копию, чтобы указывать на новую строку. Но исходная testпеременная не изменилась.

Предлагаемые решения для refвключения в объявление функции и в вызов работают, потому что мы не передадим значение testпеременной, а передадим просто ссылку на нее. Таким образом, любые изменения внутри функции будут отражать исходную переменную.

Я хочу повторить в конце: String является ссылочным типом, но поскольку он неизменный, строка test = "after passing";фактически создает новый объект, и наша копия переменной testизменяется, чтобы указывать на новую строку.

Мартин Димитров
источник
25

Как уже говорили другие, Stringтип в .NET является неизменным, и его ссылка передается по значению.

В исходном коде, как только эта строка выполнится:

test = "after passing";

затем testбольше не ссылается на исходный объект. Мы создали новый String объект и назначены testдля ссылки на этот объект в управляемой куче.

Я чувствую, что многие люди запутались здесь, потому что нет видимого формального конструктора, который бы напоминал им. В этом случае это происходит негласно, так как Stringтип имеет языковую поддержку в том, как он построен.

Следовательно, именно поэтому изменение testне видно вне области действия TestI(string)метода - мы передали ссылку по значению, и теперь это значение изменилось! Но если Stringссылка была передана по ссылке, то, когда ссылка изменилась, мы увидим ее вне области действия TestI(string)метода.

В этом случае необходимо использовать ключевое слово ref или out . Я чувствую, что 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";
    }
}
Дерек W
источник
ref = инициализированная внешняя функция, out = инициализированная внутри функции или другими словами; ссылка двухсторонняя, аут-аут. Поэтому обязательно используйте ref.
Пол Захра
@PaulZahra: outдолжен быть назначен в методе для кода для компиляции. refне имеет такого требования. Также outпараметры инициализируются вне метода - код в этом ответе является контрпримером.
Дерек W
Следует уточнить - outпараметры могут быть инициализированы вне метода, но не обязательно. В этом случае мы хотим инициализировать outпараметр, чтобы продемонстрировать точку зрения о природе stringтипа в .NET.
Дерек W
9

На самом деле это было бы то же самое для любого объекта в этом отношении, то есть быть ссылочным типом и передавать по ссылке - это две разные вещи в c #.

Это будет работать, но это применимо независимо от типа:

public static void TestI(ref string test)

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

eglasius
источник
7

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

Переменная является контейнером.

Переменная типа значения содержит экземпляр. Переменная ссылочного типа содержит указатель на экземпляр, хранящийся в другом месте.

Изменение переменной типа значения изменяет экземпляр, который она содержит. Изменение переменной ссылочного типа изменяет экземпляр, на который она указывает.

Отдельные переменные ссылочного типа могут указывать на один и тот же экземпляр. Следовательно, один и тот же экземпляр может быть видоизменен через любую переменную, которая указывает на него.

Переданный по значению аргумент - это новый контейнер с новой копией содержимого. Переданный по ссылке аргумент - это оригинальный контейнер с его исходным содержимым.

Когда аргумент типа значения передается по значению: переназначение содержимого аргумента не оказывает влияния вне области действия, поскольку контейнер является уникальным. Изменение аргумента не оказывает влияния вне области действия, поскольку экземпляр является независимой копией.

Когда аргумент ссылочного типа передается по значению: переназначение содержимого аргумента не оказывает влияния вне области действия, поскольку контейнер является уникальным. Изменение содержимого аргумента влияет на внешнюю область, поскольку скопированный указатель указывает на общий экземпляр.

Когда какой-либо аргумент передается по ссылке: переназначение содержимого аргумента влияет на внешнюю область, поскольку контейнер является общим. Изменение содержимого аргумента влияет на внешнюю область, поскольку содержимое является общим.

В заключение:

Строковая переменная является переменной ссылочного типа. Следовательно, он содержит указатель на экземпляр, хранящийся в другом месте. При передаче по значению его указатель копируется, поэтому изменение строкового аргумента должно повлиять на общий экземпляр. Однако строковый экземпляр не имеет изменяемых свойств, поэтому строковый аргумент не может быть изменен в любом случае. При передаче по ссылке контейнер указателя является общим, поэтому переназначение все равно будет влиять на внешнюю область.

Bryan
источник
6

« Картинка стоит тысячи слов ».

У меня есть простой пример, он похож на ваш случай.

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

Это то, что случилось:

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

  • Строки 1 и 2: s1и s2переменные ссылаются на один и тот же "abc"строковый объект.
  • Строка 3: поскольку строки являются неизменяемыми , поэтому "abc"строковый объект не изменяет себя (to "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.
        }
BornToCode
источник
1
Это объяснение сработало для меня лучше всего. Таким образом, в основном мы передаем все по значению, несмотря на то, что сама переменная является либо значением, либо ссылочным типом, если только мы не используем ключевое слово ref (или out). Это не заметно в нашем повседневном кодировании, потому что мы обычно не устанавливаем для наших объектов значение null или для другого экземпляра внутри метода, в который они были переданы, скорее мы устанавливаем их свойства или вызываем их методы. В случае «строки» установка его на новый экземпляр происходит постоянно, но обновление не видно, и это дает неверное толкование неподготовленному глазу. Поправь меня, если не прав.
Ε Г И І И О
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
Ваш ответ должен содержать больше, чем просто код. Он также должен содержать объяснение того, почему это работает.
Чарльз Колдуэлл