Qual é a palavra-chave `some` no Swift (UI)?

259

O novo tutorial do SwiftUI possui o seguinte código:

struct ContentView: View {
    var body: some View {
        Text("Hello World")
    }
}

A segunda linha da palavra somee em seu site é destacada como se fosse uma palavra-chave.

O Swift 5.1 parece não ter someuma palavra-chave, e não vejo o que mais a palavra somepoderia estar fazendo lá, uma vez que vai para onde o tipo costuma ir. Existe uma nova versão não anunciada do Swift? É uma função que está sendo usada em um tipo de uma maneira que eu não conhecia?

O que a palavra-chave somefaz?

Nicholas
fonte
Para quem ficou tonto com o assunto, aqui está um artigo muito decodificador e passo a passo, graças a Vadim Bulavin. vadimbulavin.com/…
Luc-Olivier

Respostas:

333

some Viewé um tipo de resultado opaco, conforme introduzido pela SE-0244 e está disponível no Swift 5.1 com Xcode 11. Você pode pensar nisso como um espaço reservado genérico "reverso".

Ao contrário de um espaço reservado genérico regular que é atendido pelo chamador:

protocol P {}
struct S1 : P {}
struct S2 : P {}

func foo<T : P>(_ x: T) {}
foo(S1()) // Caller chooses T == S1.
foo(S2()) // Caller chooses T == S2.

Um tipo de resultado opaco é um espaço reservado genérico implícito satisfeito com a implementação , para que você possa pensar sobre isso:

func bar() -> some P {
  return S1() // Implementation chooses S1 for the opaque result.
}

como esta:

func bar() -> <Output : P> Output {
  return S1() // Implementation chooses Output == S1.
}

De fato, o objetivo final com esse recurso é permitir genéricos reversos dessa forma mais explícita, o que também permitiria adicionar restrições, por exemplo -> <T : Collection> T where T.Element == Int. Veja este post para mais informações .

A principal coisa a tirar de tudo isto é que uma função que retorna some Pé aquele que retorna um valor de um específico único tipo concreto que está em conformidade com P. Tentar retornar diferentes tipos conformes dentro da função gera um erro do compilador:

// error: Function declares an opaque return type, but the return
// statements in its body do not have matching underlying types.
func bar(_ x: Int) -> some P {
  if x > 10 {
    return S1()
  } else {
    return S2()
  }
}

Como o espaço reservado genérico implícito não pode ser satisfeito por vários tipos.

Isso contrasta com uma função retornada P, que pode ser usada para representar ambos S1 e S2porque representa um Pvalor conforme arbitrário :

func baz(_ x: Int) -> P {
  if x > 10 {
    return S1()
  } else {
    return S2()
  }
}

Certo, quais benefícios os tipos de resultados opacos -> some Ptêm sobre os tipos de retorno de protocolo -> P?


1. Tipos de resultado opacos podem ser usados ​​com PATs

Uma grande limitação atual dos protocolos é que os PATs (protocolos com tipos associados) não podem ser usados ​​como tipos reais. Embora seja uma restrição que provavelmente será levantada em uma versão futura do idioma, porque os tipos de resultados opacos são efetivamente apenas espaços reservados genéricos, eles podem ser usados ​​com PATs hoje.

Isso significa que você pode fazer coisas como:

func giveMeACollection() -> some Collection {
  return [1, 2, 3]
}

let collection = giveMeACollection()
print(collection.count) // 3

2. Tipos de resultado opacos têm identidade

Como os tipos de resultados opacos impõem que um único tipo concreto seja retornado, o compilador sabe que duas chamadas para a mesma função devem retornar dois valores do mesmo tipo.

Isso significa que você pode fazer coisas como:

//   foo() -> <Output : Equatable> Output {
func foo() -> some Equatable { 
  return 5 // The opaque result type is inferred to be Int.
}

let x = foo()
let y = foo()
print(x == y) // Legal both x and y have the return type of foo.

Isso é legal porque o compilador conhece os dois xe ytem o mesmo tipo concreto. Este é um requisito importante para ==, onde ambos os parâmetros do tipo Self.

protocol Equatable {
  static func == (lhs: Self, rhs: Self) -> Bool
}

Isso significa que ele espera dois valores que são do mesmo tipo que o tipo de conformidade concreto. Mesmo se Equatablefosse utilizável como um tipo, você não seria capaz de comparar dois Equatablevalores de conformidade arbitrários entre si, por exemplo:

func foo(_ x: Int) -> Equatable { // Assume this is legal.
  if x > 10 {
    return 0
  } else {
    return "hello world"      
  }
}

let x = foo(20)
let y = foo(5)
print(x == y) // Illegal.

Como o compilador não pode provar que dois Equatablevalores arbitrários têm o mesmo tipo concreto subjacente.

De maneira semelhante, se introduzirmos outra função de retorno de tipo opaco:

//   foo() -> <Output1 : Equatable> Output1 {
func foo() -> some Equatable { 
  return 5 // The opaque result type is inferred to be Int.
}

//   bar() -> <Output2 : Equatable> Output2 {
func bar() -> some Equatable { 
  return "" // The opaque result type is inferred to be String.
}

let x = foo()
let y = bar()
print(x == y) // Illegal, the return type of foo != return type of bar.

O exemplo se torna ilegal porque, embora ambos fooe barretornem some Equatable, seus espaços reservados genéricos "reversos" Output1e Output2podem ser satisfeitos por diferentes tipos.


3. Tipos de resultado opacos são compostos por espaços reservados genéricos

Ao contrário dos valores regulares do tipo de protocolo, os tipos de resultados opacos são bem compostos com espaços reservados genéricos regulares, por exemplo:

protocol P {
  var i: Int { get }
}
struct S : P {
  var i: Int
}

func makeP() -> some P { // Opaque result type inferred to be S.
  return S(i: .random(in: 0 ..< 10))
}

func bar<T : P>(_ x: T, _ y: T) -> T {
  return x.i < y.i ? x : y
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Legal, T is inferred to be the return type of makeP.

Isso não teria funcionado se makePtivesse acabado de retornar P, pois dois Pvalores podem ter tipos concretos subjacentes diferentes, por exemplo:

struct T : P {
  var i: Int
}

func makeP() -> P {
  if .random() { // 50:50 chance of picking each branch.
    return S(i: 0)
  } else {
    return T(i: 1)
  }
}

let p1 = makeP()
let p2 = makeP()
print(bar(p1, p2)) // Illegal.

Por que usar um tipo de resultado opaco sobre o tipo de concreto?

Nesse ponto, você pode estar pensando por que não escrever o código como:

func makeP() -> S {
  return S(i: 0)
}

Bem, o uso de um tipo de resultado opaco permite tornar o tipo Sum detalhe de implementação, expondo apenas a interface fornecida P, oferecendo flexibilidade de alterar o tipo de concreto posteriormente na linha sem quebrar nenhum código que dependa da função.

Por exemplo, você pode substituir:

func makeP() -> some P {
  return S(i: 0)
}

com:

func makeP() -> some P { 
  return T(i: 1)
}

sem quebrar nenhum código que chama makeP().

Consulte a seção Tipos opacos do guia de idiomas e a proposta de evolução Swift para obter mais informações sobre esse recurso.

Hamish
fonte
20
Independente: Como Swift de 5,1, returnnão é necessário em funções de expressão única
ielyamani
3
Mas qual é a diferença entre: func makeP() -> some Pe func makeP() -> P? Eu li a proposta e não vejo essa diferença também nas amostras deles.
Artem
2
A manipulação do tipo Swifts é uma bagunça. Essa especificidade é realmente algo que não pode ser tratado em tempo de compilação? Consulte C # para referência, ele lida com todos esses casos implicitamente através de sintaxe simples. Os desvios precisam ter uma sintaxe quase explícita e sem sentido para cultistas de carga, que estão realmente ofuscando a linguagem. Você também pode explicar a lógica do design para isso, por favor? (Se você tiver um link para a proposta no github, isso também seria bom) Editar: Acabei de notar que ele está vinculado na parte superior.
SacredGeometry
2
@Zmaster O compilador tratará dois tipos de retorno opacos como diferentes, mesmo que a implementação de ambos retorne o mesmo tipo concreto. Em outras palavras, o tipo concreto específico escolhido está oculto no chamador. (Eu pretendia expandir a segunda metade da minha resposta para tornar coisas assim um pouco mais explícitas, mas ainda não cheguei a isso).
Hamish
52

A outra resposta explica bem o aspecto técnico da nova somepalavra-chave, mas essa resposta tentará explicar facilmente o porquê .


Digamos que tenho um protocolo Animal e quero comparar se dois animais são irmãos:

protocol Animal {
    func isSibling(_ animal: Self) -> Bool
}

Dessa forma, só faz sentido comparar se dois animais são irmãos se eles são do mesmo tipo de animal.


Agora, deixe-me criar um exemplo de animal apenas para referência

class Dog: Animal {
    func isSibling(_ animal: Dog) -> Bool {
        return true // doesn't really matter implementation of this
    }
}

O caminho sem some T

Agora, digamos que eu tenho uma função que retorna um animal de uma 'família'.

func animalFromAnimalFamily() -> Animal {
    return myDog // myDog is just some random variable of type `Dog`
}

Nota: esta função não será compilada. Isso porque antes que o recurso 'some' fosse adicionado, você não pode retornar um tipo de protocolo se o protocolo usar 'Self' ou genéricos . Mas digamos que você possa ... fingir que isso eleva o myDog para o tipo abstrato Animal, vamos ver o que acontece

Agora, a questão é: se eu tentar fazer isso:

let animal1: Animal = animalFromAnimalFamily()
let animal2: Animal = animalFromAnimalFamily()

animal1.isSibling(animal2) // error

Isso gerará um erro .

Por quê? Bem, o motivo é que, quando você liga para animal1.isSibling(animal2)Swift, não sabe se os animais são cães, gatos ou o que seja. Tanto quanto Swift sabe, animal1e animal2pode ser uma espécie animal não relacionada . Como não podemos comparar animais de diferentes tipos (veja acima). Isto irá erro

Como some Tresolve esse problema

Vamos reescrever a função anterior:

func animalFromAnimalFamily() -> some Animal {
    return myDog
}
let animal1 = animalFromAnimalFamily()
let animal2 = animalFromAnimalFamily()

animal1.isSibling(animal2)

animal1e nãoanimal2 são , mas são classes que implementam Animal . Animal

O que isso permite que você faça agora é quando você liga animal1.isSibling(animal2), Swift sabe disso animal1e animal2é do mesmo tipo.

Então, da maneira que eu gosto de pensar sobre isso:

some Tpermite que o Swift saiba de que implementação Testá sendo usada, mas o usuário da classe não.

(Isenção de responsabilidade de autopromoção) Escrevi uma postagem de blog que se aprofundou um pouco mais (o mesmo exemplo aqui) sobre esse novo recurso

Downgoat
fonte
2
Portanto, sua ideia é que o chamador possa tirar proveito do fato de que duas chamadas para a função retornam o mesmo tipo, mesmo que o chamador não saiba que tipo é?
Matt
1
@matt essencialmente sim. Mesmo conceito quando usado com campos, etc. - o chamador tem a garantia de que o tipo de retorno sempre será do mesmo tipo, mas não revela exatamente qual é o tipo.
Downgoat 5/06/19
@Downgoat muito obrigado pela postagem e resposta perfeitas. Como eu entendi someno tipo de retorno, funciona como restrição ao corpo da função. Portanto, é somenecessário retornar apenas um tipo concreto em todo o corpo da função. Por exemplo: se houver return randomDog, todos os outros retornos devem funcionar apenas com Dog. Todos os benefícios advêm dessa restrição: disponibilidade animal1.isSibling(animal2)e benefício da compilação de func animalFromAnimalFamily() -> some Animal(porque agora Selfé definido sob o capô). Está correto?
Artem
5
Essa linha era tudo que eu precisava, animal1 e animal2 não são Animal, mas são classes que implementam Animal, agora tudo faz sentido!
Aross
29

A resposta de Hamish é bastante impressionante e responde à pergunta de uma perspectiva técnica. Gostaria de acrescentar algumas reflexões sobre por que a palavra some- chave é usada neste local específico nos tutoriais SwiftUI da Apple e por que é uma boa prática a seguir.

some não é um requisito!

Primeiro de tudo, você não precisa declarar o bodytipo de retorno do tipo como opaco. Você sempre pode retornar o tipo concreto em vez de usar o some View.

struct ContentView: View {
    var body: Text {
        Text("Hello World")
    }
}

Isso também será compilado. Quando você olha para a Viewinterface da, verá que o tipo de retorno bodyé um tipo associado:

public protocol View : _View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}

Isso significa que você especifica esse tipo anotando a bodypropriedade com um tipo específico de sua escolha. O único requisito é que esse tipo precise implementar o Viewpróprio protocolo.

Esse pode ser um tipo específico que implementa View, por exemplo

  • Text
  • Image
  • Circle
  • ...

ou um tipo opaco que implementa View, ou seja,

  • some View

Visualizações genéricas

O problema surge quando tentamos usar uma exibição de pilha como o bodytipo de retorno, como VStackou HStack:

struct ContentView: View {
    var body: VStack {
        VStack {
            Text("Hello World")
            Image(systemName: "video.fill")
        }
    }
}

Isso não será compilado e você receberá o erro:

A referência ao tipo genérico 'VStack' requer argumentos em <...>

Isso ocorre porque as visualizações de pilha no SwiftUI são tipos genéricos ! 💡 (E o mesmo se aplica a Listas e outros tipos de exibição de contêiner.)

Isso faz muito sentido, porque você pode conectar qualquer número de visualizações de qualquer tipo (desde que esteja em conformidade com o Viewprotocolo). O tipo de concreto VStackno corpo acima é realmente

VStack<TupleView<(Text, Image)>>

Quando mais tarde decidimos adicionar uma exibição à pilha, seu tipo concreto é alterado. Se adicionarmos um segundo texto após o primeiro, obtemos

VStack<TupleView<(Text, Text, Image)>>    

Mesmo se fizermos uma pequena alteração, algo tão sutil quanto adicionar um espaçador entre o texto e a imagem, o tipo da pilha muda:

VStack<TupleView<(Text, _ModifiedContent<Spacer, _FrameLayout>, Image)>>

Pelo que sei, essa é a razão pela qual a Apple recomenda em seus tutoriais sempre usar some View, o tipo opaco mais geral que todas as visualizações satisfazem, como o bodytipo de retorno. Você pode alterar a implementação / o layout da sua visualização personalizada sem alterar manualmente o tipo de retorno toda vez.


Suplemento:

Se você deseja obter uma compreensão mais intuitiva dos tipos de resultados opacos, publiquei recentemente um artigo que vale a pena ler:

🔗 O que é isso "alguns" no SwiftUI?

Mischa
fonte
2
Este. Obrigado! A resposta de Hamish foi muito completa, mas a sua me diz exatamente por que é usada nesses exemplos.
Chris Marshall
Eu amo a idéia de "alguns". Alguma idéia se o uso de "some" afeta o tempo de compilação?
Tofu Warrior
@ Mischa Então, como fazer visualizações genéricas? com um protocolo que contém exibições e outros comportamentos?
theMouk 30/01
27

Acho que todas as respostas até agora estão faltando e que somesão úteis principalmente em algo como uma DSL (linguagem específica de domínio), como SwiftUI ou uma biblioteca / estrutura, que terá usuários (outros programadores) diferentes de você.

Você provavelmente nunca usaria someem seu código de aplicativo normal, exceto, talvez, na medida em que ele pode quebrar um protocolo genérico para que possa ser usado como um tipo (em vez de apenas como uma restrição de tipo). O que somefaz é permitir que o compilador mantenha um conhecimento do tipo específico de algo, enquanto coloca uma fachada de supertipo na frente dele.

Assim, no SwiftUI, onde você é o usuário, tudo o que você precisa saber é que algo é um some View, enquanto nos bastidores todo tipo de bobagem pode acontecer a partir do qual você está protegido. Na verdade, esse objeto é um tipo muito específico, mas você nunca precisará ouvir sobre o que é. No entanto, diferentemente de um protocolo, é um tipo de pleno direito, porque, onde quer que apareça, é apenas uma fachada para algum tipo específico de pleno direito.

Em uma versão futura do SwiftUI, na qual você espera um some View, os desenvolvedores podem alterar o tipo subjacente desse objeto em particular. Mas isso não quebrará seu código, porque ele nunca mencionou o tipo subjacente em primeiro lugar.

Portanto, some com efeito, torna um protocolo mais parecido com uma superclasse. É quase um tipo de objeto real, embora não exatamente (por exemplo, a declaração de método de um protocolo não pode retornar a some).

Então, se você ia usar some para qualquer coisa, ele provavelmente seria se você estivesse escrevendo um DSL ou framework / biblioteca para uso por outros, e você queria para mascarar detalhes tipo subjacente. Isso tornaria seu código mais simples para o uso de outras pessoas e permitiria que você alterasse os detalhes da implementação sem quebrar o código.

No entanto, você também pode usá-lo em seu próprio código como uma maneira de proteger uma região do seu código dos detalhes da implementação ocultos em outra região do seu código.

mate
fonte
23

A somepalavra-chave do Swift 5.1 ( proposta de evolução rápida ) é usada em conjunto com um protocolo como um tipo de retorno.

As notas de versão do Xcode 11 apresentam assim:

Agora, as funções podem ocultar seu tipo de retorno concreto, declarando com quais protocolos estão em conformidade, em vez de especificar o tipo exato de retorno:

func makeACollection() -> some Collection {
    return [1, 2, 3]
}

O código que chama a função pode usar a interface do protocolo, mas não tem visibilidade do tipo subjacente. ( SE-0244 , 40538331)

No exemplo acima, você não precisa dizer que retornará um Array. Isso permite que você retorne um tipo genérico que apenas esteja em conformidade Collection.


Observe também este possível erro que você pode enfrentar:

tipos de retorno 'some' estão disponíveis apenas no iOS 13.0.0 ou mais recente

Isso significa que você deve usar a disponibilidade para evitar someno iOS 12 e antes:

@available(iOS 13.0, *)
func makeACollection() -> some Collection {
    ...
}
Cœur
fonte
1
Muito obrigado por esta resposta focada e o problema do compilador no Xcode 11 beta
brainray
1
Você deve usar a disponibilidade para evitar someno iOS 12 e anteriores. Contanto que você faça, você deve ficar bem. O problema é apenas que o compilador não avisa para fazer isso.
Matt
2
Como você indica, a descrição concisa da Apple explica tudo: Agora, as funções podem ocultar seu tipo de retorno concreto, declarando quais protocolos estão em conformidade, em vez de especificar o tipo exato de retorno. E o código que chama a função pode usar a interface do protocolo. Limpo e depois alguns.
Fattie
Isso (ocultar o tipo de retorno concreto) já é possível sem o uso da palavra-chave "some". Não explica o efeito de adicionar "alguns" à assinatura do método.
Vince O'Sullivan
@ VinceO'Sullivan Não é possível remover a somepalavra - chave neste exemplo de código no Swift 5.0 ou no Swift 4.2. O erro será: "O protocolo 'Coleção' só pode ser usado como uma restrição genérica porque possui requisitos de tipo Próprio ou associado "
Cœur
2

'some' significa tipo opaco. No SwiftUI, o View é declarado como um protocolo

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    associatedtype Body : View

    /// Declares the content and behavior of this view.
    var body: Self.Body { get }
}

Ao criar sua visualização como Struct, você está em conformidade com o protocolo View e informa que o corpo var retornará algo que estará confirmando o View Protocol. É como uma abstração genérica de protocolo, na qual você não precisa definir o tipo concreto.

varunrathi28
fonte
2

Vou tentar responder com um exemplo prático muito básico (sobre o que é esse tipo de resultado opaco )

Supondo que você tenha um protocolo com o tipo associado e duas estruturas implementando-o:

protocol ProtocolWithAssociatedType {
    associatedtype SomeType
}

struct First: ProtocolWithAssociatedType {
    typealias SomeType = Int
}

struct Second: ProtocolWithAssociatedType {
    typealias SomeType = String
}

Antes do Swift 5.1, abaixo é ilegal devido a ProtocolWithAssociatedType can only be used as a generic constrainterro:

func create() -> ProtocolWithAssociatedType {
    return First()
}

Mas no Swift 5.1, isso é bom ( someadicionado):

func create() -> some ProtocolWithAssociatedType {
    return First()
}

Acima está o uso prático, amplamente utilizado no SwiftUI para some View .

Mas há uma limitação importante: o tipo de retorno precisa ser conhecido no momento da compilação; portanto, abaixo novamente, não funcionará dando Function declares an opaque return type, but the return statements in its body do not have matching underlying typeserro:

func create() -> some ProtocolWithAssociatedType {
    if (1...2).randomElement() == 1 {
        return First()
    } else {
        return Second()
    }
}
tzaloga
fonte
0

Um caso de uso simples que vem à mente é escrever funções genéricas para tipos numéricos.

/// Adds one to any decimal type
func addOne<Value: FloatingPoint>(_ x: Value) -> some FloatingPoint {
    x + 1
}

// Variables will be assigned 'some FloatingPoint' type
let double = addOne(Double.pi) // 4.141592653589793
let float = addOne(Float.pi) // 4.141593

// Still get all of the required attributes/functions by the FloatingPoint protocol
double.squareRoot() // 2.035090330572526
float.squareRoot() // 2.03509

// Be careful, however, not to combine 2 'some FloatingPoint' variables
double + double // OK 
//double + float // error
Artem Ilyumzhinov
fonte