What is the `some` keyword in Swift(UI)?

后端 未结 11 484
庸人自扰
庸人自扰 2020-12-04 04:54

The new SwiftUI tutorial has the following code:

struct ContentView: View {
    var body: some View {
        Text(         


        
相关标签:
11条回答
  • 2020-12-04 05:37

    A simple use case that springs to mind is writing generic functions for numeric types.

    /// 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
    
    0 讨论(0)
  • 2020-12-04 05:38

    I'll try to answer this with very basic practical example (what is this an opaque result type about)

    Assuming you have protocol with associated type, and two structs implementing it:

    protocol ProtocolWithAssociatedType {
        associatedtype SomeType
    }
    
    struct First: ProtocolWithAssociatedType {
        typealias SomeType = Int
    }
    
    struct Second: ProtocolWithAssociatedType {
        typealias SomeType = String
    }
    

    Before Swift 5.1, below is illegal because of ProtocolWithAssociatedType can only be used as a generic constraint error:

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

    But in Swift 5.1 this is fine (some added):

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

    Above is practical usage, extensively used in SwiftUI for some View.

    But there is one important limitation - returning type needs to be know at compile time, so below again won't work giving Function declares an opaque return type, but the return statements in its body do not have matching underlying types error:

    func create() -> some ProtocolWithAssociatedType {
        if (1...2).randomElement() == 1 {
            return First()
        } else {
            return Second()
        }
    }
    
    0 讨论(0)
  • 2020-12-04 05:41

    Hamish's answer is pretty awesome and answers the question from a technical perspective. I would like to add some thoughts on why the keyword some is used in this particular place in Apple's SwiftUI tutorials and why it's a good practice to follow.

    some is Not a Requirement!

    First of all, you don't need to declare the body's return type as an opaque type. You can always return the concrete type instead of using the some View.

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

    This will compile as well. When you look into the View's interface, you'll see that the return type of body is an associated type:

    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 }
    }
    

    This means that you specify this type by annotating the body property with a particular type of your choice. The only requirement is that this type needs to implement the View protocol itself.

    That can either be a specific type that implements View, for example

    • Text
    • Image
    • Circle

    or an opaque type that implements View, i.e.

    • some View

    Generic Views

    The problem arises when we try to use a stack view as the body's return type, like VStack or HStack:

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

    This won't compile and you'll get the error:

    Reference to generic type 'VStack' requires arguments in <...>

    That's because stack views in SwiftUI are generic types!

    0 讨论(0)
  • 2020-12-04 05:42

    some View is an opaque result type as introduced by SE-0244 and is available in Swift 5.1 with Xcode 11. You can think of this as being a "reverse" generic placeholder.

    Unlike a regular generic placeholder which is satisfied by the caller:

    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.
    

    An opaque result type is an implicit generic placeholder satisfied by the implementation, so you can think of this:

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

    as looking like this:

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

    In fact, the eventual goal with this feature is to allow reverse generics in this more explicit form, which would also let you add constraints, e.g -> <T : Collection> T where T.Element == Int. See this post for more info.

    The main thing to take away from this is that a function returning some P is one that returns a value of a specific single concrete type that conforms to P. Attempting to return different conforming types within the function yields a compiler error:

    // 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()
      }
    }
    

    As the implicit generic placeholder cannot be satisfied by multiple types.

    This is in contrast to a function returning P, which can be used to represent both S1 and S2 because it represents an arbitrary P conforming value:

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

    Okay, so what benefits do opaque result types -> some P have over protocol return types -> P?


    1. Opaque result types can be used with PATs

    A major current limitation of protocols is that PATs (protocols with associated types) cannot be used as actual types. Although this is a restriction that will likely be lifted in a future version of the language, because opaque result types are effectively just generic placeholders, they can be used with PATs today.

    This means you can do things like:

    func giveMeACollection() -> some Collection {
      return [1, 2, 3]
    }
    
    let collection = giveMeACollection()
    print(collection.count) // 3
    

    2. Opaque result types have identity

    Because opaque result types enforce a single concrete type is returned, the compiler knows that two calls to the same function must return two values of the same type.

    This means you can do things like:

    //   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.
    

    This is legal because the compiler knows that both x and y have the same concrete type. This is an important requirement for ==, where both parameters of type Self.

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

    This means that it expects two values that are both the same type as the concrete conforming type. Even if Equatable were usable as a type, you wouldn't be able to compare two arbitrary Equatable conforming values with each other, for example:

    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.
    

    As the compiler cannot prove that two arbitrary Equatable values have the same underlying concrete type.

    In a similar manner, if we introduced another opaque type returning function:

    //   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.
    

    The example becomes illegal because although both foo and bar return some Equatable, their "reverse" generic placeholders Output1 and Output2 could be satisfied by different types.


    3. Opaque result types compose with generic placeholders

    Unlike regular protocol-typed values, opaque result types compose well with regular generic placeholders, for example:

    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.
    

    This wouldn't have worked if makeP had just returned P, as two P values may have different underlying concrete types, for example:

    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.
    

    Why use an opaque result type over the concrete type?

    At this point you may be thinking to yourself, why not just write the code as:

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

    Well, the use of an opaque result type allows you to make the type S an implementation detail by exposing only the interface provided by P, giving you flexibility of changing the concrete type later down the line without breaking any code that depends on the function.

    For example, you could replace:

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

    with:

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

    without breaking any code that calls makeP().

    See the Opaque Types section of the language guide and the Swift evolution proposal for further information on this feature.

    0 讨论(0)
  • 2020-12-04 05:44

    I think what all the answers so far are missing is that some is useful primarily in something like a DSL (domain-specific language) such as SwiftUI or a library/framework, which will have users (other programmers) different from yourself.

    You would probably never use some in your normal app code, except perhaps insofar as it can wrap a generic protocol so that it can be used as a type (instead of just as a type constraint). What some does is to let the compiler keep a knowledge of what specific type something is, while putting a supertype facade in front of it.

    Thus in SwiftUI, where you are the user, all you need to know is that something is a some View, while behind the scenes all sort of hanky-panky can go on from which you are shielded. This object is in fact a very specific type, but you'll never need to hear about what it is. Yet, unlike a protocol, it is a full-fledged type, because wherever it appears it is merely a facade for some specific full-fledged type.

    In a future version of SwiftUI, where you are expecting a some View, the developers could change the underlying type of that particular object. But that won't break your code, because your code never mentioned the underlying type in the first place.

    Thus, some in effect makes a protocol more like a superclass. It is almost a real object type, though not quite (for example, a protocol's method declaration cannot return a some).

    So if you were going to use some for anything, it would most likely be if you were writing a DSL or framework/library for use by others, and you wanted to mask underlying type details. This would make your code simpler for others to use, and would allow you to change the implementation details without breaking their code.

    However, you might also use it in your own code as a way of shielding one region of your code from the implementation details buried in another region of your code.

    0 讨论(0)
  • 2020-12-04 05:45

    The some keyword from Swift 5.1 (swift-evolution proposal) is used in conjunction with a Protocol as a return type.

    Xcode 11 release notes present it like that:

    Functions can now hide their concrete return type by declaring what protocols it conforms to, instead of specifying the exact return type:

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

    Code that calls the function can use the interface of the protocol, but doesn’t have visibility into the underlying type. (SE-0244, 40538331)

    In the example above, you don't need to tell that you're going to return an Array. That allows you to even return a generic type that just conforms to Collection.


    Note also this possible error that you may face:

    'some' return types are only available in iOS 13.0.0 or newer

    It means that you're supposed to use availability to avoid some on iOS 12 and before:

    @available(iOS 13.0, *)
    func makeACollection() -> some Collection {
        ...
    }
    
    0 讨论(0)
提交回复
热议问题