Skip to main content

How to use Non-Exhaustive Enums in Swift

Tags

How to use Non-Exhaustive Enums in Swift

In mid-December of 2017 a proposal went into review for the Swift language entitled, “SE-0192 – Non-Exhaustive Enums.”  In this proposal the Swift core team outlined a change to allow switch statements that consume enum types to be non-exhaustive or exhaustive, depending upon their needs.   The motivation behind this change was to allow for the easier additions to C based Enums in Apple or third party libraries without creating source breaking changes in projects downstream.  A few library example that were brought up to support this proposal were UIKit and Foundation as they both received case additions this year with the release of iOS 11. The overall goal is to provide a way for library authors to add new cases in their Enums while applications developers that consume these enum can choose whether they want their switch statements to be exhaustive or not.

The Non-Exhaustive Enum proposal kicked off an interesting debate amongst the Swift community on whether this was the right approach or whether this feature added significant value.  Personally, I feel that the ending compromise of this proposal provided flexibility in the usage to make me in favor of the cost of implementation.

 

How to Use Non-Exhaustive Enums 👨‍💻

Before this proposal you had a choice to use an exhaustive or non-exhaustive switch statement.  The exhaustive case being the statement that switched through all values, and the non-exhaustive statement containing the default case as the catch-all for all cases not implemented.

enum Example: Int {
    case one = 1
    case two = 2
    case three = 3
}
 
// Exhaustive
switch number {
    case .one:
        print("One")
    case .two:
        print("Two")
    case .three:
        print("Three")
}
 
// Non-Exhaustive
switch number {
    case .one:
        print("One")
    default:
        print("Catch All")
}

Once non-exhaustive enums are added to the language, you will have the option to add @unknown cases or @unknown defaults to your switch statements.  The new @unkown keyword will now emit a warning letting you know that your switch statement must be exhaustive. This alerts you to an addition in the enum that your switch is not consuming.  This helps in two ways. First, it does not produce an error for exhaustive switches that include the @unknown case. Second, it saves your switch from blindly executing the default case, and hopefully allows you, the developer, to capture bugs quicker by not having to debug the default case.

To illustrate non-exhaustive enums, I created two switch statements that enumerate over an enum for processing server response codes.  In the bottom switch an error will be thrown because the developer needs to add a case for .ServiceUnavailable, otherwise the a default could be added.  In the top switch, a warning is issued instead telling the developer that the switch must be exhaustive. The top switch does not break binary compatibility and also identifies that there is a missing case without a default.

// This ENUM is in Swift currently, but the proposal states that this is to be used with C ENUMs.
enum ServerResponseCodes: Int {
 
    case OK = 200
    case Created = 201
    case Accepted = 202
    case NoContent = 204
    //case ResetContent = 205
 
    case MovedPermanently = 301
    case Found = 302
    case SeeOther = 303
    //case NotModified = 304
    //case UseProxy = 305
 
    case BadRequest = 400
    case Unauthorized = 401
    //case PaymentRequired = 402
    //case Forbidden = 403
    //case NotFound = 404
    //case MethodNotAllowed = 405
    //case NotAcceptable = 406
    //case RequestTimeout = 408
 
    case InternalServerError = 500
    case NotImplemented = 501
    case BadGateway = 502
    case ServiceUnavaiable = 503
    //case GatewayTimeout = 504
    //case HTTPVersionNotSupported = 505
}
 
// -------------------------------------------- //
 
// It is now a source-compatible change to add a case to a non-frozen enum (whether imported from C or defined in the standard library).
// It is not a source-compatible change to add a case to a frozen enum.
// It is still not a source-compatible change to remove a case from a public enum (frozen or non-frozen).
// It is a source-compatible change to change a non-frozen enum into a frozen enum, but not vice versa.
func getResponseStatus(responseCode: ServerResponseCodes) {
    // Swift 4.2 - 5.0 Non-Exaustive Enum Switch Statement
    #if swift(>=4.2)
        switch responseCode {
        case .OK:
            print("OK")
        case .Created:
            print("Created")
        case .Accepted:
            print("Accepted")
        case .NoContent:
            print("No Content")
        case .MovedPermanently:
            print("Moved Permanently")
        case .Found:
            print("Found")
        case .SeeOther:
            print("See Other")
        case .BadRequest:
            print("Bad Request")
        case .Unauthorized:
            print("Unauthorized")
        case .InternalServerError:
            print("Internal Server Error")
        case .NotImplemented:
            print("Not Implemented")
        case .BadGateway:
            print("Bad Gateway")
 
        // Emits a warning: Switch must be exhaustive
        @unknown default:
 
        // Does not emit a warning at all when a new case is added
        //default:
            // If any of these were added in our library.
            //case PaymentRequired = 402
            //case Forbidden = 403
            //case NotFound = 404
            //case MethodNotAllowed = 405
            print("Catch all for cases that could have been addd.")
            print("Emits a warning if new cases are added to the enum.")
        }
    // Swift 4.1 Enum Switch Statement
    // The Switch below results in an error
    #elseif swift(>=4.1)
        switch responseCode {
        case .OK:
            print("OK")
        case .Created:
            print("Created")
        case .Accepted:
            print("Accepted")
        case .NoContent:
            print("No Content")
        case .MovedPermanently:
            print("Moved Permanently")
        case .Found:
            print("Found")
        case .SeeOther:
            print("See Other")
        case .BadRequest:
            print("Bad Request")
        case .Unauthorized:
            print("Unauthorized")
        case .InternalServerError:
            print("Internal Server Error")
        case .NotImplemented:
            print("Not Implemented")
//     If all cases are switched through in an exhaustive manner, a default case is not needed.
//     Adding a new case to the enum results in: error: switch must be exhaustive
//        default:
//            print("Default Fall Through")
        }
    #endif
}

 

In Summary ⌛️

Personally, I feel non-exhaustive enums will provide flexibility for library authors and projects that consume these libraries.  Not sure how much that affects main stream Swift development, but this could be useful for application consuming the above listed changes for UIKit and Foundation.  In the end, this proposal will be introduced into the Swift language in Swift 5 as a compiler diagnostic that omits a warning if @unknown default: or @unknown case _: are not used in your non-exhaustive switch statement.  This will provide developers the ability to consume and make the appropriate changes to their applications.

I added a network example of the code used in this post on my Github here.  This should provide a bit more context that the Example enum. Thank you for reading and if you have any questions, comments, or concerns, please leave a comment and I will try and get back to your as soon as possible.

Credits: Cover image designed by Freepik.

Member for

3 years 9 months
Matt Eaton

Long time mobile team lead with a love for network engineering, security, IoT, oss, writing, wireless, and mobile.  Avid runner and determined health nut living in the greater Chicagoland area.