Предотвратите цикл самосохранения в комбинированной плоской карте

#swift #memory-management #memory-leaks #combine

Вопрос:

Мне трудно понять, как создать конвейер быстрого объединения, содержащий .flatMap ссылку на который self . Чтобы предотвратить цикл сохранения, это должно быть [weak self] ссылкой, но это не работает с a .flatMap .

Вот упрощенный пример, показывающий мою проблему:

 import Foundation
import Combine

class SomeService {
    func someOperation(label: String) -> Future<String, Never> {
        Future { promise in
            print("Starting work for", label)
            
            DispatchQueue.main.asyncAfter(deadline: .now()   2) {
                print("Finished work for", label)

                promise(.success(label))
            }
        }
    }
}

class SomeDataSource {
    let someService = SomeService()
    var cancellables = Set<AnyCancellable>()
    
    deinit {
        print("Deinit SomeDataSource")
    }
    
    func complexOperation() {
        // First part 'defined' is inside the complexOperation method:
        someService.someOperation(label: "First part")
            // Second part is 'defined' in another method (it is shared with other tasks)
            .flatMap { _ in self.partOfComplexOperation(label: "Second part") } // <--- This creates a retain cycle
            .sink { label in
                print("Received value in sink", label)
            }
            .store(in: amp;cancellables)
    }
    
    func partOfComplexOperation(label: String) -> Future<String, Never> {
        someService.someOperation(label: label)
    }
}

var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
 

Выход:

 Starting work for First part
Finished work for First part
Starting work for Second part
Finished work for Second part
Received value in sink Second part
Deinit SomeDataSource
 

Проблема здесь в том, что я хочу SomeDataSource , чтобы мой был деинициализирован сразу после запуска «Первой части» и даже не начинал вторую часть. Итак, результат, который я ищу, это:

 Starting work for First part
Deinit SomeDataSource
Finished work for First part
 

Я ищу какое — то сочетание .flatMap и .compactMap . Существует ли это? Если я сначала .compactMap { [weak self] _ in self } получу ожидаемый результат, но, может быть, есть лучший способ?

 import Foundation
import Combine

class SomeService {
    func someOperation(label: String) -> Future<String, Never> {
        Future { promise in
            print("Starting work for", label)
            
            DispatchQueue.main.asyncAfter(deadline: .now()   2) {
                print("Finished work for", label)

                promise(.success(label))
            }
        }
    }
}

class SomeDataSource {
    let someService = SomeService()
    var cancellables = Set<AnyCancellable>()
    
    deinit {
        print("Deinit SomeDataSource")
    }
    
    func complexOperation() {
        // First part 'defined' is inside the complexOperation method:
        someService.someOperation(label: "First part")
            .compactMap { [weak self] _ in self }
            // Second part is 'defined' in another method (it is shared with other tasks)
            .flatMap { dataSource in dataSource.partOfComplexOperation(label: "Second part") }
            .sink { label in
                print("Received value in sink", label)
            }
            .store(in: amp;cancellables)
    }
    
    func partOfComplexOperation(label: String) -> Future<String, Never> {
        someService.someOperation(label: label)
    }
}

var someDataSource: SomeDataSource? = SomeDataSource()
someDataSource?.complexOperation()
someDataSource = nil
 

Выход:

 Starting work for First part
Deinit SomeDataSource
Finished work for First part
 

Ответ №1:

Решение здесь состоит в том, чтобы не сохранять себя. Вы даже не хотите видеть себя на плоской карте, так зачем ее сохранять…

 let label = someService.someOperation(label: "First part")
    .flatMap { [someService] _ in 
        someService.someOperation(label: label)
    }
 

Конечно, просмотр всей этой работы someService подразумевает, что в сервисе отсутствует какая-то функциональность. Видя, что результат someOperation игнорируется, это также может быть красным флагом.

Если бы вы действительно оказались в ситуации, когда вам пришлось использовать себя, то решение выглядело бы так:

 let foo = someOperation()
    .flatMap { [weak self] in 
        self?.otherOperation() ?? Empty(completeImmediately: true)
    }
 

Или вы могли бы подумать о чем-то вроде:

 someOperation()
    .compactMap { [weak someService] _ in
        someService?.otherOperation()
    }
    .switchToLatest()
 

Который будет отменен otherOperation() , если поступит новое событие от someOperation() .