Тип ссылки на С#?

Я знаю, что "строка" в С# является ссылочным типом. Это на 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. Тем не менее, я получаю" перед прохождением "перед прохождением" заставляя себя казаться, что он передается по значению не по ссылке. Я понимаю, что строки неизменяемы, но я не вижу, как это объясняет, что здесь происходит. Что мне не хватает? Спасибо.

Ответ 1

Ссылка на строку передается по значению. Там большая разница между передачей ссылки по значению и передачей объекта по ссылке. К сожалению, слово "ссылка" используется в обоих случаях.

Если вы передадите ссылку на ссылку по ссылке, она будет работать так, как вы ожидаете:

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 referring to
        test.Append("changing");
    }
}

Подробнее см. мою статью о передаче параметров.

Ответ 2

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

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

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

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

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

Ответ 3

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

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

test = "after passing";

то test больше не ссылается на исходный объект. Мы создали объект new 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";
    }
}

Ответ 4

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

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

public static void TestI(ref string test)

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

Ответ 5

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

Переменная - это контейнер.

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

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

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

Аргумент by-by-value - это новый контейнер с новой копией содержимого. Аргумент pass-by-reference - это исходный контейнер с его исходным контентом.

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

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

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

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

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

Ответ 6

Выше ответы полезны, я хотел бы добавить пример, который, я думаю, демонстрирует, что происходит, когда мы передаем параметр без ключевого слова 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.
        }

Ответ 7

Try:


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

Ответ 8

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

 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
 }