PDA

Просмотр полной версии : [Статья] Различия между ref и value type переменными.


Sinyss
30.07.2013, 03:08
В своей прошлой статье ([Ссылки могут видеть только зарегистрированные и активированные пользователи]) я обещал уделить внимание различиям между значимым(value) и ссылочным(reference) типом переменных.
С# предоставляет 2 типа - класс и структура, которые почти идентичны, но 1й ссылочного, а 2й значимого типа.
Что такое структура?
Если по-простому то это обрезанный класс. Если из класса убрать поддержку наследования, методов сборки мусора то вы получите структуру. Структуры определяются так же как и классы, только используется ключевое слово struct вместо class. Структуры так же обладают теми же членами что и классы, конструкторы, поля, методы, свойства, операторы. Простой пример структуры:

struct Point
{
private int x, y; // приватные поля

public Point (int x, int y) // конструктор
{
this.x = x;
this.y = y;
}

public int X // свойство
{
get {return x;}
set {x = value;}
}

public int Y
{
get {return y;}
set {y = value;}
}
}


Значимые и ссылочные типы:
Есть еще 1 различие между структурой и классом и оно самое важное для понимания. В рантайме(во время исполнения программы) виртуальная машина обращается с значимыми и ссылочными типами по разному. Когда создается структура, в памяти выделяется 1 место для хранения ее значения (То же касается примитивных типов, таких как int, float, bool, char). Когда рантайму надо значение переменной он напрямую обращается к значению, что может быть очень эффективно, в частности с примитивными типами.
Со ссылочными типами все иначе, объект создается в памяти а потом управляется через отдельную ссылку, которая скорее указатель. Допустим Point это структура, а Form это класс. Создание экземпляра происходит так:

Point p1 = new Point(); // Структура
Form f1 = new Form(); // Класс

В первом случае 1 место выделяется для хранения значения p1. А во втором случае выделяется 2 места , 1 для объекта, а второе для его ссылки. Будет понятней если переписать 2ю строку так:
Form f1; // Размещаем ссылку на обьект (null)
f1 = new Form(); // Размещаем значение обьекта

Если мы скопируем объекты в новые переменные:
Point p2 = p1;
Form f2 = f1;
p2, будучи структурой становится независимой копией p1, со своими, отдельными полями.
Но в случае с f2, все что мы скопировали это ссылка, и f1 и f2 указывают на 1 и тот же объект.

ВАЖНОЕ
При передаче как параметр в метод:
В C# по умолчанию параметры передаются по значению, что означает что они будут скопированы и потом переданы методу. Для значимых типов это означает физическое копирование экземпляра(как p2 было скопировано), в то время как для ссылочных типов это означает копирование только ссылки. Пример:

Point myPoint = new Point (0, 0); // новая value-type переменная
Form myForm = new Form(); // новая reference-type переменная
Test (myPoint, myForm); // Метод для теста

void Test (Point p, Form f)
{
p.X = 100; // Без еффекта на MyPoint поскольку p - копия
f.Text = "Hello, World!"; // Это изменит текст в myForm поскольку
// myForm и f указывают на 1 объект
f = null; // без эффекта на myForm
}
Присвоение f значения null ничего не изменит, поскольку это копия ссылки, и мы стерли только копию.
Мы можем изменить то как передаются параметры с помощью ключевого слова ref. Когда мы передаем "по ссылке", метод обращается напрямую к аргументам с которыми метод был вызван. В примере ниже вы можете представить что f и p были замещены myPoint и myForm

Point myPoint = new Point (0, 0); // новая value-type переменная
Form myForm = new Form(); // новая reference-type переменная
Test (ref myPoint, ref myForm); // передаем myPoint и myForm по ссылке

void Test (ref Point p, ref Form f)
{
p.X = 100; // Это изменит позицию myPoint (ну или значение свойства X если точнее).
f.Text = “Hello, World!”; // Это изменит текст в myForm
f = null; // Это уничтожит переменную myForm
}
В этом случае присвоение null к f также сделает myForm null, потому что в этот раз мы работаем с оригинальной ссылкой на переменную а не с ее копией.

Расположение в памяти:
CLR выделяет память для объектов в двух местах: в стеке и в куче. Стек - простая структура которая работает по принципу первый зашел - последний вышел (first-in-last-out) FILO. Когда вызывается метод CLR сохраняет ссылку на верхушку стека. Метод запихивает(push) данные в стек по мере выполнения. Когда метод завершается, CLR "выпихивает"(pop) данные сохраненные методом в стеке пока не найдет сохраненную ссылку.
Куча в то же время может быть представлена как случайно перемешанная куча объектов. Ее преимущество в том что она позволяет объектам размещаться и убираться из кучи в любом порядке. Куча требует траты ресурсов на менеджер памяти(дефрагментация объектов) и сборщик мусора(определяет, есть ли в стеке ссылка на объект, если нету - удаляет объект.) что бы держать кучу в порядке.
Для демонстрации работы стека и кучи рассмотрим пример:
void CreateNewTextBox()
{
TextBox myTextBox = new TextBox(); // TextBox - класс
}
В этом методе мы создаем локальную переменную которая указывает на объект(короче - адрес). Локальная переменная хранится в стеке, в то время как сам объект хранится в куче.
Стек всегда используется для хранения 2х вещей:
1) Ссылочной части локальных переменных ссылочного типа и параметров (таких как ссылка на myTextBox).
2) Локальных переменных ссылочного типа и параметров метода (структуры, простые типы)
В куче хранятся:
1) Содержимое объектов ссылочного типа.
2) Любая структура внутри объекта ссылочного типа. (Например если в классе есть структура то она будет хранится в куче а не в стеке.)

Очистка памяти
После того как мы выполнили CreateNewTextBox() локальная переменная myTextBox, которая хранится в стеке будет выпихнута из стека. Но что произойдет с объектом на который она ссылалась? Мы можем игнорировать его, сборщик мусора отловит его позже(когда именно - не известно) и автоматически удалит из кучи.
Сборщик знает что надо удалить его, потому что, на него не ссылаются из стека или нету такой цепочки ссылок которая бы привела к этому объекту. Программисты C++ которые заботятся об утечках памяти захотят его удалить, но вообще то невозможно удалить конкретный объект программно. Мы должны полагаться на очистку памяти средствами CLR.
НО! У автоматической сборки мусора есть фича. Объекты которые работают с другими ресурсами(кроме памяти) (в частности "handles", такие как Windows handles, file handles, SQL handles) должны быть закрыты уже программно(то есть приказать прекратить использовать ресурс. например File.Close()) когда в этих ресурсах уже нету нужды. Тоже касается всех компонентов Windows, поскольку у них есть Windows handles. Вы можете спросить, почему нельзя утилизировать такие ресурсы в финалайзерах объекта(финалайзер - метод который увеличивает приоритет объекта быть утилизированным сборщиком мусора). Основная причина в том что сборщик мусора больше заботится о памяти а не о ресурсах. Так что на ПК где есть пару гигабайт свободной оперативной памяти, сборщик мусора может ждать один-два часика прежде чем запустится. (из логики что оперативная память все равно не надо операционной системе).
Вернемся к нашему TextBox, пример был немного искусственный, поскольку что бы компонент отобразился надо было бы его добавить в Controls какого то элемента. Дополнительный бонус от такого размещения в том что когда этот элемент будет закрыт то для каждого его элемента в Controls будет автоматически вызван метод Dispose, который "отпускает" Windows handle и убирает его с экрана.

Все классы которые поддерживают интерфейс IDisposable (в том числе все Windows.Forms компоненты) содержат метод Dispose. Этот метод должен быть вызван когда в объекте больше нет необходимости для того что бы очистить ресурсы, кроме памяти. Есть 2 метода его вызова:
1) Вручную (просто вручную вызвать метод Dispose)
2) Автоматически, если компонент был добавлен в контейнер(Form, Panel, TabPage, пользовательский элемент). Контейнер сам позаботится о своем уничтожении и уничтожении своих элементов.

Тоже самое касается классов типа FileStream их тоже надо "уничтожать" вручную. К счастью C# предоставляет спец. конструкцию using для уничтожения таких объектов:
using (Stream s = File.Create ("myfile.txt"))
{
...
}
Данный код можно перевести в следующий:
Stream s = File.Create ("myfile.txt");
try
{
...
}
finally
{
if (s != null) s.Dispose();
}
Блок finally позаботится что метод Dispose будет вызван если произойдет исключение.

Большинство элементов не содержат обвертки в виде неуправляемых handles которые требуют вызова Dispose(). Так что по большей части вы можете игнорировать Dispose в WPF!

Спасибо за внимание.
PS: Если нашли ошибку - отпишите в личку, в 2 ночи дописывал статью, мог что то пропустить.

Буянь
02.08.2013, 15:59
Долго втыкал по поводу ref'ов. И подумал, а так не будет нагляднее?



class Program
{
static void Main()
{
Point pointForFoo = new Point();
LikeAForm formForFoo = new LikeAForm();
Point pointForRefFoo = new Point();
LikeAForm formForRefFoo = new LikeAForm();

Foo(pointForFoo, formForFoo);

Console.WriteLine("main>> Form.Text: '" + formForFoo.Text + "';");
Console.WriteLine("main>> Point.X: " + pointForFoo.X);

refFoo(ref pointForRefFoo, ref formForRefFoo);
if (formForRefFoo == null)
Console.WriteLine("main>> Form.Text: null;");
Console.WriteLine("main>> Point.X: " + pointForRefFoo.X);

Console.ReadLine();
}

static void Foo(Point p, LikeAForm f)
{
f.Text = "From foo";
p.X = 15;

f = null;
}

static void refFoo(ref Point p, ref LikeAForm f)
{
f.Text = "From ref foo";
p.X = 105;

f = null;
}
}

struct Point
{
private int _x, _y;

public int X
{
get {return _x;}
set
{
Console.WriteLine("Set X point: " + value); //Мы такие современные, ко-ко-ко, давайте выводить текст в консоль из сеттера?
_x = value;
}
}

public int Y
{
get
{return _y;}
set
{
Console.WriteLine("Set Y point: " + value);
_y = value;
}
}
}

class LikeAForm
{
private string _text;

public string Text
{
get
{return _text;}
set
{
Console.WriteLine("text_setter>> \'" + value + "\'");
_text = value;
}
}
}





[Ссылки могут видеть только зарегистрированные и активированные пользователи] ([Ссылки могут видеть только зарегистрированные и активированные пользователи])

Sinyss
02.08.2013, 16:16
Так читателю придется компилить код, что бы понять, в моем случае достаточно читать...
PS: Вообще выходит довольно много информации в 1й статье, тяжело за раз воспринимать, надо чем то разбавлять...