Классы или структуры. Типы значений и ссылочные типы

Последнее обновление: 11.12.2023

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

Все типы данных в F# можно разделить на типы значений (value types), еще называемые значимыми типами, и ссылочные типы (reference types). Важно понимать между ними различия.

К типам значений относятся: структуры и примитивные типы bool, byte, sbyte, int16, uint16, int, uint, int64, uint64, nativeint, unativeint, decimal, float, float32, char. Стоит отметить, что данные примитивные типы сами по сути реализованы как структуры. А классы и тип string (который по сути представляет класс) относятся к ссылочным типам (reference types).

Для того, чтобы понять различие между типами значений и ссылочными типами, надо понять организацию памяти в .NET. Здесь память делится на два типа: стек и куча (heap). Параметры и переменные функций, которые представляют типы значений, размещают свое значение в стеке. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещается поверх предыдущего. Время жизни переменных таких типов ограничено их контекстом. Физически стек - это некоторая область памяти в адресном пространстве.

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

Например, рассмотрим следующую программу

let calculate n = 
    let a = 1
    let b = 2 
    let c = a + b + n
    printfn $"c = {c}"

calculate 5

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

Стек Stack in F#

При вызове функции calculate в еe фрейм в стеке будут помещаться значения n,a, b и c. Они определяются в контексте данной функции. Когда метод функциия, область памяти, которая выделялась под стек, впоследствии может быть использована другими функциями.

Причем если параметр или переменная метода представляет тип значений, то в стеке будет храниться непосредственное значение этого параметра или определяемых в ней с помощью let значений. Например, в данном случае значения и параметр функции calculate представляют значимый тип - тип int, поэтому в стеке будут храниться их числовые значения.

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

Для сравнения поведения определим следующую программу:

type Country() = class
    [<DefaultValue>]
    val mutable x: int
    [<DefaultValue>]
    val mutable y: int
end

type State = struct
    val mutable x: int
    val mutable y: int
end

let test() = 
    let state1  = State()
    let country1 = Country()
    ()
    
test()

Здесь определены структура State и класс Country, которые содержат два поля типа int. Они фактически выглядят аналогично.

Также здесь определена функция test, в которой определяется значение типа State и значение типа Country. В итоге при выполнении этой функции в стеке выделяется память для объекта state`:

let state1 = State()

Далее в стеке создается ссылка для объекта country1:

let country1 = Country()

Ссылка в стеке для объекта country1 будет представлять адрес на место в хипе, по которому в реальности размещены данные этого объекта.

Value types and reference types in F#

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

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

Копирование значений

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

Например, возьмем в начале структуры:

type State = struct
    val mutable x: int
    val mutable y: int
end

let test() = 
    let mutable state1 = State()    // Структура State
    let mutable state2 = State()
    state2.x <- 1
    state2.y <- 2
    state1 <- state2          // присваиваем переменной state1 значение структуры state2
    state2.x <- 5            // state1.x=1 по-прежнему
    printfn $"state1.x = {state1.x}"        // state1.x = 1
    printfn $"state2.x = {state2.x}"        // state2.x = 5
    
test()

Так как state1 - структура, то при присвоении state1 <- state2 она получает копию структуры state2. Поэтому изменения в одной структуре никак не затронут другую структуру.

Теперь рассмотрим аналогичный пример с классами:

type Country() = class
    [<DefaultValue>]
    val mutable x: int
    [<DefaultValue>]
    val mutable y: int
end

let test() = 
    let mutable country1 = Country()    // Класс Country
    let mutable country2 = Country()
    country2.x <- 1
    country2.y <- 2
    country1 <- country2          // присваиваем переменной country1 значение объекта country2
    country2.x <- 7                // теперь и country1.x = 7, так как обе ссылки и country1 и country2 
                                    // указывают на один объект в хипе
    printfn $"country1.x = {country1.x}"        // country1.x = 7
    printfn $"country2.x = {country2.x}"        // country2.x = 7
    
test()

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

Объекты структур и классов как параметры функций

Организацию объектов в памяти следует учитывать при передаче объектов в качестве параметров в функции. Если параметр функции представляет структуру, то передаваемое при вызове функция получает копию структуры. Но по умолчанию мы не сможем изменить значение переданного параметра:

type Person = struct
    
    val mutable name: string
end
let changePerson (person: Person) = 
    person.name <- "Sam"		// ! ошибка
    printfn $"Name: {person.name}"

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

type Person() = class
    
    [<DefaultValue>]
    val mutable name: string
end
let changePerson (person: Person) = 
    person.name <- "Sam"

let mutable tom = Person()
tom.name <- "Tom"
printfn $"Name: {tom.name}"     // Name: Tom
changePerson(tom)				
printfn $"Name: {tom.name}"     // Name: Sam
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850