Swift-style-guide: Padrão de inicialização preguiçoso

Criado em 15 abr. 2015  ·  29Comentários  ·  Fonte: raywenderlich/swift-style-guide

Muitos de nós em tutoriais hoje em dia estamos usando padrões como este para inicializar variáveis:

lazy var locationManager: CLLocationManager = {
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
  }()

Eu vi isso para configurar controles como UILabels e outros também. Isso tem a vantagem de manter o código de inicialização em um local acessível.

Eu pensei em começar uma discussão para ver se este é um padrão que queremos encorajar no guia de estilo?

Comentários muito úteis

Como um FYI, Joe Groff de Swift confirmou que vars preguiçosos são automaticamente @noescape para que eles não se capturem.

https://twitter.com/jckarter/status/704100315587477504

Tenha em mente que você ainda pode capturar self de outras maneiras (por exemplo, variáveis ​​de bloco que capturam self dentro de uma var preguiçosa)

Todos 29 comentários

btw, o self é retido ciclicamente aqui? Acho que [unowned self] ou [weak self] devem ser adicionados

também tem uma pergunta:
Qual é o nome recomendado para strongSelf neste caso, não encontrei isso em seu guia:

if let strongSelf = self {

}

Não acho que importe que self seja retido ou não, porque o encerramento é executado sempre que a variável locationManager é usada e depois é destruída.

Embora eu não tenha certeza do que acontece quando você nunca chama locationManager . Nesse caso, o fechamento pode, de fato, estar se apegando a si mesmo. Interessante. :-)

Edit: Acabei de testar isso e usar self neste encerramento parece estar bem. Suponho que o encerramento não seja realmente criado até que você chame locationManager pela primeira vez. E então termina imediatamente e é destruído novamente.

A propósito, o ! não é necessário; isso funciona muito bem:

lazy var locationManager: CLLocationManager = {

Obrigado, atualizei o bloco de código original para remover o ! . Mas ainda precisamos discutir a questão principal - devemos incentivar esse padrão em nosso guia de estilo ou não?

Eu gosto disso. Mantém a inicialização da coisa próxima à declaração da coisa. Como resultado, viewDidLoad() tende a ficar mais curto e fácil de ler.

Eu gosto disso também. 👍

Se houver problemas com um ciclo de referência forte, [unowned self] deve corrigi-lo, eu acho:

lazy var locationManager: CLLocationManager = {
  [unowned self] in
  let manager = CLLocationManager()
  manager.delegate = self
  ...
  return manager
}()

Eu gosto dessa abordagem, mas estou lutando para ver as objeções. É apenas que essa abordagem não é óbvia e pode ter nuances sutis (como o potencial para ciclos de retenção fortes)?

(referência: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-ID57)

IMO isso é apenas um padrão. Talvez pertença a um repositório de padrões rápidos. Quanto aos ciclos de retenção, estou perdendo. É apenas uma atribuição típica de delegado. Por que isso deve ser tratado de forma diferente do que se você acabou de fazer a inicialização do CLLocationManager em um método de inicialização? O padrão delegate normalmente encoraja a propriedade delegate (neste caso, dentro de CLLocationManager) a ser definida como fraca, não?

Meu erro é forte reter ciclos. Não é um problema porque o encerramento é executado. Se estivesse retornando um encerramento, talvez fosse necessário usar [unowned self] , embora não neste caso porque está sendo atribuído como um delegado de posse fraca.

Também vale a pena ter em mente que o encerramento especificado para gerar o lazy var não deve depender de nenhuma propriedade mutável da classe. No ponto em que o var preguiçoso é gerado (através da avaliação do encerramento), ele capturará os valores atuais dessas dependências. Vai ser confuso se o var não mudar.

Veja este exemplo em http://mikebuss.com/2014/06/22/lazy-initialization-swift/

class Person {
    var name: String

    lazy var personalizedGreeting: String = {
        [unowned self] in
        return "Hello, \(self.name)!"
        }()

    init(name: String) {
        self.name = name
    }
}

Acho que isso está confuso:

let p = Person(name: "sam")
p.personalizedGreeting
// => "Hello, sam!"

p.name = "definitely somebody else"
p.personalizedGreeting
// => "Hello, sam!"

Depois de toda essa divagação, ainda acho que é um bom padrão e adequado para uso em tutoriais, mas é importante que os autores entendam essas questões para não cometer esses erros relativamente complexos.

/vou calar agora

Nos documentos da Apple que @sammyd vincula, a Apple realmente dá isso como exemplo:

lazy var asHTML: () -> String = {
    [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

No segundo link que @sammyd dá (ele está feliz hoje :] ), o autor dá isso como exemplo:

lazy var personalizedGreeting: String = {
        [unowned self] in
        return "Hello, \(self.name)!"
        }()

Eu não acho que usar unowned self será um problema nesses casos... ou seja, na criação de propriedades preguiçosas... eu imagino que em certos casos, porém (como @sammyd menciona novamente, retornando um bloco, talvez outros?) _não_ usar unowned pode ser problemático.

Para simplificar, por que não usar sempre unowned para propriedades preguiçosas?

Independentemente disso, sou a favor de usar esse padrão. :+1:

Observe que a variável asHTML _armazena_ o fechamento para uso posterior. O que estamos falando aqui é inicializar uma variável regular usando um encerramento que é executado imediatamente.

Eu gosto, tanto para propriedades lazy quanto para propriedades não lazy .

Não tenho certeza de qual seria a orientação neste guia. _Sempre_ assim? Se você tivesse algumas constantes ou cores calculadas, também posso ver o benefício de "à moda antiga" inicializar várias coisas em viewDidLoad() para reutilizar objetos. Parece-me uma coisa caso a caso.

Eu usei chamadas initWithFrame para instanciar visualizações de interface do usuário por anos com subvisualizações de uilabels / textviews / e isso simplifica o código e evita tropeçar em chamadas de ciclo de vida de exibição - principalmente ao chamar super classes.
Ele sofre um pequeno impacto no desempenho? Pode ser. Isso vai contra o fluxo do uikit e usando arquivos nib? sim. Passo horas solucionando problemas de arquivos nib? Não. Meus aplicativos são enviados no prazo? sim. Estou preocupado? Não. Outros desenvolvedores estão chocados? provavelmente. Eu me importo - Não.
Código simples = lógica simples - Simples de manter.

Alguns argumentariam que, para desempenho ou memória, até que o controlador de visualização seja apresentado (ele foi enviado para o controle de navegação) - então não instancia os controles (aguarde por viewDidLoad / loadView ). Isso foi antes de termos acesso a vars preguiçosos nos próprios controladores de exibição.
Então, isso resolve esse problema de instanciação preventiva.

por exemplo.
preguiçoso var loginVC = { return LoginVC()}()

Nota - eu substituí o método init aqui - (sem arquivos xib)

required public init() {
    super.init(nibName: nil, bundle: nil)
}

Do stackoverflow
http://stackoverflow.com/questions/27585409/using-a-programmatically-created-uitableviewcell-subclass-in-swift/31801507#31801507

Isto

class EventCell: UITableViewCell {

var eventName: UILabel = UILabel()
var eventCity: UILabel = UILabel()
var eventTime: UILabel = UILabel()

override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
    super.init(style: style, reuseIdentifier: reuseIdentifier)

    self.contentView.addSubview(eventName)
    self.contentView.addSubview(eventCity)
    self.contentView.addSubview(eventTime)
}

override func layoutSubviews() {
    super.layoutSubviews()

    eventName = UILabel(frame: CGRectMake(20, 10, self.bounds.size.width - 40, 25))
    eventCity = UILabel(frame: CGRectMake(0, 0, 0, 0))
    eventTime = UILabel(frame: CGRectMake(0, 0, 0, 0))

}

}

VS

class EventCell: UITableViewCell {
   lazy public  var lblName = {
    return UILabel (frame: CGRectMake(10, 0, self.bounds.size.width , 40))
  }()

  lazy public  var lblCity = {
   return UILabel (frame: CGRectMake(10, 40, self.bounds.size.width , 20))
  }()
 lazy public  var lblTime = {
      return UILabel (frame: CGRectMake(self.bounds.size.width - 80, 10, , 25))
   }()


   override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
   super.init(style: style, reuseIdentifier: reuseIdentifier)

    self.contentView.addSubview(lblName)
    self.contentView.addSubview(lblCity)
    self.contentView.addSubview(lblTime)
 }

}  

Sobre [unowned self] usado para evitar o ciclo de retenção: não é mais o caso.

O código de exemplo fornecido pela Apple e vinculado acima por @sammyd não usa mais [unowned self] :

lazy var asHTML: Void -> String = {
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

E, de fato, eu nunca poderia perder um deinit usando um var preguiçoso inicializado com um fechamento que faz referência a um self nu.

Agora eu não sei se self é implicitamente unowned ou unowned(unsafe) nesses encerramentos. No entanto, devemos parar de torná-lo explícito, pois é implícito.

O exemplo asHTML não está inicializando um valor com um closure, ele está armazenando um closure em uma variável. No caso asHTML, [unowned self] ainda é necessário para evitar um ciclo de referência forte. Tudo isso é mencionado na documentação vinculada do Swift.

Eric Chamberlain, arquiteto-chefe - iOS
ArcTouch - Estúdio de desenvolvimento de aplicativos

eric. [email protected] mailto:[email protected]
+1-415-944-2000

Aplicativos personalizados para marcas de classe mundial e as 500 maiores da Fortune
arctouch.com/work | arctouch.com/blog

Em 8 de dezembro de 2015, às 8h18, Gwendal Roué [email protected] escreveu:

Sobre [unowned self] usado para evitar o ciclo de retenção: não é mais o caso.

O código de amostra https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097 -CH20-ID57 fornecido pela Apple e vinculado acima por @sammyd https://github.com/sammyd não usa mais [unowned self]:

lazy var asHTML: Void -> String = {
if deixe texto = self.text {
return "<(self.name)>(texto)(self.name)>"
} senão {
return "<(self.name) />"
}
}
E, de fato, eu nunca poderia perder um deinit usando um var preguiçoso inicializado com um fechamento que faz referência a um eu nu.

Agora eu não sei se self é implicitamente sem dono ou sem dono (inseguro) nesses fechamentos. No entanto, devemos parar de torná-lo explícito, pois é implícito.


Responda a este e-mail diretamente ou visualize-o no GitHub https://github.com/raywenderlich/swift-style-guide/issues/88#issuecomment -162933300.

@arctouch-ericchamberlain, você está certo, o código de exemplo agora armazena um fechamento na propriedade.

No entanto, repito: "Eu nunca poderia perder um deinit usando um var preguiçoso inicializado com um fechamento que faz referência a um eu nu".

Por exemplo:

class C {
    lazy var d: String = { "\(self)" }()
    deinit { print("deinit") }
}

C()   // deinit
C().d // deinit

Você pode tentar você mesmo. Se você encontrar um ciclo de retenção, ficarei feliz em usar [unowned self] novamente.

Isso é porque

preguiçoso var algo = {..}()

não retém nada. O fechamento é construído e executado quando a variável é inicializada. Enquanto,

preguiçoso var algo = {…}

cria um ciclo de referência forte quando a variável é inicializada.

A variável asHTML no código de exemplo se enquadra no último caso.

Remova () do seu inicializador e veja o que acontece.

Eric Chamberlain, arquiteto-chefe - iOS
ArcTouch - Estúdio de desenvolvimento de aplicativos

eric. [email protected] mailto:[email protected]
+1-415-944-2000

Aplicativos personalizados para marcas de classe mundial e as 500 maiores da Fortune
arctouch.com/work | arctouch.com/blog

Em 9 de dezembro de 2015, às 14h10, Gwendal Roué [email protected] escreveu:

@arctouch-ericchamberlain https://github.com/arctouch-ericchamberlain , você está certo, o código de exemplo agora armazena um encerramento na propriedade.

No entanto, repito: "Eu nunca poderia perder um deinit usando um var preguiçoso inicializado com um fechamento que faz referência a um eu nu".

Por exemplo:

classe C {
preguiçoso var d: String = { "(self)" }()
deinit { print("deinit") }
}

C() // encerra
C().d // deinit
Você pode tentar você mesmo. Se você encontrar um ciclo de retenção, ficarei feliz em usar [unowned self] novamente.


Responda a este e-mail diretamente ou visualize-o no GitHub https://github.com/raywenderlich/swift-style-guide/issues/88#issuecomment -163413290.

Eu não te sigo.

Nenhuma dessas vars preguiçosas requer [unowned self] :

lazy var d: String = { "\(self)" }()
lazy var asHTML: Void -> String = { "\(self)" }

Que tipo de var preguiçoso faz?

Ops, chamar asHTML realmente cria o ciclo de retenção.

Obrigado @arctouch-ericchamberlain. Eu estive enganado.

@groue

A razão pela qual você descobriu que os documentos da Apple não usam [unowned self] é porque eles estão configurando a situação em que há um problema:

Infelizmente, a classe HTMLElement, conforme escrito acima, cria um ciclo de referência forte entre uma instância de HTMLElement e o encerramento usado para seu valor asHTML padrão.

Eles então corrigem isso com a adição de [unowned self] :

Uma referência sem proprietário é o método de captura apropriado a ser usado para resolver o ciclo de referência forte no exemplo HTMLElement anterior.

O que resulta nesta implementação:

lazy var asHTML: Void -> String = {
    [unowned self] in
    if let text = self.text {
        return "<\(self.name)>\(text)</\(self.name)>"
    } else {
        return "<\(self.name) />"
    }
}

Essa página de documentação tem uma boa descrição de quando os ciclos de retenção são possíveis e quando você deve usar unowned ou weak para sair dele.

@johndpope

Tente comparar com esta implementação, não tenho certeza do que perdi, mas as amostras que vi da Apple tendem a evitar colchetes de fechamento apenas para definir coisas.

class EventCell: UITableViewCell {

    lazy public var lblName = UILabel(frame: CGRectZero)
    lazy public var lblCity = UILabel(frame: CGRect())
    lazy public var lblTime = UILabel(frame: CGRect(x: 0, y: 0, width: 0, height: 0))

} 

Discordo da noção de que lazy deve ser usado para facilitar a leitura. Seu único propósito é realmente o desempenho, já que pode introduzir problemas com o estado, por exemplo, usando propriedades mutáveis ​​dentro do encerramento. Você já tem a inicialização em um lugar, é chamado de init designado.

Você está usando lazy porque tem algo pesado que pode não ser executado, portanto, não é necessário ou deseja atrasar sua execução, o que coincidentemente permite que o usuário aborte.

Portanto, as chances são de que o fechamento se mantenha após sua propriedade da referida classe, criando um vazamento.
Sempre use fraco/sem dono, mesmo que tenha 100% de certeza de que o encerramento será chamado.
Eventualmente, eu suspeito que o compilador irá lidar com isso. Uma propriedade lazy com closure só pode ser executada uma vez, pense dispatch_once(..) . De qualquer forma, é provavelmente assim que eu vou rolar.

Primeiro, vamos tirar a referência de memória do caminho. (Mesmo os posts de blog de maior sucesso sobre preguiçosos erram isso. Precisamos mudar isso Maxime!) Por razões descritas acima, isso não vaza. Ou seja: O encerramento é executado preguiçosamente uma vez. Se for executado, self é retido e depois liberado. Se não for executado, nunca será retido.

O código de exemplo abaixo mostra isso definitivamente:

import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

class Person {
  init(name: String) {
    self.name = name
    print("\(name) is born.")
  }

  deinit {
    print("\(name) dies.")
  }

  var name: String

  lazy var greeting: String = {
    return "Hello \(self.name)!"
  }()

}

do {
  let t = Person(name: "Ray")
  print(t.greeting)
}

print ("Next")

// Prints: 
// Ray is born.
// Hello Ray!
// Ray dies.
// Next

do {
  let t = Person(name: "Ray")
}

print ("End of the world")

// Prints:
// Ray is born.
// Ray dies.
// End of the world

Em seguida, vamos olhar para toda essa coisa preguiçosa. Esta é uma ótima maneira de controlar a inicialização em relação ao controlador de visualização, pois você paga exatamente no ponto de uso. É particularmente importante para CLLocationManager , pois é provável que você queira controlar quando o acesso acontece para que ele não solicite permissões no meio da sequência.

Outro padrão que é um pouco menos parecido com javascript é fazer um método privado chamado e chamá-lo:

lazy var locationManager: CLLocationManager = self.makeLocationManager()

private func makeLocationManager() -> CLLocationManager
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
}

Algumas notas:

  • Não tenho certeza de qual versão do Swift isso se tornou possível.
  • Sem vazamento de pilha; mesmos motivos acima. Não acredite em mim? Tente!
  • locationManager tem uma anotação de tipo explícita. No momento, isso é exigido pelo compilador ou você recebe uma mensagem de erro enigmática sobre self não resolvido.
  • Funciona para classes, mas não para estruturas imutáveis.

Para componentes de interface do usuário que não são alocados em um storyboard ou incondicionalmente em viewDidLoad() , acho que lazy é um bom padrão. Mas acho que não estou pronto para dizer "preguiçoso em todas as coisas", pois pode exigir alguns truques, como LazyBoxes, para contornar erros de imutabilidade.

Devo mencionar que, embora não tenha sido aceito, provavelmente há uma mudança bastante grande no modo como o lazy funciona:
https://github.com/apple/swift-evolution/blob/master/proposals/0030-property-behavior-decls.md

O que você pensa sobre isso:

Preguiçoso
Considere o uso de inicialização lenta para um controle de granulação mais fino sobre a vida útil do objeto.

// Mark: - Location services
extension MyViewController {
  lazy var locationManager: CLLocationManager = self.makeLocationManager()

  private func makeLocationManager() -> CLLocationManager
    let manager = CLLocationManager()
    manager.desiredAccuracy = kCLLocationAccuracyBest
    manager.delegate = self
    manager.requestAlwaysAuthorization()
    return manager
  }
  // :
}

Eu não me importo com a inicialização do estilo de bloco de variáveis. Na verdade, colocar essas inicializações na declaração da variável permite uma substituição fácil do teste de unidade / interface do usuário.

A propósito, definir locationManager como propriedade de classe não está funcionando por causa da atribuição de manager.delegate = self

O que você sugere?

Apenas faça sua classe (view controller?) estar em conformidade com CLLocationManagerDelegate.

@rayfix Movi este snippet para super class. Eu deixei de incluir o protocolo CLLocationManagerDelegate na base.

Obrigado 😀

Como um FYI, Joe Groff de Swift confirmou que vars preguiçosos são automaticamente @noescape para que eles não se capturem.

https://twitter.com/jckarter/status/704100315587477504

Tenha em mente que você ainda pode capturar self de outras maneiras (por exemplo, variáveis ​​de bloco que capturam self dentro de uma var preguiçosa)

Só queria que você soubesse que, usando a nova visualização de gráfico de memória no Xcode 8, descobri que houve uma captura quando não usei a construção self sem dono no seguinte:

lazy var locationManager: CLLocationManager = {
    [unowned self] in
    var _locationManager = CLLocationManager()
    _locationManager.delegate = self
    _locationManager.desiredAccuracy = Set.shared.trackingAccuracy
    _locationManager.allowsBackgroundLocationUpdates = true
    _locationManager.activityType = .fitness      
    _locationManager.distanceFilter = Double(Set.shared.waypointInterval)
    return _locationManager
}()

Mas isso não ocorreu quando eu o incluí e não fiz outras alterações.

@rondiel note que o delegado do CLLocationManager foi declarado como atribuição (iOS 10 SDK)
@property(assign, nonatomic, nullable) id<CLLocationManagerDelegate> delegate;

Esta página foi útil?
0 / 5 - 0 avaliações

Questões relacionadas

xezun picture xezun  ·  6Comentários

jackwu95 picture jackwu95  ·  6Comentários

hollance picture hollance  ·  28Comentários

grosch picture grosch  ·  6Comentários

rayfix picture rayfix  ·  3Comentários