Ссылка на ссылку в С#?

Как мы все знаем, объект классов С# рассматривается как ссылки, поэтому что происходит, когда вы передаете ссылочный объект в качестве ссылки на метод? Скажем, у нас есть:

public class A { ... }

а затем:

public void F(ref A a) { ... }

Означает ли компилятор, что a уже является ссылочным типом и сохраняет его таким образом, или он создает новую ссылку на этот объект?

И что, если у нас есть что-то вроде этого:

public void F(ref A a)
{
    F(ref a);
}

В этом коде, помимо очевидного StackOverflowException, компилятор создает ссылку на ссылку... для ссылки на a, который является ссылочным объектом?

Ответ 1

Это лучше всего иллюстрируется примером:

public class C { public int P { get; set; } }
public class X
{
    static void M(C c1, C c2, ref C c3, ref C c4)
    {
      c1.P = 11;
      c2 = new C() { P = 12 };
      c3.P = 13;
      c4 = new C() { P = 14 };
    }
    static void Main()
    {
      C q1 = new C() { P = 1 };
      C q2 = new C() { P = 2 };
      C q3 = new C() { P = 3 };
      C q4 = new C() { P = 4 };
      M(q1, q2, ref q3, ref q4);
      Console.WriteLine(q1.P);
      Console.WriteLine(q2.P);
      Console.WriteLine(q3.P);
      Console.WriteLine(q4.P);
    }
}

Что происходит?

q1 и c1 относятся к одному и тому же объекту, но относятся к другим переменным. Мутирование c1.P мутирует q1.P, потому что обе переменные относятся к одному и тому же объекту, поэтому q1 теперь 11.

q2 и c2 относятся к одному и тому же объекту, но относятся к другим переменным. Мутирование c2 не мутирует q2, так как c2 и q2 - разные переменные; изменение одного не меняет другого. q2 остается 2, и новый объект теряется.

q3 и c3 - два имени для одной и той же переменной и, следовательно, относятся к одному и тому же объекту. Когда вы меняете c3.P, который автоматически меняет q3.P, потому что это два имени для одной и той же вещи.

q4 и c4 - два имени для одной и той же переменной, поэтому мутация q4 также мутирует c4.

Это имеет смысл?

К сожалению, ключевое слово для "сделать псевдоним этой переменной" - это "ref". Было бы более ясно, был ли это "псевдоним".

Чтобы ответить на второй вопрос: нет, это не создает цепочку ссылок. Давайте сделаем более ясный пример:

...
int c1 = 123;
M(ref c1);
...
void M1(ref int q1) { M2(ref q1); }
void M2(ref int q2) { M2(ref q2); }

Это говорит о том, что c1 и q1 - разные имена для одной и той же переменной, а q1 и q2 - разные имена для одной и той же переменной, поэтому c1, q1 и q2 - все псевдонимы друг для друга. Там никогда нет ссылки на ссылку на переменную в С# так, как есть на С++.

Ответ 2

В вызове типа

F(ref a);   // ByRef parameter

переменная a "используется непосредственно" телом метода F. Там только одно место хранения. Если метод F присваивает свой параметр, это присваивание будет доступно всем, кто может видеть a, немедленно. И наоборот, если кто-то (за пределами F) присваивает a, тогда как метод F запущен, тогда параметр F внезапно переключится на новый объект.

С другой стороны, при вызове типа

F(a);  // normal value parameter

переменная a сначала копируется в новую переменную, а затем новая переменная используется внутри F. Теперь, если тип параметра F является типом значения (например, struct или enum), копировать выполняется по значению. Таким образом, копируются все данные. Но если тип параметра является ссылочным типом (class (включая тип массива), interface, delegate), копия a включает копию только ссылки.

Чтобы проверить ваше понимание значения параметра с параметром типа class, выяснить, что делают эти методы:

static void F1(List<int> list>)  // no ref modifier
{
    list.Clear();
}

static void F2(List<int> list>)  // no ref modifier
{
    list = new List<int>();
}

Вот, возможно, интересный пример с ref:

static void G(ref string a, ref string b)
{
    if (string.Equals(a, b))
        b += "_unique";

    // Is it now safe to say that a and b are distinct?
    // No. Anyone could have changed either a or b by now.
    // For example, a and b could "alias" public fields visisble to other threads.
}

В качестве примера использования G выше рассмотрим код var x = "initial"; G(ref x, ref x);, и в этом случае a будет изменяться вместе с b внутри метода G.

Ответ 3

ref просто создает ссылку на исходное значение. С ссылочными типами, что "значение" - это местоположение переменной памяти. Когда вы используете ref, метод теперь может изменить исходную ссылку на переменную. Если вы затем сделаете то же самое с аргументом, который уже есть ref, второй метод просто имеет ту же ссылку, что и первый метод.

Ответ 4

Два понятия не одно и то же. Параметр метода может быть изменен с помощью ref независимо от того, является ли он типом значения или ссылочным типом.

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

static void Main()
{
    Foo item = new Foo("aaa");
    GetByReference(ref item);
    Console.WriteLine(item.Name)
}
static void ChangeByReference(ref Foo itemRef)
{
    itemRef = new Foo("bbb");
}

это будет фактически печатать "bbb", потому что в этом случае вы не изменили значения объектов, но вы сами изменили сам объект

Ответ 5

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

public void F(ref A a, A b){
    a = new A(1);
    b.Property = 12;
    b = new B(2);
}

public void G(){
    A a = new A(0);
    A b = new A(0);
    F(a,b);
    System.Console.WriteLine(a + " - " + b);
}

Вывод 1-12, поскольку указатель объекта b не изменяется, но исходный объект изменяется.

Ответ 6

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

Ссылочные типы и ссылочные параметры представляют собой различные объекты. В С# переменные всегда передаются по значению. Это значение может быть ссылкой на другой объект или сохраненное значение.

Другими словами, ссылочные типы "передаются по ссылке", потому что, когда вы передаете экземпляр объекта методу, метод получает ссылку на экземпляр объекта.
В случае ссылочных параметров ссылка на переменную (следовательно, поэтому имеет смысл подумать об этом как псевдоним). Это другая форма "передачи по ссылке".

Переход по вашему примеру:

public void F(ref A a)
{
    F(ref a);
}

Здесь, как у нас, есть единственный объект (исходный параметр a), на который ссылаются бесконечные времена. (Обратите внимание, что это не то, что на самом деле происходит). Эта диаграмма призвана обеспечить идиоматическое представление того, что происходит под обложками при работе с эталонными параметрами.

enter image description here

Дополнительную информацию см. в разделе 1.6.6.1 спецификации 4.0 С#.