Время жизни сохраняемой памяти при быстром закрытии

#generics #swift #memoization

#дженерики #swift #запоминание

Вопрос:

В расширенном выступлении Swift на WWDC 2014 докладчик привел этот пример запоминания функций с использованием дженериков:

 func memoize<T: Hashable, U>( body: (T)->U ) -> (T)->U {
    var memo = Dictionary<T, U>()
    return { x in
        if let q = memo[x] { return q }
        let r = body(x)
        memo[x] = r
        return r
    }
}
  

У меня возникли проблемы с пониманием времени жизни этого var. memo Содержит ли каждый вызов fibonacci запоминаемой функции строгую ссылку на нее? И если да, то как бы вы освободили эту память, когда закончите с этим?

Ответ №1:

В терминологии блоков C / Objective-C memo это __block переменная (в Swift вам не нужно явно записывать __block для захвата переменных по ссылке). Переменная может быть назначена в блоке (замыкании), и все области, которые видят эту переменную, будут видеть изменения от любой другой переменной (они имеют общую ссылку на переменную). Переменная будет действительна до тех пор, пока некоторый блок (замыкание), который ее использует (в данном случае есть только один блок, который его использует), все еще жив. После освобождения последнего блока, который его использует, переменная выходит из области видимости. Как это работает, является деталью реализации.

Если бы эта переменная имела тип указателя объекта, то объект, на который указывается, будет сохранен любыми блоками, которые его захватывают. Однако в этом случае переменная имеет Dictionary тип struct, который является типом значения. Таким образом, нет необходимости беспокоиться об управлении памятью. Переменная — это структура, а структура живет столько же, сколько и переменная. (Сама структура может выделять память в другом месте и освобождать ее в своем деструкторе, но это полностью внутренне обрабатывается структурой, и внешняя сторона не должна знать или заботиться об этом.)

Обычно не нужно беспокоиться о том, как __block переменные работают внутри. Но, по сути, переменная заключена в упрощенную форму «объекта», причем фактическая «переменная» является полем этого «объекта», который управляется памятью посредством подсчета ссылок. Блоки, которые их захватывают, содержат «сильные ссылки» на этот псевдо-объект — когда в куче создается блок (технически, когда они копируются из блоков стека в кучу), который использует эту __block переменную, это увеличивает количество ссылок; когда блок, который его использует, освобождается, он уменьшаетсяколичество ссылок. Когда счетчик ссылок становится равным 0, этот псевдо-«объект» освобождается, сначала вызывая соответствующий деструктор для его типа переменной.

Чтобы ответить на ваш вопрос, «запоминаемая функция Фибоначчи» представляет собой блок (замыкание). И это то, что содержит сильную ссылку на все, что содержит memo переменную. «Вызовы» не имеют сильных или слабых ссылок на него; когда вызывается функция, она использует ссылку, которую имеет сама функция. Время жизни memo переменной — это время жизни «запомнившейся функции Фибоначчи» в данном случае, потому что это единственное замыкание, которое фиксирует эту переменную.

Ответ №2:

Каждый вызов memoize() будет создавать свою собственную memo переменную, независимую от других вызовов. Он живет до тех пор, пока существует ссылающееся на него замыкание; когда замыкание освобождается, переменные, захваченные им (в данном случае memo ), также освобождаются.

Вы можете думать об этом как о возврате структуры, подобной этому псевдокоду:

 struct Closure<T,U>
{
    var memo: Dictionary<T, U>
    func call(t: T): U
    {
        ....
    }
}

func memoize<T: Hashable, U>( body: (T)->U ) -> Closure<T,U>
{
    return Closure(memo: Dictionary<T, U>())
}
  

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

1. Это не то же самое, что этот код, потому что несколько замыканий могут захватить эту переменную, и переменная будет состоянием, общим для всех замыканий. Кроме того, переменное состояние также разделяется между замыканием и кодом вне замыкания (т. Е. Код в memoize функции напрямую также может считывать и записывать memo ).

2. @newacct Код предназначен для иллюстрации этого конкретного случая, а не замыканий в целом.

Ответ №3:

Внутренний блок (возвращаемое значение) сохранит memo, что означает, что memo будет сохраняться до тех пор, пока сохраняется возвращенный blck.

Новый экземпляр memo будет создаваться каждый раз при вызове функции memoize . Вызов возвращенного блока не приведет к созданию новых экземпляров memo.

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