Home > database >  Understanding CaseIterable in swift
Understanding CaseIterable in swift

Time:12-24

I've read the documentation, and seen perhaps only a part of what the protocol is. I just am not following the logic. Can someone help me understand this?

What I see in xcode when I examine the protocol

/// Conforming to the CaseIterable Protocol
/// =======================================
///
/// The compiler can automatically provide an implementation of the
/// `CaseIterable` requirements for any enumeration without associated values
/// or `@available` attributes on its cases. The synthesized `allCases`
/// collection provides the cases in order of their declaration.
///
/// You can take advantage of this compiler support when defining your own
/// custom enumeration by declaring conformance to `CaseIterable` in the
/// enumeration's original declaration. The `CompassDirection` example above
/// demonstrates this automatic implementation.
public protocol CaseIterable {

    /// A type that can represent a collection of all values of this type.
    associatedtype AllCases : Collection = [Self] where Self == Self.AllCases.Element

    /// A collection of all values of this type.
    static var allCases: Self.AllCases { get }
}

I'm struggling to follow what is happening here and why. Can someone walk me through the logic of this please?

One of the other big struggles I'm having because of this is if I conform a protocol to be CaseIterable.

protocol Foo: CaseIterable {}

I can't use it as a variable anymore.

struct Bar {
    var foo: Foo
}

I get this error Protocol 'Foo' can only be used as a generic constraint because it has Self or associated type requirements.

It does have Self requirements but I can't figure out how to get around this problem. If someone could help me understand why this happens and how to fix it too, I'd be very grateful.

CodePudding user response:

CaseIterable exists to allow you to programmatically walk through all the possible cases of an enum, allowing you use the enum type as a Collection of its cases:

enum CardinalDirection { case north, south, east, west }

for direction in CardinalDirection.allCases {
    // Do something with direction which is one of north, south, east, west
    print("\(direction)")
}

This prints

north
south
east
west

There is nothing that prevents you from making another kinds of types conform to CaseIterable; however, the compiler will only synthesize conformance for enum types. It's not useful for most other kinds of types; however, I have occasionally found it useful for types that conform to OptionSet. In that case you have to manually implement conformance.

struct AssetFlags: OptionSet, CaseIterable
{
    typedef RawValue = UInt8
    typedef AllCases = [AssetFlags]

    let rawValue: RawValue

    static let shouldPreload = AssetFlags(rawValue: 0x01)
    static let isPurgeable   = AssetFlags(rawValue: 0x02)
    static let isLocked      = AssetFlags(rawValue: 0x04)
    static let isCached      = AssetFlags(rawValue: 0x08)

    static var allCases: AllCases = [shouldPreload, isPurgeable, isLocked, isCached]
}

Note that OptionSet is conceptually similar to an enum. They both define a small set of distinct values they can have. With one they are mutually exclusive, while for the other they may be combined. But the key thing for CaseIterable to be useful is the finite nature of the set of possible distinct values. If your type has that characteristic, conforming to CaseIterable could be useful, otherwise, it wouldn't make sense. Int or String, for example, are not good candidates or CaseIterable.

In my own code, I don't bother conforming to CaseIterable, even for enum types, until a specific need arises that requires it. Actually I take that approach to all protocol conformance. It's a specific case of the more general YAGNI rule of thumb: "You ain't gonna need it."

Regarding your Bar class, the problem is not specifically related to CaseIterable, but rather to using a protocol with Self or associated type requirements, which for Foo happens to be inherited from CaseIterable.

Swift 5.7 relaxed the rules concerning Self and associated type requirements a bit, allowing you to use the any keyword to tell the compiler you want to use an existential Foo instead of a concrete Foo to write

struct Bar {
   var foo: any Foo
}

If you want a concrete Foo you could use some. The original way to do it though, which still works, is to make Bar explicitly generic

struct Bar<T: Foo> {
   var foo: T
}
  • Related