This is the sixth day of my participation in the August More text Challenge. For details, see:August is more challenging

Hi 👋

  • Wechat: RyukieW
  • 📦 Technical article archive
  • 🐙 making
My personal project Mine Elic endless sky ladder Dream of books
type The game financial
AppStore Elic Umemi

First, I encountered problems

When writing a SwiftUI demo today, I’m going to use an enumeration as a data source for lists.

1.1 Body of the list

struct ContentView: View {
    var body: some View {
        NavigationView {
            Form {
                ForEach(ContentTypes.allCases, content: { item in
                    NavigationLink(destination: item.destinationView) {
                        Text(item.rawValue)
                    }
                })
            }
            .navigationBarTitle("SwiftUITourDemos", displayMode: .automatic)
        }
    }
}
Copy the code

1.2 the enumeration

enum ContentTypes: String.CaseIterable.Identifiable {
    var id: String { rawValue }
    
    case StateAndStructClass = "@State & struct/class"

    var destinationView: View {
        switch self {
        case .StateAndStructClass:
            return StateAndStructClassView()}}}Copy the code

1.3 the problem

The code looks fine at first glance, however:

Protocol ‘View’ can only be used as a generic constraint because it has Self or associated type requirements

A protocol classic error when using a protocol.

1.4 What is View

By clicking on the definition of a View, you can see that it’s a protocol.

Second, solve the problem

SwiftUI produces this code by default

Since the body (defined in the View protocol) uses some View as the type, can I also use some View in the destinationView of ContentTypes?

2.1 give it a try

Hey, there you go

So what exactly is’ some ‘? It represents an opaque type and is usually used in conjunction with protocols. It’s not a feature of SwiftUI, it’s a feature of Swift. It’s just that I haven’t seen it before 😂.

Three, opaque type

  • reference
    • LanguageGuide-OpaqueTypes
    • Opaque type

A function or method with an opaque return type hides the type information of the return value. Instead of providing a specific type as a return type, the function describes the return value in terms of the protocol it supports. Hiding the type information is useful when dealing with the relationship between the module and the calling code, because the underlying data type returned can still remain private. And unlike return protocol types, opaque types guarantee type consistency: the compiler gets the type information, but the module consumer does not.

3.1 Problems to Be Solved

As an example, suppose you are writing a module that draws geometric shapes made of ASCII symbols. Its basic feature is that it has a draw() method that returns a string representing the final geometry, which you can describe using the Shape protocol that includes this method:

protocol Shape {
    func draw(a) -> String
}

struct Triangle: Shape {
    var size: Int
    func draw(a) -> String {
        var result: [String] = []
        for length in 1.size {
            result.append(String(repeating: "*", count: length))
        }
        return result.joined(separator: "\n")}}let smallTriangle = Triangle(size: 3)
print(smallTriangle.draw())
Copy the code

You can use generics to do things like flip vertically, as shown below. However, there is one big limitation to this approach: the result of the flip operation exposes the generic type we used to construct the result:

struct FlippedShape<T: Shape> :Shape {
    var shape: T
    func draw(a) -> String {
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")}}let flippedTriangle = FlippedShape(shape: smallTriangle)
print(flippedTriangle.draw())
Copy the code

JoinedShape

= JoinedShape

If you concatenate a flipped Triangle with a normal Triangle, it will get something like JoinedShape

, Triangle>.

struct JoinedShape<T: Shape.U: Shape> :Shape {
    var top: T
    var bottom: U
    func draw(a) -> String {
        return top.draw() + "\n" + bottom.draw()
    }
}
let joinedTriangles = JoinedShape(top: smallTriangle, bottom: flippedTriangle)
print(joinedTriangles.draw())
Copy the code

Exposing the specific types used in the construct can leak type information because the partially exposed interface of an ASCII geometry module must declare full return types, which should not actually be declared. Output of the same geometry, the module may have a variety of internal implementation, and external use, should be independent of the internal implementation logic of various transformation sequence. Wrapped types such as JoinedShape and FlippedShape are not of concern to module users and should not be visible. The exposed interface of a module should consist of basic operations such as concatenation, rollover, and so on, which should also return independent values of type Shape.

3.2 Return the opaque type

You can think of opaque types as the opposite of generic types. Generics allow a method to be called with an implementation-independent type for its parameters and return values. For example, the return type of the following function is determined by the caller:

func max<T> (_ x: T._ y: T) -> T where T: Comparable { . }
Copy the code

The values of x and y are determined by the code calling Max (_:_:), and their types determine the specific type of T. The calling code can use any type that follows Comparable protocols, and the code inside the function has to be written in a generic way to handle the various types passed in by the caller. An implementation of Max (_:_:) uses only features common to all types that follow the Comparable protocol.

In functions that return opaque types, these roles are reversed. Opaque types allow a function implementation to select a return type independent of the calling code. For example, the following example returns a trapezoid without directly exporting the underlying type of the trapezoid:

struct Square: Shape {
    var size: Int
    func draw(a) -> String {
        let line = String(repeating: "*", count: size)
        let result = Array<String>(repeating: line, count: size)
        return result.joined(separator: "\n")}}func makeTrapezoid(a) -> some Shape {
    let top = Triangle(size: 2)
    let middle = Square(size: 2)
    let bottom = FlippedShape(shape: top)
    let trapezoid = JoinedShape(
        top: top,
        bottom: JoinedShape(top: middle, bottom: bottom)
    )
    return trapezoid
}
let trapezoid = makeTrapezoid()
print(trapezoid.draw())
Copy the code

In this example, the makeTrapezoid() function defines the return value type to be some Shape; Therefore, this function returns a given type that follows the Shape protocol without specifying any specific type. Writing the makeTrapezoid() function thus indicates the basic nature of its public interface: it returns a geometry, not a special type generated by the partial public interface. The above implementation uses two triangles and a square, and there are many other ways to rewrite the trapezoid function without changing the return type.

This example highlights the opposite of opaque return types and generics. The code in makeTrapezoid() can return any type it wants, as long as the type follows the Shape protocol, just as any required type can be used when calling a generic function. The calling code for this function needs to be generic, just like the implementation code for generic functions, so that any Shape value returned by makeTrapezoid() can be used properly.

You can also combine opaque return types with generics. The following two generic functions both return opaque types that follow the Shape protocol.

func flip<T: Shape> (_ shape: T) -> some Shape {
    return FlippedShape(shape: shape)
}
func join<T: Shape.U: Shape> (_ top: T._ bottom: U) -> some Shape {
    JoinedShape(top: top, bottom: bottom)
}

let opaqueJoinedTriangles = join(smallTriangle, flip(smallTriangle))
print(opaqueJoinedTriangles.draw())
Copy the code

In this case, the value of opaqueJoinedTriangles is exactly the same as in the previous example of generics. Unlike the previous ones, however, flip(-:) and join(-:-:) wrap the return of an operation on a generic parameter as an opaque type, which ensures that the generic parameter type is not visible in the result. Both functions are generic because they rely on generic parameters that pass the type information needed for FlippedShape and JoinedShape.

If an opaque type is returned in more than one place in a function, all possible return values must be of the same type. Even for generic functions, opaque return types can use generic parameters, but the return type needs to be unique. For example, the following is an illegal example that contains a rollovers function with special treatment for the Square type.

func invalidFlip<T: Shape> (_ shape: T) -> some Shape {
    if shape is Square {
        return shape // Error: inconsistent return type
    }
    return FlippedShape(shape: shape) // Error: inconsistent return type
}
Copy the code

If you call this function with type Square, it returns type Square; Otherwise, it returns a FlippedShape type. This violates the unique requirement for return value types, so invalidFlip(_:) is incorrect. One way to fix invalidFlip(_:) is to move the special processing for Square into the FlippedShape implementation so that this function always returns FlippedShape:

struct FlippedShape<T: Shape> :Shape {
    var shape: T
    func draw(a) -> String {
        if shape is Square {
            return shape.draw()
        }
        let lines = shape.draw().split(separator: "\n")
        return lines.reversed().joined(separator: "\n")}}Copy the code

The requirement that the return type is always unique does not affect the use of generics in the opaque type returned. For example, the following function returns a generic parameter in the underlying type:

func `repeat`<T: Shape> (shape: T.count: Int) -> some Collection {
    return Array<T>(repeating: shape, count: count)
}
Copy the code

In this case, the underlying type returned varies depending on the T: but whatever shape is passed in, repeat(shape:count:) creates and returns an array of elements with the corresponding shape. However, the return value is always the same underlying type [T], so this meets the requirement that opaque return types are always unique.

3.3 Differences between opaque types and protocol types

Although using an opaque type as a function return value looks very similar to returning a protocol type, one major difference between the two is the need for type consistency. An opaque type can only correspond to one specific type, even if the function caller doesn’t know which type it is. A protocol type can correspond to multiple types at the same time, as long as they all follow the same protocol. In general, protocol types are more flexible; underlying types can store a greater variety of values, and opaque types are more restrictive to these underlying types.

For example, here is the version of the flip(_:) method that returns the protocol type instead of the opaque type:

func protoFlip<T: Shape> (_ shape: T) -> Shape {
    return FlippedShape(shape: shape)
}
Copy the code

This version of protoFlip(_:) has the same function body as flip(_:), and it always returns a unique type. But unlike flip(_:), protoFlip(_:) doesn’t need to always return a unique type — it just needs to follow the Shape protocol. In other words, protoFlip(_:) has a much looser constraint on API callers than flip(_:). It retains the flexibility to return a number of different types:

func protoFlip<T: Shape> (_ shape: T) -> Shape {
    if shape is Square {
        return shape
    }

    return FlippedShape(shape: shape)
}
Copy the code

The modified code may return an instance of Square or a FlippedShape, depending on the parameter representing the shape, so the same function may return two completely different types. When multiple instances of the same shape are flipped, other valid versions of this function may return completely different types of results. The uncertainty of the return type of protoFlip(_:) means that many operations that depend on the return type information cannot be performed. For example, the result returned by this function cannot be compared using the == operator.

let protoFlippedTriangle = protoFlip(smallTriangle)
let sameThing = protoFlip(smallTriangle)
protoFlippedTriangle = = sameThing  / / error
Copy the code

In the example above, the error in the last line comes from several reasons. The most immediate problem is that the Shape protocol does not include a declaration for the == operator. If you try to add this declaration, you’ll run into a new problem: the == operator needs to know the types of the left and right arguments. These operators typically use the type Self as an argument to match specific types that match the protocol, but since type erasements occur when a protocol is used as a type, there is no implementation requirement for Self.

It is more flexible to use the protocol type as the return type of a function, as long as the function returns the type that follows the protocol. However, being more flexible results in sacrificing the ability to perform some operations on the return value. The example above illustrates why you can’t use the == operator — it depends on specific type information, which is not provided by using the protocol type.

Another problem with this approach is that the operations that transform shapes cannot be nested. The result of flipping a triangle is a value of type Shape, whereas the protoFlip(_:) method takes a parameter that follows the Shape protocol type, whereas the value of the protocol type does not follow this protocol; The return value of protoFlip(_:) does not follow Shape either. This means that multiple transform operations such as protoFlip(protoFlip(smallTriange)) are illegal, because the resulting type after the flip operation cannot be used as a parameter for protoFlip(_:).

Opaque types, by contrast, retain the uniqueness of the underlying type. Swift’s ability to infer the association type makes opaque types more likely to be used as function return values than protocol types. Such as:

protocol Container {
    associatedtype Item
    var count: Int { get }
    subscript(i: Int) -> Item { get}}extension Array: Container {}Copy the code

You cannot use Container as the return type of a method because this protocol has an association type. You can’t use it as a constraint on a generic return type either, because there isn’t enough information exposed outside the function body to infer a generic type.

// Error: protocol with associated type cannot be used as return type.
func makeProtocolContainer<T> (item: T) -> Container {
    return [item]
}

// Error: not enough information to infer the type of C.
func makeProtocolContainer<T.C: Container> (item: T) -> C {
    return [item]
}
Copy the code

Using the opaque type some Container as the return type makes it clear what API contract is required — the function returns a collection type, but does not specify its specific type:

func makeOpaqueContainer<T> (item: T) -> some Container {
    return [item]
}
let opaqueContainer = makeOpaqueContainer(item: 12)
let twelve = opaqueContainer[0]
print(type(of: twelve))
/ / output "Int"
Copy the code

Twelve’s type can be inferred to be Int, which shows that type inference applies to opaque types. In the implementation of makeOpaqueContainer(item:), the underlying type is an opaque collection [T]. In this case, T is Int, so the return value is an array of integers, and the associative type Item is inferred to be Int. The Subscipt method in the Container protocol returns Item, which means that the type of twelve can also be inferred to be Int.

Four,

After understanding the opaque type, it occurred to me that some of the previous protocol usage problems could be solved more gracefully. After I optimize, I will share with you 😁.

reference

  • Swift.org-OpaqueTypes
  • SwiftGG- Opaque type
  • StackOverFlow-What is the some keyword in Swift(UI)?