5. ledna 2010

Lekce ze Scaly: Jak se vyvarovat NullPointerException

Jak se v jazyce Scala brání NullPointerException? V hojné míře se používá třída Option, která zajistí, že nemusí metoda vracet null, ale vrací existující instanci. Jak to celé funguje si ukážeme v Javě:

public abstract class Option<T> {
  public abstract T get();
  public abstract boolean isNone();
  public abstract boolean isSome();
}

Pokud potřebujeme aby nějaká metoda vracela něco a nebo null, použijeme třídu Option. Díky ní metoda vrátí existující instanci, ze které jednoduše zjistíme, zda návratová hodnota má být null (isNone() vrací true) a nebo je to nějaká smyslu plná hodnota (isSome() vrací true) a pak se k této hodnotě dostaneme pomocí metody get().

Teď už to chce jenom nějaký rozumný příklad. Vezněme si např. Map a její metodu get:

V get(T key);
Ta v případě, že klíč v mapě neexistuje vrací null. Takže typicky píšeme něco jako:

String value = map.get(key);
if (value != null) {
  System.out.println(value);
} else {
  System.out.println("Nic");
}

A jak by tedy měla metoda get objektu Map vypadat kdybychom byli ve Scale:

Option<V> get(T key);
A nyní jak by vypadal náš kód pro čtení z mapy:

Option<String> value = map.get(key);
if (value.isSome()) {
  System.out.println(value.get());
} else {
  System.out.println("Nic");
}

A zbavili jsme se ošklivého porovnání na null, což jsme chtěli. Navíc pokud metoda vrací Option a ne přímo hodnotu je podstatně větší pravděpodobnost, že nezapomeneme otestovat zda Option něco obsahuje. Pokud metoda vrací přímo hodnotu nebo null, pak hrozí, že hodnotu rovnu použijeme a neotestujeme ji na null.

A nyní přistupme k implementaci:

public class None<T> extends Option<T> {

  public T get() {
    throw new UnsupportedOperationException("The None had no value");
  }

  public boolean isNone() {
    return true;
  }

  public boolean isSome() {
    return false;
  }

  public boolean equals(Object obj) {
    return ((obj == this) || (obj instanceof None));
  }

 public int hashCode() {
   return 1;
  }

}

public class Some<T> extends Option<T> {
  protected T value;
 
  public Some(T value) {
    super();
    this.value = value;
  }

  public T get() {
    return value;
  }

  public boolean isNone() {
    return false;
  }

  public boolean isSome() {
    return true;
  }

  public boolean equals(Object obj) {
    if (obj == this) return true;
    if (obj instanceof Some) {
      Some other = (Some) obj;
      return value.equals(other.value);
    }
    return false;
  }

  public int hashCode() {
    return value.hashCode();
  }
 
}

A jak to celé funguje ve Scale? Díky pattern matchingu a type inference ještě elegantněji:

val value = map.get(key) 
value match {
  case None => print "Nic"
  case Some(x) => print x
}