понедельник, 3 января 2022 г.

Передача параметров в C #

 

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

Когда вы передаете экземпляр в качестве аргумента методу, он передает copy экземпляр. Теперь, если экземпляр, который вы передаете, является value type(находится в stack), передаете вы копию Этого значения, поэтому, если вы измените его, оно не будет отражено в вызывающем. Если экземпляр является ссылочным типом, вы передаете копию ссылки (опять же находящуюся в stack) объекту. Итак, у вас есть две ссылки на один и тот же объект. Оба они могут быть объектом. Но если в теле метода вы создадите экземпляр нового объекта, ваша копия ссылки больше не будет ссылаться на исходный объект, она будет ссылаться на новый объект, который вы только что создали. Таким образом, у вас будет 2 ссылки и 2 объекта.

Передача параметров в C #

Многие люди довольно запутались в том, как передаются параметры в C #, особенно в отношении ссылочных типов. Эта страница должна помочь прояснить некоторую путаницу. Если у вас есть предложения, как это сделать более понятным, напишите мне.

У Microsoft также есть хорошая страница по этой теме (которая, как мне кажется, использует ту же терминологию, что и эта страница - дайте мне знать, если они не согласны).

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

Оглавление

Преамбула: что такое ссылочный тип?

Примечание. У меня есть целая статья о ссылочных типах и типах значений, которые могут оказаться полезными, особенно если вы новичок в .NET. Эта преамбула является более короткой версией той же информации, но если вы обнаружите что-то непонятное, вероятно, стоит прочитать обе.

В .NET (и, следовательно, в C #) существует два основных типа типов: ссылочные типы и типы значений . Классы, делегаты и интерфейсы являются ссылочными типами; структуры и перечисления являются типами значений. Они действуют по-разному, и большая путаница с передачей параметров на самом деле связана с тем, что люди не понимают должным образом разницу между ними. Вот краткое объяснение:

Ссылочный тип представляет собой тип , который имеет в качестве своего значения в ссылку на соответствующие данные , а не сами данные. Например, рассмотрим следующий код:

StringBuilder sb = новый StringBuilder ();

(Я использовал StringBuilderкак случайный пример ссылочного типа - в этом нет ничего особенного.) Здесь мы объявляем переменную sb, создаем новый StringBuilderобъект и назначаем sbссылку на объект. Значение sb- это не сам объект, это ссылка. Присвоение ссылочных типов простое - присваиваемое значение является значением выражения / переменной, то есть ссылка:

StringBuilder first = новый StringBuilder ();
first.Append ( "привет" );
StringBuilder second = first;
Console.WriteLine (второй); // Печатает привет

Здесь мы объявляем переменную first, создаем новый StringBuilderобъект и назначаем firstссылку на объект. Затем мы присваиваем secondзначение firstЭто означает, что они оба относятся к одному и тому же объекту. Если мы first.Appendизменим содержимое объекта с помощью другого вызова , это изменение будет видно через second:

StringBuilder first = новый StringBuilder ();
first.Append ( "привет" );
StringBuilder second = first;
first.Append ("мир");
Console.WriteLine (второй); // Выводит привет, мир

Однако они сами по себе остаются независимыми переменными. Изменение значения firstдля ссылки на совершенно другой объект (или действительно изменение значения на пустую ссылку) вообще не влияет secondна объект, на который он ссылается:

StringBuilder first = новый StringBuilder ();
first.Append ( "привет" );
StringBuilder second = first;
first.Append ("мир");
first = new StringBuilder ("до свидания");
Console.WriteLine (первая); // Распечатывает до свидания
Console.WriteLine (второй); // По-прежнему печатает привет, мир

Повторюсь: типы классов, типы интерфейсов, типы делегатов и типы массивов - все это ссылочные типы.

Дальнейшая преамбула: что такое тип значения?

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

публичная  структура IntHolder
{
    public  int i;
}

Везде, где есть переменная типа IntHolder, значение этой переменной содержит все данные - в данном случае единственное целочисленное значение. Присваивание копирует значение, как показано здесь:

IntHolder first = новый IntHolder ();
first.i = 5;
IntHolder second = first;
first.i = 6;
Console.WriteLine (second.i);

(Загрузить образец кода) Вывод:

5

Здесь second.iимеет значение 5, потому что это значение first.iимеет, когда second = firstпроисходит присвоение - значения в second не зависят от значений, firstкроме того, когда происходит присвоение.

Простые типы (такие как floatintchar), перечисляемые типы и типы Struct все типы значений.

Обратите внимание, что многие типы (например, string) некоторым образом выглядят как типы-значения, но на самом деле являются ссылочными типами. Они известны как неизменяемые типы. Это означает, что однажды созданный экземпляр не может быть изменен. Это позволяет ссылочному типу действовать аналогично типу значения в некоторых отношениях - в частности, если у вас есть ссылка на неизменяемый объект, вы можете чувствовать себя комфортно, возвращая его из метода или передавая его другому методу, безопасно в знаниях. что это не будет изменено за вашей спиной. Вот почему, например,string.Replace не изменяет строку, для которой он вызывается, но возвращает новый экземпляр с новыми строковыми данными в - если исходная строка была изменена, любые другие переменные, содержащие ссылку на строку, увидят изменение, что очень редко бывает желанный.

Сравните это с изменяемым (изменяемым) типом, например ArrayList- если метод возвращает ArrayListссылку, хранящуюся в переменной экземпляра, вызывающий код может затем добавлять элементы в список, при этом экземпляр не может сказать об этом, что обычно является проблемой. Сказав, что неизменяемые ссылочные типы действуют как типы значений, они не являются типами значений, и их не следует рассматривать как фактические типы значений.

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

Проверяем, понимаете ли вы преамбулу ...

Что бы вы ожидали увидеть из приведенного выше кода, если бы объявление IntHolderтипа было как класс, а не структура? Если вы не понимаете, почему результат был таким 6, пожалуйста, перечитайте обе преамбулы и напишите мне, если он все еще неясен - если вы его не понимаете, это моя вина, а не ваша, и мне нужно улучшить эту страницу. Если вы делаете это понимают, передача параметров становится очень легко понять - читайте дальше.

Различные виды параметров

В C # есть четыре различных типа параметров: параметры значения (по умолчанию), ссылочные параметры (которые используют 

Параметры значения

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

void Foo (StringBuilder x)
{
    х = ноль ;
}

...

StringBuilder y = новый StringBuilder ();
y.Append ( "привет" );
Фу (у);
Console.WriteLine (y == null );

(Загрузить образец кода) Вывод:

Ложь

Значение yне изменяется только потому, что xустановлено на nullОднако помните, что значение переменной ссылочного типа является ссылкой - если две переменные ссылочного типа ссылаются на один и тот же объект, то изменения данных в этом объекте будут видны через обе переменные. Например:

void Foo (StringBuilder x)
{
    x.Append ( "мир" );
}

...

StringBuilder y = новый StringBuilder ();
y.Append ( "привет" );
Фу (у);
Console.WriteLine (y);

(Загрузить образец кода) Вывод:

Привет мир

После вызова Fooобъект StringBuilder, на который y ссылается, содержит "hello world", поскольку в Fooданных "world" был добавлен к этому объекту через содержащуюся ссылку x.

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

недействительными Foo (IntHolder х)
{
    xi = 10;
}

...

IntHolder y = новый IntHolder ();
yi = 5;
Фу (у);
Console.WriteLine (yi);

(Загрузить образец кода) Вывод:

5

Когда Fooвызывается, xначинается как структура со значением i=5iЗатем его значение изменяется на 10. Fooничего не знает о переменной y, и после завершения метода значение in yбудет точно таким же, как было раньше (т.е. 5).

Как и раньше, убедитесь, что вы понимаете, что произойдет, если он IntHolder будет объявлен как класс, а не структура. Вы должны понимать, почему в таком случае y.i после звонка будет 10 Foo.

Справочные параметры

Ссылочные параметры не передают значения переменных, используемых при вызове члена функции - они используют сами переменные. Вместо того, чтобы создавать новое место хранения для переменной в объявлении члена функции, используется то же место хранения, поэтому значение переменной в члене функции и значение ссылочного параметра всегда будут одинаковыми. Ссылочные параметры нуждаются в refмодификаторе как в объявлении, так и в вызове - это означает, что он всегда ясен, когда вы передаете что-то по ссылке. Давайте посмотрим на наши предыдущие примеры, просто изменив параметр на параметр ссылки:

void Foo ( ссылка StringBuilder x)
{
    х = ноль ;
}

...

StringBuilder y = новый StringBuilder ();
y.Append ( "привет" );
Foo ( ссылка );
Console.WriteLine (y == null );

(Загрузить образец кода) Вывод:

Истинный

Здесь, поскольку yпередается ссылка на, а не на его значение, изменения значения параметра xнемедленно отражаются в yВ приведенном выше примере yоказывается nullСравните это с результатом того же кода без refмодификаторов.

Теперь рассмотрим код структуры, который у нас был ранее, но с использованием ссылочных параметров:

void Foo ( ссылка IntHolder x)
{
    xi = 10;
}

...

IntHolder y = новый IntHolder ();
yi = 5;
Foo ( ссылка );
Console.WriteLine (yi);

(Загрузить образец кода) Вывод:

10

Две переменные совместно используют место хранения, поэтому изменения xтакже видны насквозь y, поэтому y.iв конце этого кода указано значение 10.

Заметка на полях: в чем разница между передачей объекта значения по ссылке и объекта ссылки по значению?

Вы могли заметить, что последний пример, передача структуры по ссылке, имел тот же эффект в этом коде, что и передача класса по значению. Однако это не значит, что это одно и то же. Рассмотрим следующий код:

void Foo (??? IntHolder x)
{
    х = новый IntHolder ();
}

...

IntHolder y = новый IntHolder ();
yi = 5;
Foo (??? y);

Рассмотрим случай, когда IntHolder- структура (т. Е. Тип значения), а параметр является параметром ссылки (т. Е. Заменить ???на refприведенный выше). После вызова Foo(ref y)значение yявляется новым IntHolderзначением, т. y.iЕ. 0.

В случае, если IntHolderэто класс (то есть ссылочный тип), а параметр является параметром значения (т.е. удалить ???выше), значение yне изменяется - это ссылка на тот же объект, который был до вызова функции-члена. Это различие абсолютно важно для понимания передачи параметров в C #, и поэтому я считаю, что очень сбивает с толку утверждение, что объекты передаются по ссылке по умолчанию, а не правильное утверждение, что ссылки на объекты по умолчанию передаются по значению.

Выходные параметры

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

Выходные параметры очень похожи на эталонные параметры. Единственные отличия:

  • Переменной, указанной при вызове, необязательно должно быть присвоено значение перед ее передачей члену функции. Если функция-член завершается нормально, переменная считается присвоенной впоследствии (чтобы вы могли ее «прочитать»).
  • Параметр считается изначально неназначенным (другими словами, вы должны присвоить ему значение, прежде чем вы сможете «прочитать» его в члене функции).
  • Параметру должно быть присвоено значение до того, как функция-член завершится в обычном режиме.

Вот пример кода, показывающий это, с intпараметром ( int это тип значения, но если вы правильно поняли ссылочные параметры, вы сможете увидеть, каково поведение ссылочных типов):

void Foo ( вне  int x)
{
    // Не могу прочитать здесь x - он считается неназначенным

    // Присвоение - это должно произойти до того, как метод сможет нормально завершиться
    х = 10;

    // Теперь можно прочитать значение x: 
    int a = x;
}

...

// Объявляем переменную, но не присваиваем ей значение 
int y;

// Передаем его как выходной параметр, даже если его значение не присвоено 
Foo ( out y);

// Теперь ему присвоено значение, поэтому мы можем его записать:
Console.WriteLine (y);

(Загрузить образец кода) Вывод:

10

Массивы параметров

Массивы параметров позволяют передавать переменное количество аргументов в функцию-член. Определение параметра должно включать paramsмодификатор, но использование параметра не имеет такого ключевого слова. Массив параметров должен находиться в конце списка параметров и должен быть одномерным массивом. При использовании функции-члена в вызове может появиться любое количество параметров (в том числе ни одного), если каждый из параметров совместим с типом массива параметров. В качестве альтернативы можно передать один массив, и в этом случае параметр действует так же, как параметр обычного значения. Например:

void ShowNumbers ( params  int [] числа)
{
    foreach ( int x в числах)
    {
        Console.Write (x + "" );
    }
    Console.WriteLine ();
}

...

int [] x = {1, 2, 3};
ShowNumbers (x);
ShowNumbers (4, 5);

(Загрузить образец кода) Вывод:

1 2 3 
4 5 

При первом вызове значение переменной xпередается по значению, поскольку типом xуже является массив. Во втором вызове создается новый массив целых чисел, содержащий два указанных значения, и передается ссылка на этот массив (по-прежнему по значению).

Мини-глоссарий

Некоторые неофициальные определения и резюме терминов:

Член функции
Член функции - это метод, свойство, событие, индексатор, определяемый пользователем оператор, конструктор экземпляра, статический конструктор или деструктор.
Выходной параметр
Параметр, очень похожий на ссылочный параметр, но с другими определенными правилами назначения.
Ссылочный параметр (семантика передачи по ссылке)
Параметр, который разделяет место хранения переменной, используемой при вызове члена функции. Поскольку они используют одно и то же место хранения, они всегда имеют одно и то же значение (поэтому изменение значения параметра приводит к изменению значения переменной вызова).
Тип ссылки
Тип, в котором значение переменной / выражения этого типа является ссылкой на объект, а не на сам объект.
Место хранения
Часть памяти, содержащая значение переменной.
Параметр значения (семантика по умолчанию, передаваемая по значению)
Параметр значения, который имеет собственное место хранения и, следовательно, собственное значение. Начальное значение - это значение выражения, используемого при вызове члена функции.
Тип значения
Тип, в котором значение переменной / выражения этого типа - это сами данные объекта.
Переменная
Имя, связанное с местом и типом хранилища. (Обычно с местом хранения связана одна переменная. Исключения составляют ссылочные и выходные параметры.)

Еще один пример кода, чтобы установить это:

void Main()
{


    int k = 0;
    TestPlain(k);
    Console.WriteLine("TestPlain:" + k);

    TestRef(ref k);
    Console.WriteLine("TestRef:" + k);

    string t = "test";

    TestObjPlain(t);
    Console.WriteLine("TestObjPlain:" +t);

    TestObjRef(ref t);
    Console.WriteLine("TestObjRef:" + t);
}

public static void TestPlain(int i)
{
    i = 5;
}

public static void TestRef(ref int i)
{
    i = 5;
}

public static void TestObjPlain(string s)
{
    s = "TestObjPlain";
}

public static void TestObjRef(ref string s)
{
    s = "TestObjRef";
}

И выход:

TestPlain: 0 

TestRef: 5 

TestObjPlain: тест 

TestObjRef: TestObjRef

Комментариев нет:

Отправить комментарий

Паттерн 'Репозиторий' в ASP.NET

  Последнее обновление: 1.11.2015         Одним из наиболее часто используемых паттернов при работе с данными является паттерн 'Репозито...