Ранее мы рассматривали следующие элементарные типы данных: int, byte, double, string, object и др. Также есть сложные типы: структуры, перечисления, классы. Все эти типы данных можно разделить на типы значений, еще называемые значимыми типами, (value types) и ссылочные типы (reference types). Важно понимать между ними различия.
Типы значений (значимые типы):
- Целочисленные типы (
byte, sbyte, short, ushort, int, uint, long, ulong
) - Типы с плавающей запятой (
float, double
) - Тип
decimal
- Тип
bool
- Тип
char
- Перечисления
enum
- Структуры (
struct
)
Ссылочные типы:
- Тип
object
- Тип
string
- Классы (
class
) - Интерфейсы (
interface
) - Делегаты (
delegate
)
В чем же между ними различия? Для этого надо понять организацию памяти в .NET. Здесь память делится на два типа: стек и куча (heap). Параметры и переменные метода, которые представляют типы значений, размещают свое значение в стеке. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещается поверх предыдущего. Время жизни переменных таких типов ограничено их контекстом. Физически стек — это некоторая область памяти в адресном пространстве.
Когда программа только запускается на выполнение, в конце блока памяти, зарезервированного для стека устанавливается указатель стека. При помещении данных в стек указатель переустанавливается таким образом, что снова указывает на новое свободное место. При вызове каждого отдельного метода в стеке будет выделяться область памяти или фрейм стека, где будут храниться значения его параметров и переменных.
Например:
class Program
{
static void Main(string[] args)
{
Calculate(5);
}
static void Calculate(int t)
{
int x = 6;
int y = 7;
int z = y + t;
}
}
Пи запуске такой программы в стеке будут определяться два фрейма — для метода Main (так как он вызывается при запуске программы) и для метода Calculate:
При вызове этого метода Calculate в его фрейм в стеке будут помещаться значения t, x, y и z. Они определяются в контексте данного метода. Когда метод отработает, область памяти, которая выделялась под стек, впоследствии может быть использована другими методами.
Причем если параметр или переменная метода представляет тип значений, то в стеке будет храниться непосредсвенное значение этого параметра или переменной. Например, в данном случае переменные и параметр метода Calculate представляют значимый тип — тип int, поэтому в стеке будут храниться их числовые значения.
Ссылочные типы хранятся в куче или хипе, которую можно представить как неупорядоченный набор разнородных объектов. Физически это остальная часть памяти, которая доступна процессу.
При создании объекта ссылочного типа в стеке помещается ссылка на адрес в куче (хипе). Когда объект ссылочного типа перестает использоваться, в дело вступает автоматический сборщик мусора: он видит, что на объект в хипе нету больше ссылок, условно удаляет этот объект и очищает память — фактически помечает, что данный сегмент памяти может быть использован для хранения других данных.
Так, в частности, если мы изменим метод Calculate следующим образом:
static void Calculate(int t)
{
object x = 6;
int y = 7;
int z = y + t;
}
То теперь значение переменной x будет храниться в куче, так как она представляет ссылочный тип object, а в стеке будет храниться ссылка на объект в куче.
Составные типы
Теперь рассмотим ситуацию, когда тип значений и ссылочный тип представляют составные типы — структуру и класс:
State state1 = new State(); // State - структура, ее данные размещены в стеке
Country country1 = new Country(); // Country - класс, в стек помещается ссылка на адрес в хипе
// а в хипе располагаются все данные объекта country1
struct State
{
public int x;
public int y;
}
class Country
{
public int x;
public int y;
}
Таким образом, в стеке окажутся все поля структуры state1 и ссылка на объект country1 в хипе.
Но, допустим, в структуре State также определена переменная ссылочного типа Country. Где она будет хранить свое значение, если она определена в типе значений?
State state1 = new State();
Country country1 = new Country();
struct State
{
public int x;
public int y;
public Country country = new();
}
class Country
{
public int x;
public int y;
}
Значение переменной state1.country также будет храниться в куче, так как эта переменная представляет ссылочный тип:
Копирование значений
Тип данных надо учитывать при копировании значений. При присвоении данных объекту значимого типа он получает копию данных. При присвоении данных объекту ссылочного типа он получает не копию объекта, а ссылку на этот объект в хипе. Например:
State state1 = new State(); // Структура State
State state2 = new State();
state2.x = 1;
state2.y = 2;
state1 = state2;
state2.x = 5; // state1.x=1 по-прежнему
Console.WriteLine(state1.x); // 1
Console.WriteLine(state2.x); // 5
Country country1 = new Country(); // Класс Country
Country country2 = new Country();
country2.x = 1;
country2.y = 4;
country1 = country2;
country2.x = 7; // теперь и country1.x = 7, так как обе ссылки и country1 и country2
// указывают на один объект в хипе
Console.WriteLine(country1.x); // 7
Console.WriteLine(country2.x); // 7
Так как state1 — структура, то при присвоении state1 = state2
она получает копию структуры state2. А объект класса country1 при присвоении country1 = country2;
получает ссылку на тот же объект, на который указывает country2. Поэтому с изменением country2, так же будет меняться и country1.
Ссылочные типы внутри типов значений
Теперь рассмотрим более изощренный пример, когда внутри структуры у нас может быть переменная ссылочного типа, например, какого-нибудь класса:
State state1 = new State();
State state2 = new State();
state2.country = new Country();
state2.country.x = 5;
state1 = state2;
state2.country.x = 8; // теперь и state1.country.x=8, так как state1.country и state2.country
// указывают на один объект в хипе
Console.WriteLine(state1.country.x); // 8
Console.WriteLine(state2.country.x); // 8
struct State
{
public int x;
public int y;
public Country country = new(); // выделение памяти для объекта Country
}
class Country
{
public int x;
public int y;
}
Переменные ссылочных типов в структурах также сохраняют в стеке ссылку на объект в хипе. И при присвоении двух структур state1 = state2;
структура state1 также получит ссылку на объект country в хипе. Поэтому изменение state2.country повлечет за собой также изменение state1.country.
Объекты классов как параметры методов
Организацию объектов в памяти следует учитывать при передаче параметров по значению и по ссылке. Если параметры методов представляют объекты классов, то использование параметров имеет некоторые особенности. Например, создадим метод, который в качестве параметра принимает объект Person:
Person p = new Person { name = "Tom", age = 23 };
ChangePerson(p);
Console.WriteLine(p.name); // Alice
Console.WriteLine(p.age); // 23
void ChangePerson(Person person)
{
// сработает
person.name = "Alice";
// сработает только в рамках данного метода
person = new Person { name = "Bill", age = 45 };
Console.WriteLine(person.name); // Bill
}
class Person
{
public string name = "";
public int age;
}
При передаче объекта класса по значению в метод передается копия ссылки на объект. Эта копия указывает на тот же объект, что и исходная ссылка, потому мы можем изменить отдельные поля и свойства объекта, но не можем изменить сам объект. Поэтому в примере выше сработает только строка person.name = "Alice"
.
А другая строка person = new Person { name = "Bill", age = 45 }
создаст новый объект в памяти, и person теперь будет указывать на новый объект в памяти. Даже если после этого мы его изменим, то это никак не повлияет на ссылку p
в методе Main, поскольку ссылка p все еще указывает на старый объект в памяти.
Но при передаче параметра по ссылке (с помощью ключевого слова ref) в метод в качестве аргумента передается сама ссылка на объект в памяти. Поэтому можно изменить как поля и свойства объекта, так и сам объект:
Person p = new Person { name = "Tom", age = 23 };
ChangePerson(ref p);
Console.WriteLine(p.name); // Bill
Console.WriteLine(p.age); // 45
void ChangePerson(ref Person person)
{
// сработает
person.name = "Alice";
// сработает
person = new Person { name = "Bill", age = 45 };
}
class Person
{
public string name = "";
public int age;
}
Операция new
создаст новый объект в памяти, и теперь ссылка person (она же ссылка p из метода Main) будет указывать уже на новый объект в памяти.
C# ref в сравнении с out
Ключевые слова Ref
и out
в C# используются для передачи аргументов внутри метода или функции. Оба слова указывают на то, что аргумент/параметр передается по ссылке. По умолчанию параметры передаются в метод по значению. Используя эти ключевые слова (ref
и out
), мы можем передать параметр по ссылке.
Ключевое слово ref
Ключевое слово ref
означает, что значение будет передано по ссылке и упаковка/преобразование производится не будет. Это означает, что любые изменения, внесенные в этот аргумент в методе, будут отражены в этой переменной, когда управление вернется к вызывающему методу.
Пример кода
public static string GetNextName(ref int id)
{
string returnText = "Next-" + id.ToString();
id += 1;
return returnText;
}
static void Main(string[] args)
{
int i = 1;
Console.WriteLine("Previous value of integer i:" + i.ToString());
string test = GetNextName(ref i);
Console.WriteLine("Current value of integer i:" + i.ToString());
}
Вывод
Ключевое слово out
Ключевое слово out
передает аргументы по ссылке. Это очень похоже на ключевое слово ref
.
Пример кода
public static string GetNextNameByOut(out int id)
{
id = 1;
string returnText = "Next-" + id.ToString();
return returnText;
}
static void Main(string[] args)
{
int i = 0;
Console.WriteLine("Previous value of integer i:" + i.ToString());
string test = GetNextNameByOut(out i);
Console.WriteLine("Current value of integer i:" + i.ToString());
}
Вывод
Ref в сравнении с Out
Ref | Out |
Параметр или аргумент должен быть сначала инициализирован, прежде чем он будет передан в ref . | Инициализация параметра или аргумента перед передачей его в out не является обязательной. |
Не требуется присваивать или инициализировать значение параметра (который передается по ref ) перед возвратом в вызывающий метод. | Вызываемый метод обязан присвоить или инициализировать значение параметра (который передается в out ) перед возвратом в вызывающий метод. |
Передача значения параметра по Ref полезна, когда вызываемый метод также должен модифицировать передаваемый параметр. | Объявление параметра в методе out полезно, когда из функции или метода необходимо вернуть несколько значений. |
Инициализация значения параметра перед его использованием в вызывающем методе не обязательна. | Значение параметра должно быть инициализировано в вызывающем методе перед его использованием. |
Когда мы используем REF , данные могут передаваться двунаправленно. | Когда мы используем OUT , данные передаются только однонаправленно (от вызываемого метода к вызывающему методу). |
И ref , и out по-разному обрабатываются во время выполнения программы, а во время компиляции они обрабатываются одинаково. | |
Свойства не являются переменными, поэтому они не могут быть переданы в качестве параметра out или ref . |
Ключевое слово Ref / Out и перегрузка методов
И ref
, и out
обрабатываются по-разному во время выполнения программы, и одинаково во время компиляции, поэтому методы не могут быть перегружены, если один метод принимает аргумент как ref
, а другой — как out
.
Пример кода
public static string GetNextName(ref int id)
{
string returnText = "Next-" + id.ToString();
id += 1;
return returnText;
}
public static string GetNextName(out int id)
{
id = 1;
string returnText = "Next-" + id.ToString();
return returnText;
}
Вывод при компиляции кода:
Однако перегрузка методов возможна, когда один метод принимает аргумент ref
или out
, а другой принимает тот же аргумент без ref
или out
.
Пример кода
public static string GetNextName(int id)
{
string returnText = "Next-" + id.ToString();
id += 1;
return returnText;
}
public static string GetNextName(ref int id)
{
string returnText = "Next-" + id.ToString();
id += 1;
return returnText;
}
Резюме
Ключевые слова out
и ref
полезны, когда мы хотим вернуть значение в тех же переменных, которые были переданы в качестве аргумента.