Sunday, November 29, 2009

Groovy: Switching on Enumerations

Recently, in Groovy, I had a need to switch on a variable that contained an enumerated value and was wondering what I should use in my case clauses. For example given the following:

enum OS { LINUX, MAC, WINDOWS }
Should (or could) I switch on OS.LINUX or "LINUX", or even "${OS.LINUX}"? I wrote a little script to help me decide:

enum OS { LINUX, MAC, WINDOWS }

[OS.LINUX, "LINUX", OS.MAC, "MAC", OS.WINDOWS, "WINDOWS"].each { os ->
switch(os) {
case OS.LINUX:
println "Enum: OS.LINUX"
break
case "${OS.LINUX}":
println "String: LINUX"
break
case OS.MAC:
println "Enum: OS.MAC"
break
case "WINDOWS":
println "String: WINDOWS"
break
default:
println "Unknown OS: '$os'"
}
}

The output from the above:

  LINUX => Enum: OS.LINUX
LINUX => String: LINUX
MAC => Enum: OS.MAC
MAC => Unknown OS: 'MAC'
WINDOWS => String: WINDOWS
WINDOWS => String: WINDOWS

Looking at the results, I'm not sure I really have an answer. Using the "WINDOWS" case as an example, the first two cases could be replaced with "LINUX". But then, am I really switching on an enumerated value or just taking advantage of the fact that enumerations are easily converted to strings?

At the very least, this shows there are options depending on how strict I might want to be with the typing of the variable to be switched on.

2 comments:

  1. You are right that Groovy is casting the enumerated value to a string before doing the comparison.
    Personally, I'd switch on the enumeration itself, not its string representation. As of 1.7rc1, Groovy enums can have their toString overridden at compile-time to be something other than you might expect. Like this
    enum OS {
      LINUX {
        public String toString() {
          return "Solaris";
        }
      },
      WINDOWS {
        public String toString() {
          return "BSD";
        }
      },
      MAC {
        public String toString() {
          return "BeOS";
        }
      }
    }

    There might even be the possibility of altering each enumerated value's string representation at runtime with metaClass, though I haven't found a way to do this yet.

    It is interesting though that Groovy gives priority to the String case and not the enumerated type. I guess this is yet another justification for my paranoia and using strongly typed declarations (i.e. getting rid of the string cases and doing
    [OS.LINUX, OS.MAC, OS.WINDOWS].each { OS os ->).

    It might be nice to be more flexible, allowing both strings and enum types, but I think the former is more error-prone as the IDE will not be able to assist you and casing is also important. If provided as an interface to someone else, I'd rather have another method that lets them pass a string then try to get the value from that, like
    public String doSomething(String arg) {
      return someMethod( OS.valueOf(OS, arg) )
    }
    that would make troubleshooting easier (gives java.lang.IllegalArgumentException: No enum const class "<bad name here>" instead of just going to default case.

    ReplyDelete
  2. Thanks for the input Keegan.

    I recently revisited this in a situation where I had to parse XML and match the text of an element to one of the enumerated types.

    To continue the example, I ended up with something like this:

    enum OS { LINUX, MAC, WINDOWS }

    def osString = "MAC" // parsed from XML
    def result

    for(def os in OS) {
    if ("$os" == osString) {
    result = os
    break
    }
    }

    println result.class.name
    println result

    Resulting in:

    OS
    MAC

    I think that looks better than having a case for each potential value in an enum.

    ReplyDelete