- Implementing Domain:Specific Languages with Xtext and Xtend
- Lorenzo Bettini
- 5310字
- 2021-08-13 16:26:19
Xtend – a better Java with less "noise"
Xtend is a statically typed language and it uses the Java type system (including Java generics). Thus Xtend and Java are completely interoperable.
Most of the linguistic concepts of Xtend are very similar to Java, that is, classes, interfaces, and methods. Moreover, Xtend supports most of the features of Java annotations. One of the goals of Xtend is to have a less "noisy" version of Java; indeed, in Java, some linguistic features are redundant and only make programs more verbose.
Let's write a "Hello World" program in Xtend:
package org.example.xtend.examples class XtendHelloWorld { def static void main(String[] args) { println("Hello World") } }
You can see that it is similar to Java, though there are some differences. First of all, the missing semicolons ;
are not mistakes; in Xtend, they are not required (though they can be used). All method declarations start with either def
or override
(explained later in the chapter). Methods are public
by default.
Note that the editor works almost the same as the one provided by JDT (for example, see the Outline view for the Xtend class in the following screenshot). You may also want to have a look at the generated Java class in the xtend-gen
folder corresponding to the Xtend class (refer the following screenshot):
Although it is usually not required to see the generated Java code, it might be helpful, especially when starting to use Xtend, to see what is generated in order to learn Xtend's new constructs. Instead of manually opening the generated Java file, you can open the Xtend Generated Code view; the contents of this view will show the generated Java code, in particular, the code corresponding to the section of the Xtend file you are editing (refer the following screenshot). This view will be updated when the Xtend file is saved.
Here are some more Xtend examples:
class MyFirstXtendClass { val s = 'my field' // final field var myList = new LinkedList<Integer> // non final field def bar(String input) { var buffer = input buffer == s || myList.size > 0 // the last expression is the return expression } }
Fields and local variables are declared using val
(for final fields and variables) and var
(for non-final fields and variables). Fields are private by default.
You may have noticed that we wrote ==
for comparing two Java strings. This is usually one of the common mistakes in Java, since you will then compare the object references, not their actual values. However, Xtend handles operators transparently and correctly, thus ==
actually compares the values of the strings (indeed, it maps to the method equals
).
Xtend provides some "syntactic sugar" (that is, syntax that is designed to write code which is easier to read) for getter and setter methods, so that instead of writing, for example, o.getName(),
you can simply write o.name
; similarly, instead of writing o.setName("..."),
you can simply write o.name = "..."
. The same convention applies for boolean
fields according to JavaBeans conventions (where the getter method starts with is
instead of get
). Similar syntactic sugar is available for method invocations, so that when a method has no parameter, the parenthesis can be avoided. Therefore, in the preceding code, myList.size
corresponds to myList.size()
.
The preceding code also shows other important features of Xtend:
- Type inference: when the type of a variable can be inferred (for example, from its initialization expression), you are not required to specify it.
- Everything is an expression: in Xtend there are no statements, everything is an expression; in a method body, the last expression is the
return
expression (note the absence ofreturn
, although areturn
expression can be explicitly specified).
The declaration of a method's return type is not required: it is inferred from the returned expression (in this example it is boolean
).
Tip
Text hovering the Xtend elements in the editor will give you information about the inferred types.
Note that the types of method parameters must always be specified.
Access to static members (fields and methods) of classes in Xtend must be done using the operator ::
, instead of .
that is used only for non-static members (differently from Java, where .
is used for both), for example:
import java.util.Collections class StaticMethods { def static void main(String[] args) { val list = Collections::emptyList System::out.println(list) } }
Access to inner classes/interfaces of a Java class is done by using $
, for example, given this Java class:
public class MyJavaClass { interface MyInnerInterface { public static String s = "s"; } }
We can access the inner interface in Xtend using the following syntax:
MyJavaClass$MyInnerInterface
and, accordingly, the static
field can be accessed as follows:
MyJavaClass$MyInnerInterface::s
Note
In Xtend 2.4.2, static members and inner types are accessible with the dot operator as well. Therefore, the preceding line can also be written as follows: MyJavaClass.MyInnerInterface.s.
References to a Java class, that is, a type literal (which in Java you refer to by using the class name followed by .class
), are expressed with typeof(class name)
, for example:
typeof(Entity) // corresponds to Entity.class in Java
Note
In Xtend 2.4.2, type literals can also be specified by their simple name: instead of typeof(Entity),
you can simply write Entity
.
Xtend is stricter concerning method overriding: if a subclass overrides a method, it must explicitly define that method with override
instead of def
, otherwise a compilation error is raised. This should avoid accidental method overrides (that is, you did not intend to provide an overridden version of a method of a superclass). Even more importantly, if the method that is being overridden later is removed, you would want to know why your method may not ever be called.
In Xtend (and in general, by default, in any DSL implemented with Xtext using the default terminals grammar), strings can be specified both with single and double quotes. This allows the programmer to choose the preferred format depending on the string contents so that quotes inside the string do not have to be escaped, for example:
val s1 = "my 'string'" val s2 = 'my "string"'
Escaping is still possible using the backslash character \
as in Java.
Extension methods
Extension methods is a syntactic sugar mechanism that allows you to add new methods to existing types without modifying them; instead of passing the first argument inside the parentheses of a method invocation, the method can be called with the first argument as its receiver. It is as if the method was one of the argument type's members.
For example, if m(Entity)
is an extension method, and e
is of type Entity
, you can write e.m()
instead of m(e)
, even though m
is not a method defined in Entity
.
Using extension methods often results in a more readable code, since method calls are chained; for example, o.foo().bar()
rather than nested, for example: bar(foo(o))
.
Xtend provides several ways to make methods available as extension methods, as described in this section.
Xtend provides a rich runtime library with several utility classes and static methods. These static methods are automatically available in Xtend code so that you can use all of them as extension methods.
Of course, the editor also provides code completion for extension methods so that you can experiment with the code assistant. These utility classes aim at enhancing the functionalities of standard types and collections.
Tip
Extension methods are highlighted in orange in the Xtend editor.
For example, you can write:
"my string".toFirstUpper
instead of:
StringExtensions.toFirstUpper("my string")
Similarly, you can use some utility methods for collections, for example, addAll
as in the following code:
val list = new ArrayList<String> list.addAll("a", "b", "c")
You can also use static methods from existing Java utility classes (for example, java.util.Collections
) as extension methods by using a static extension import in an Xtend source file, for example:
import static extension java.util.Collections.*
In that Xtend file, all the static methods of java.util.Collections
will then be available as extension methods.
Methods defined in an Xtend class can automatically be used as extension methods in that class; for example:
class ExtensionMethods { def myListMethod(List<?> list) { // some implementation } def m() { val list = new ArrayList<String> list.myListMethod }
Finally, by adding the "extension
" keyword to a field, a local variable, or a parameter declaration, its instance methods become extension methods in that class, code block, or method body, respectively. For example, assume you have this class:
class MyListExtensions { def aListMethod(List<?> list) { // some implementation } def anotherListMethod(List<?> list) { // some implementation } }
and you want to use its methods as extension methods in another class, C
. Then, in C
, you can declare an extension field (that is, a field declaration with the extension
keyword) of type MyListExtensions,
and in the methods of C,
you can use the methods declared in MyListExtensions
as extension methods:
class C { extension MyListExtensions e = new MyListExtensions def m() { val list = new ArrayList<String> list.aListMethod list.anotherListMethod }
You can see that the two methods of MyListExtensions
are used as extension methods in C
. Indeed, the two method invocations inside the method m
are equivalent to:
e.aListMethod(list) e.anotherListMethod(list)
As mentioned earlier, you can achieve the same goal by adding the keyword extension
to a local variable:
defm
() { val extensionMyListExtensions
e = newMyListExtensions
vallist
= newArrayList<String>
list.aListMethod
list.anotherListMethod
}
or to a parameter declaration:
defm
(extensionMyListExtensions
e) { val list = newArrayList<String>
list.aListMethod
list.anotherListMethod
}
When declaring a field with the keyword extension
, the name of the field is optional. The same holds true when declaring a local variable with the keyword extension
.
As we hinted previously, the static methods of the classes of the Xtend library are automatically available in Xtend programs. Indeed, the static methods can be used independently of the extension method's mechanisms. The println
method in the first XtendHelloExample
class is indeed a static method from an Xtend utility class. Many utility methods are provided for dealing with collections; in particular, instead of writing:
val list = new ArrayList<String> list.addAll("a", "b", "c")
we can simply write:
val list = newArrayList("a", "b", "c")
By using these methods, you make full use of the type inference mechanism of Xtend.
The implicit variable – it
You know that in Java, the special variable this
is implicitly bound in a method to the object on which the method was invoked; the same holds true in Xtend. However, Xtend also introduces another special variable it
. While you cannot declare a variable or parameter with the name this
, you are allowed to do so using the name it
. If in the current program context a declaration for it
is available, then all the members of that variable are implicitly available without using the .
(just like all the members of this
are implicitly available in an instance method), for example:
class ItExamples { def trans1(String it) { toLowerCase // it.toLowerCase } def trans2(String s) { var it = s toLowerCase // it.toLowerCase } }
This allows you to write much more compact code.
Lambda expressions
A lambda expression (or lambda for short) defines an anonymous function. Lambda expressions are first class objects that can be passed to methods or stored in a variable for later evaluation.
Lambda expressions are typical of functional languages that existed long before object-oriented languages were designed. Therefore, as a linguistic mechanism, they are so old that it is quite strange that Java has not provided them from the start (they are planned to be available in Java 8). In Java, lambda expressions can be simulated with anonymous inner classes, as in the following example:
import java.util.*; public class JavaAnonymousClasses { public static void main(String[] args) { List<String> strings = new ArrayList<String>(); ... Collections.sort(strings, new Comparator<String>() { public int compare(String left, String right) { return left.compareToIgnoreCase(right); } }); } }
In the preceding example, the sorting algorithm would only need a function implementing the comparison; thus, the ability to pass anonymous functions to other functions would make things much easier (anonymous inner classes in Java are often used to simulate anonymous functions).
Indeed, most uses of anonymous inner classes employ interfaces (or abstract classes) with only one method (for this reason, they are also called SAM types – Single Abstract Method); this should make it even more evident that these inner classes aim to simulate lambda expressions.
Xtend supports lambda expressions: they are declared using square brackets []
; parameters and the actual body are separated by a pipe symbol, |
. The body of the lambda is executed by calling its apply
method and passing the needed arguments.
The following code defines a lambda expression that is assigned to a local variable, taking a string and an integer as parameters and returning the string concatenation of the two. It then evaluates the lambda expression passing the two arguments:
val l = [ String s, int i | s + i ] println(l.apply("s", 10))
Xtend also introduces types for lambda expressions (function types ); parameter types (enclosed in parentheses) are separated from the return type by the symbol =>
(of course, generic types can be fully exploited when defining lambda expression types). For example, the preceding declaration could have been written with an explicit type as follows:
val (String, int)=>String l = [ String s, int i | s + i ]
Recall that Xtend has powerful type inference mechanisms: variable type declarations can be omitted when the context provides enough information. In the preceding declaration, we made the type of the lambda expression explicit, thus the types of parameters of the lambda expression are redundant since they can be inferred:
val (String, int)=>String l = [ s, i | s + i ]
Function types are useful when declaring methods that take a lambda expression as a parameter (remember that the types of parameters must always be specified), for example:
def execute((String, int)=>String f) { f.apply("s", 10) }
We can then pass a lambda expression as an argument to this method. When we pass a lambda as an argument to this method, there is enough information to fully infer the types of its parameters, which allows us to omit these declarations:
execute([s, i | s + i])
A lambda expression also captures the current scope; all final local variables and all parameters that are visible at definition time can be referred to from within the lambda expression's body. This is similar to Java anonymous inner classes that can access surrounding final
variables and final
parameters.
Note
In Xtend, method parameters are always automatically final
.
Indeed, under the hood, Xtend generates Java inner classes for lambda expressions (and it will be Java 8 compatible in the future). The lambda expression is "closed" over the environment in which it was defined: the referenced variables and parameters of the enclosing context are captured by the lambda expression. For this reason, lambda expressions are often referred to as closures.
For example, consider the following code:
package org.example.xtend.examples class LambdaExamples { def static execute((String,int)=>String f) { f.apply("s", 10) } def static void main(String[] args) { val c = "aaa" println(execute([ s, i | s + i + c ])) // prints s10aaa } }
You can see that the lambda expression uses the local variable c
when it is defined, but the value of that variable is available even when it is evaluated.
Note
Formally, lambda expressions are a linguistic construct, while closures are an implementation technique. From another point of view, a lambda expression is a function literal definition while a closure is a function value. However, in most programming languages and in the literature, the two terms are often used interchangeably.
Although function types are not available in Java, Xtend can automatically perform the required conversions; in particular, Xtend can automatically deal with Java SAM (Single Abstract Method) types: if a Java method expects an instance of a SAM type, in Xtend, you can call that method by passing a lambda (Xtend will perform all the type checking and conversions). This is a further demonstration of how Xtend is tightly integrated with Java.
Therefore, the Java example using java.util.Collections.sort
passing an inner class can be written in Xtend as follows (the code is much more compact when using a lambda):
val list = newArrayList("Second", "First", "Third")
Collections::sort(list,
[ arg0, arg1 | arg0.compareToIgnoreCase(arg1) ])
Again, note how Xtend infers the type of the parameters of the lambda.
Xtend also supports loops; in particular, in for
loops, you can use the type inference and avoid writing the type (for example, for
(s : list)
). However, if you get familiar with lambdas, you will discover that you will tend not to use loops most of the time. In particular, in Java, you typically use loops to find something in a collection; with Xtend lambdas (and all the utility methods of the Xtend library that can be automatically used as extension methods), you do not need to write those loops anymore; your code will be more readable. For example, consider this Java code (where strings
is a List<String>
):
String found = null; for (String string : strings) { if (string.startsWith("F")) { found = string; break; } } System.out.println(found);
The corresponding Xtend code using a lambda is as follows:
println(strings.findFirst([ s | s.startsWith("F")]))
In the Java version, you do not immediately understand what the code does. The Xtend version can almost be read as an English sentence (with some known mathematical notations): "find the first element s in the collection such that s starts with an F".
Xtend provides some additional syntactic sugar for lambdas to make code evenmore readable.
First of all, when a lambda is the last argument of a method invocation, it can be put outside the (...)
parentheses (and if the invocation only requires one argument, the ()
can be omitted):
Collections::sort(list)[arg0, arg1 | arg0.compareToIgnoreCase(arg1)] strings.findFirst[ s | s.startsWith("F") ]
Furthermore, the special symbol it
we introduced earlier is also the default parameter name in a lambda expression; thus, if the lambda has only one parameter, you can avoid specifying it and instead use it
as the implicit parameter:
strings.findFirst[ it.startsWith("F") ]
and since all the members of it
are implicitly available without using ".
", you can simply write the following:
strings.findFirst[startsWith("F")]
This is even more readable. If you need to define a lambda that takes no parameter at all, you need to make it explicit by defining an empty list of parameters before the |
symbol. For example, the following code will issue a compilation error since the lambda (implicitly) expects one argument:
val l = [println("Hello")]
l.apply()
The correct version should be expressed as follows:
val l = [ | println("Hello")]
l.apply()
When implementing checks and generating code for a DSL, most of the time you will have to inspect models and traverse collections; all of the preceding features will help you a lot with this. If you are still not convinced, let us try an exercise: suppose you have the following list of Person
(where Person
is a class with string fields firstname
, surname
, and an integer field age
):
personList = newArrayList( new Person("James", "Smith", 50), new Person("John", "Smith", 40), new Person("James", "Anderson", 40), new Person("John", "Anderson", 30), new Person("Paul", "Anderson", 30))
and we want to find the first three younger persons whose first name starts with J,
and we want to print them as "surname, firstname" on the same line separated by ";
", thus, the resulting output should be (note: ;
must be a separator):
Anderson, John; Smith, John; Anderson, James
Try to do that in Java; in Xtend (with lambdas and extension methods), it is as simple as follows:
val result = personList.filter[firstname.startsWith("J")].
sortBy[age].
take(3).
map[surname + ", " + firstname].
join("; ")
println(result)
Multi-line template expressions
Besides traversing models, when writing a code generator, most of the time you will write strings that represent the generated code; unfortunately, also for this task, Java is not ideal. In fact, in Java, you cannot write multi-line string literals.
This actually results in two main issues: if the string must contain a newline character, you have to use the special character \n
; if, for readability, you want to break the string literal in several lines, you have to concatenate the string parts with +
.
If you have to generate only a few lines, this might not be a big problem; however, a generator of a DSL usually needs to generate lots of lines.
For example, let us assume that you want to write a generator for generating some Java method definitions; you can write a Java class with methods that take care of generating specific parts, as shown in the following code:
public class JavaCodeGenerator { public String generateBody(String name, String code) { return "/* body of " + name + " */\n" + code; } public String generateMethod(String name, String code) { return "public void " + name + "() {" + "\t" + generateBody(name, code) + "}"; } }
You can then invoke it as follows:
JavaCodeGenerator generator = new JavaCodeGenerator();
System.out.println(generator.generateMethod("m",
"System.out.println(\"Hello\"));\nreturn;"));
We can, however, spot some drawbacks of this approach:
- The shape of the final generated code is not immediately understandable (the variable parts break the constant parts)
- Code indentation is not trivial to handle
- Newline and tab characters are not easy to spot
- Some recurrent characters must be manually escaped (for example, the
""
quotations)
The result of running this generator is as follows:
public void m() { /* body of m */ System.out.println("Hello")); return;}
Although the generated code is a valid Java code, it is not nicely formatted (the Java compiler does not care about formatting, but it would be good to generate nicely formatted code, since the programmer might want to read it or debug it). This is due to the way we wrote the methods in the class JavaCodeGenerator
: the code is buggy with this respect, but it is not easy to get this right when using Java strings.
Xtend provides multi-line template expressions to address all of the preceding issues (indeed, all strings in Xtend are multi-line). The corresponding code generator written in Xtend using multi-line template expressions is shown in the following screenshot:
Before explaining the code, we must first mention that the final output is nicely formatted as it was meant to be:
public void m() { /* body of m */ System.out.println("Hello"); return; }
Template expressions are defined using triple single quotes (this allows us to use double quotes directly without escaping them); they can span multiple lines, and a newline in the expression will correspond to a newline in the final output. Variable parts can be directly inserted in the expression using guillemets («»
, also known as angle quotes or French quotation marks). Note that between the guillemets, you can specify any expression, and you can even invoke methods. You can also use conditional expressions and loops (we will see an example later in this book; you can refer to the documentation for all the details).
Note
Curly brackets {}
are optional for Xtend method bodies that only contain template expressions.
Another important feature of template expressions is that indentation is handled automatically and in a smart way. As you can see from the previous screenshot, the Xtend editor uses a specific syntax coloring strategy for multi-line template strings, in order to give you an idea of what the indentations will look like in the final output.
Tip
To insert the guillemets in the Xtend Eclipse editor, you can use the keyboard shortcuts Ctrl + Shift + < and Ctrl + Shift + > for « and » respectively. On a Mac operating system, they are also available with Alt + q («) and Alt + Q (»). Alternatively, you can use content assist inside a template expression to insert a pair of them.
The drawback of guillemets is that you will have to have a consistent encoding, especially if you work in a team using different operating systems. You should always use UTF-8 encoding for all the projects that use Xtend to make sure that the right encoding is stored in your project preferences (which is in turn saved on your versioning system, such as Git). You should right-click on the project and then select Properties..., and in the Resource property, set the encoding explicitly (refer to the following screenshot). You must set this property before writing any Xtend code (changing the encoding later will change all the guillemets characters, and you will have to fix them all by hand). Systems such as Windows use a default encoding that is not available in other systems, such as Linux, while UTF-8 is available everywhere.
Additional operators
Besides standard operators, Xtend has additional operators that helps to keep code compact.
First of all, most standard operators are extended to lists with the expected meaning. For example, when executing the following code:
val l1 = newArrayList("a") l1 += "b" val l2 = newArrayList("c") val l3 = l1 + l2 println(l3)
the string [a, b, c]
will be printed.
Quite often, you will have to check whether an object is not null before invoking a method on it, otherwise you may want to return null (or simply perform no operation). As you will see in DSL development, this is quite a recurrent situation. Xtend provides the operator ?.
, which is the null-safe version of the standard selection operator (the dot .
). Writing o?.m
corresponds to if (o != null) o.m
. This is particularly useful when you have cascade selections, for example, o?.f?.m
.
The Elvis ?:
operator is another convenient operator for dealing with default values in case of null instances. It has the following semantics: x ?: y
returns x
if it is not null and y
otherwise.
Combining the two operators allows you to set up default values easily, for example:
// equivalent to: if (o != null) o.toString else 'default' result = o?.toString ?: 'default'
The with operator (or double arrow operator), =>
, binds an object to the scope of a lambda expression in order to do something on it; the result of this operator is the object itself. Formally, the operator =>
is a binary operator that takes an expression on the left-hand side and a lambda expression with a single parameter on the right-hand side: the operator executes the lambda expression with the left-hand side as the argument. The result is the left operand after applying the lambda expression.
For example, the code:
return eINSTANCE.createEntity => [ name = "MyEntity"]
is equivalent to:
val entity = eINSTANCE.createEntity entity.name = "MyEntity" return entity
This operator is extremely useful in combination with the implicit parameter it
and the syntactic sugar for getters and setters to initialize a newly created object to be used in a further assignment without using temporary variables (again increasing code readability). As a demonstration, consider the Java code snippet we saw in Chapter 2, Creating Your First Xtext Language, that we used to build an Entity
with an Attribute
(with its type) that we will report here for convenience:
Entity entity = eINSTANCE.createEntity();
entity.setName("MyEntity");
entity.setSuperType(superEntity);
Attribute attribute = eINSTANCE.createAttribute();
attribute.setName("myattribute");
AttributeType attributeType = eINSTANCE.createAttributeType();
attributeType.setArray(false);
attributeType.setDimension(10);
EntityType entityType = eINSTANCE.createEntityType();
entityType.setEntity(superEntity);
attributeType.setElementType(entityType);
attribute.setType(attributeType);
entity.getAttributes().add(attribute);
This requires many variables that are a huge distraction (are you able to get a quick idea of what the code does?). In Xtend, we can simply write:
eINSTANCE.createEntity => [
name = "MyEntity"
superType = superEntity
attributes += eINSTANCE.createAttribute => [
name = "myattribute"
type = eINSTANCE.createAttributeType => [
array = false
dimension = 10
elementType = eINSTANCE.createEntityType => [
entity = superEntity
]
]
]
]
Polymorphic method invocation
Method overloading resolution in Java (and by default in Xtend) is a static mechanism, meaning that the selection of the specific method takes place according to the static type of the arguments. When you deal with objects belonging to a class hierarchy, this mechanism soon shows its limitation: you will probably write methods that manipulate multiple polymorphic objects through references to their base classes, but since static overloading only uses the static type of those references, having multiple variants of those methods will not suffice. With polymorphic method invocation (also known as multiple dispatch, or dynamic overloading), the method selection takes place according to the runtime type of the arguments.
Xtend provides Dispatch Methods for polymorphic method invocation: upon invocation, overloaded methods marked as dispatch
are selected according to the runtime type of the arguments.
Going back to our Entities DSL of Chapter 2, Creating Your First Xtext Language, ElementType
is the base class of BasicType
and EntityType
, and AttributeType
has a reference, elementType
, to an ElementType
; to have a string representation for such a reference, we can write two dispatch methods as in the following example:
def dispatch typeToString(BasicType type) { type.typeName } def dispatch typeToString(EntityType type) { type.entity.name }
Now when we invoke typeToString
on the reference elementType
, the selection will use the runtime type of that reference:
def toString(AttributeType attributeType) {
attributeType.elementType.typeToString
}
With this mechanism, you can get rid of all the ugly instanceof
cascades (and explicit class casts) that have cluttered many Java programs.
Enhanced switch expressions
Xtend provides a more powerful version of Java switch
statements. First of all, only the selected case
is executed, in comparison to Java that falls through from one case to the next (thus, you do not have to insert an explicit break instruction to avoid subsequent case block execution); furthermore, a switch can be used with any object reference.
Xtend switch expressions allow you to write involved case expressions, as shown in the following example:
def String switchExample(Entity e, Entity specialEntity) { switch e { case e.name.length > 0 : "has a name" case e.superType != null : "has a super type" case specialEntity : "special entity" default: "" } }
If the case expression is a boolean
expression (like the first two cases in the preceding example), then the case matches if the case expression evaluates to true
. If the case expression is not of type boolean
, it is compared to the value of the main expression using the equals
method (the third case in the preceding example). The expression after the colon of the matched case is then evaluated, and this evaluation is the result of the whole switch expression.
Another interesting feature of Xtend switch expressions is type guards. With this functionality, you can specify a type as the case condition and the case matches only if the switch value is an instance of that type (formally, if it conforms to that type). In particular, if the switch value is a variable, that variable is automatically casted to the matched type within the case body. This allows us to implement a cleaner version of the typical Java cascades of instanceof
and explicit casts. Although we could use dispatch methods to achieve the same goal, switch expressions with type guards can be a valid and more compact alternative.
For example, the code in the previous section using dispatch methods can be rewritten as follows:
def toString(AttributeType attributeType) { val elementType = attributeType.elementType switch elementType { BasicType : // elementType is a BasicType here elementType.typeName EntityType: // elementType is an EntityType here elementType.entity.name } }
Note how entityType
is automatically casted to the matched type in the case body.
Depending on your programming scenario, you might want to choose between dispatch methods and type-based switch expressions; however, keep in mind that while dispatch methods can be overridden and extended (that is, in a derived class, you can provide an additional dispatch method for a combination of parameters that was not handled in the base class), switch expressions are inside a method, and thus they do not allow for the same extensibility features. Moreover, dispatch cases are automatically reordered with respect to type hierarchy (most concrete types first), while switch cases are evaluated in the specified order.