Einfache Sprachelemente


 ... [ Groovy - Titelseite ] ... [ << Einleitung ] ... [ Funktionale Elemente >> ] ...  




Übersicht:

  Sicheres Dereferenzieren
  Indizierter Zugriff auf Arrays und Listen
  Optionale Parameter, benannte Parameter & Parameterlisten
  Einfache Typen
  Getter und Setter
  Strings und Reguläre Ausdrücke
  Überladen von Operatoren
  Der Typ Range



Sicheres Dereferenzieren

Groovy hat neben dem . Operator eine weitere Möglichkeit zum Dereferenzieren und Selektieren bekommen: den ?. Operator. Mit diesem ist es möglich, das Testen auf null-Referenzen zu unterlassen, und das Auswerten des Folgeausdrucks zu unterdrücken. Für den Fall das die Referenz null ist, wird der Ausdruck wird ist zu null ausgewertet.
class A {
    String s = "foo";
}
A a = null;
assert null == a?.s; //!!
a = new A();
assert "foo" == a?.s;
In dem Beispiel würde das normale Dereferenzieren mittels a.s zu einer NullPointerException führen. Mit Hilfe des neuen Operators kann man sich das häufige Testen auf Gleichheit mit null sparen.

Indizierter Zugriff auf Arrays und Listen

In Groovy verhalten sich Listen und Arrays sehr ähnlich. Beide sind mit eckigen Klammer indizierbar. Es ist auch möglich, die Indizierung nicht wie gewohnt von vorn, sondern von hinten zu beginnen. a[-1] liefert das letzte Element von a; a[-2] liefert das vorletzte Element, usw. Dies ist eine praktische Abkürzung, allerdings werden dadurch einige IndexOutOfBoundsExceptions unterdrückt.

Optionale Parameter, benannte Parameter & Parameterlisten

In vielen Programmiersprachen werden Parameter von Funktionen anhand der Reihenfolge der formalen und aktuellen Parameter zugeordnet. Der erste aktuelle Parameter entspricht dabei dem ersten formalen Parameter usw. In Groovy ist es möglich, wie auch in Python und VHDL, Parameter anhand ihres Namens zu identifizieren.

String concat(Map args)
{
    return args.get("a", "anfang") + args.get("limiter", "") + args.get("b", "ende");
}
assert "schallrauch" == concat(a:"schall", b:"rauch", limiter:"");
assert "anfang bis ende" == concat(limiter:" bis ");
In dem Beispiel sieht man die Syntax, die für benannte Parameter genutzt wird. Wie man an der zweiten Assertion sehen kann, müssen nicht alle Parameter angegeben werden, da man in der concat Methode Standardwerte genutzt hat. In Groovy sind benannte Parameter immer auch optionale Parameter. Tatsächlich wird bei der Nutzung von benannten Parametern immer nur ein Parameter übergeben: eine Map, die Strings (die Parameternamen) auf Objects (die Parameterwerte) abbildet. Wird ein Parameter ausgelassen, dann zeigt der zugehörige String auf null.

Ab der Version 1.5 ist es auch in Java möglich, beliebige viele Parameter zu übergeben, indem man die ... Syntax bei der Funktionsdeklaration nutzt, was man auch unter dem Namen variable Parameterliste kennt. In Groovy nutzt man stattdessen ein Object[] als letzten Parameter. Auch in Java ist der letzte Parameter implizit bei der ... Notation ein Array.

int sum(Object[] l)
{
    int res = 0;
    for(o in l)	{
	res += o;
    }
    return res;
}
assert sum(2, 3, 37) == 42;
assert sum() == 0;

Im Vergleich mit Java sind die optionalen Parameter tatsächlich neu. Im Gegensatz zu den variablen Parameterlisten bleibt die maximale Anzahl der aktuellen Parameter begrenzt.

String concat(String msg, String extension = "")
{
    return msg + extension;
}
assert "schall" == concat("schall");
assert "ab" == concat("a", "b");
In dem Beispiel kann die Methode concat mit einem oder mit zwei Parametern aufgerufen werden. Bislang sind bei der Deklaration der optionlen Parameter nur Literale und keine Referenzen möglich.



Einfache Typen

Gegenüber Java gibt es in Groovy einige Besonderheiten bei der Typisierung. Es gibt keine einfachen Typen mehr, diese werden durch die entsprechende Wrapper-Klasse ersetzt.

int i = 42;
println i.class
j = 42.0
println j.class
double k = 42
println k.class
Ausgabe:
          class java.lang.Integer
          class java.math.BigDecimal
          class java.lang.Double
        
Trotzdem sind alle gewöhnlichen Operatoren für Zahlen nutzbar, zum Beispiel +, -, ++ . Beim Konvertieren von Zahlen wird darauf geachtet, dass keine Informationen verloren gehen. Somit gilt:
1/2 == 0.5
Leider gilt aber immer noch:
(1/3)*3 != 1
BigDecimal schafft zwar einen beliebig grossen Wertebereich, die Genauigkeit ist aber immer fix.



Getter und Setter

In Java wird automatisch ein öffentlicher Konstruktor generiert, wenn der Programmierer keinen explizit angibt. In Groovy werden zusätzlich zu jedem Objekt-Attribut

T xxx;
mit der Sichtbarkeit default eine set-Methode
public void setXxx(T value) { this.xxx = value; }
und eine get-Methode
public T getXxx() { return this.xxx; }
generiert. Dies erleichtert das Implementieren von Klassen mit vielen einfachen Attributen und reduziert die sonst hohe Redundanz im Quelltext. Die set- und get-Methoden können vom Programmierer überschrieben werden, wenn sie mehr Logik benötigen.

Vorsicht: Dieses Feature wurde noch nicht implementiert.



Strings und Reguläre Ausdrücke

In Java ist es recht umständlich, mehrzeilige Strings einzugeben. Daher hat man in Groovy die Möglichkeit HERE-Dokumente einzugeben, wie es in der bash üblich ist:

String msg = """
ein text, der ganz
viele
Zeilen
hat"""

println msg

In den meisten Skriptsprachen, zum Beispiel Tcl, gibt es Möglichkeiten in einem String-"Literal" Ausdrücke anzugeben, welche zur Laufzeit ausgewertet werden. Dies ist in Groovy mit GStrings möglich. Es können darin Variablenbezeichner durch ihren aktuellen Wert ersetzt werden:

int alter = 42
println "ich bin $alter Jahre alt"
println "ich bin ${alter} Jahre alt"
Außerdem ist es möglich, beliebige Ausdrücke in sogenannten GStrings auswerten zu lassen:
println """heute ist:
${new Date()}"""

println """gestern war:
${new Date() - 1}"""

Um eine Zeichenkette mit vielen Escape-Sequenzen darzustellen, kann diese in Schrägstriche (/) geklammert werden. Bei dieser "slashy" Syntax werden alle umgekehrten Schrägstriche so übernommen, wie sie eingegeben werden. Dies ist insbesondere bei Regulären Ausdrücken hilfreich, da in deren Syntax viele Backslashes vorkommen.

Insgesamt gibt es fünf Darstellungsmöglichkeiten für Strings und GStrings: 'foo', "foo", '''foo''', """foo""", /foo/ repräsentieren alle die gleiche Zeichenkette. Die unterschiedlichen Begrenzer haben folgenden Eigenschaften:

  • Drei Mal geklammerte Strings können mehrzeilig eingegeben werden.
  • Einfache Anführungszeichen, egal ob ein Mal oder drei Mal geklammerte, verhindern die Auswertung von GStrings.

Groovy bietet eine Unterstützung für Reguläre Ausdrücke auf Syntax-Ebene, es werden dazu drei Operatoren bereitgestellt:

  • s =~ re Ergibt genau dann true, wenn re in s enthalten ist.
  • s ==~ re Ergibt genau dann true, wenn re dem kompletten String s entspricht.
  • ~String erzeugt einen Regulären Ausdruck aus dem String vom Typ java.util.regex.Pattern. Dadurch kann das häufige Neuerzeugen eines Pattern-Objektes verhindert werden.



Überladen von Operatoren

In Java sind für Referenztypen lediglich die Vergleichsoperatoren erlaubt, sowie für die Stringverknüpfung der + Operator. In Groovy wird viel mehr mit Operatoren als mit Funktionsnamen gearbeitet, was die Lesbarkeit erhöhen kann. Die Wrapperklassen der einfachen Datentypen überschreiben beispielsweise die arithmetischen Operatoren. Das Überladen von Operatoren kann aber auch vom Anwendungsprogrammierer vorgenommen werden, da alle Operatoren auf einen Funktionsnamen abgebildet werden. Die folgende Tabelle, gibt einige Beispiele dazu an.

Operator Methodenaufruf
a + b a.plus(b)
a - b a.minus(b)
a * b a.multiply(b)
a ** b a.power(b)
a++, ++a a.next()
a == b a.equals(b)
a > b a.compareTo(b) > 0
switch(a){case b:} b.isCase(a)
a[b] a.getAt(b)
a[b] = c a.putAt(b, c)

Der Index-Operator [] kann dazu genutzt werden, auf Attribute lesend oder schreibend zuzugreifen. Damit ist es möglich den Namen des Attributs, welches man bearbeiten möchte, zur Laufzeit zu berechnen:

class Foo
{
    String schall = "laut"
}
String name = "schall"
Foo foo = new Foo();
foo[name] = "42"
println foo.schall
println foo[name]

Mit Switch-Anweisungen lassen sich beliebige Objekte vergleichen, nicht nur ints wie in Java. Im folgenden Beispielcode wird überprüft, ob ein Objekt eine Zahl oder einen String enthält:

Object o = 3.3;
switch(o) {
case String:
    println "ein string";
    break;
case Integer:
    println "eine ganze Zahl";
    break;
default:
    println "was anderes";
    break;
}
Diese Nutzung ist möglich, da in der Klasse Class der isCase Operator entsprechend überschrieben wurde.

Etwas sinnvoller lässt sich mit Hilfe von Switch-Anweisungen testen, welchem Regulären Ausdruck ein String entspricht:

//String userInput = "23:59";
String userInput = "42a";
switch(userInput) {
case ~/[0-9]+/:
    println "eine Zahl";
    break;
case ~/[0-2][0-9]:[0-5][0-9]/:
    println "eine Uhrzeit";
    break;
default:
    println "was anderes";
    break;
}

switch Anweisungen entsprechen eher verschachtelten else-if Strukturen, als Java-switches. Daher muss bei der Nutzung von komplexen Datentypen in switch Anweisungen immer darauf geachtet werde, die korrekte Reihenfolge einzuhalten.

Eine weitere Möglichkeit bieten switch Anweisungen in Kombination mit Closures. Beliebige boolesche Ausdrücke lassen sich somit kompakt in einer Anweisung formulieren, ohne auf die else-if Schachtelungen zurückgreifen zu müssen. Die Variable it wird immer mit dem zu überprüfenden Wert belegt.

int userInput = 123;
switch(userInput) {
case {it % 2 == 0}:
    println "gerade";
    break;
case {it % 10 == 3}:
    println "endet mit 3";
    break;
default:
    println "was anderes";
    break;
}



Der Typ Range

Ranges (Bereichstypen) dienen dazu, einen bestimmten Bereich, zum Beispiel von Zahlen, zu beschreiben. Damit sind Ranges in index-basierten Schleifen häufig einsetzbar:
for( i in 24..29) {
    println i;
}

Der Operator .. erzeugt einen beidseitig inklusiven Bereich, der Operator ..< einen rechtsseitig exklusiven Bereich.

Ranges können sehr Speicherplatzt-sparend implementiert werden. Ein Range besteht aus einer linken und einer rechten Grenze, sowie einer Funktion zum inkrementieren und einer zum dekrementieren. Die linke Grenze muß nicht zwangsläufig die kleinere sein, in dem Fall wird einfach die Reihenfolge umgekehrt.

Ranges können für alle Typen benutzt werden, die
  • die ++ und -- Operatoren überschreiben
  • java.lang.Comparable implementieren
Somit funktionieren Ranges auch mit Strings. Ein Beispiel:
for( i in ("fol"..<"fop")) {
    println i;
}
Ausgabe:
        fol
        fom
        fon
        foo
      



 ... [ Groovy - Titelseite ] ... [ << Einleitung ] ... [ Funktionale Elemente >> ] ... [ nach oben ] ...