Skip to content

クラスとオブジェクト

Onion はクラス、継承、インターフェースを完全にサポートします。Java や Kotlin を書いたことがあれば、ほぼそのままの感覚で使えます。

クラス定義

class キーワードでクラスを定義します:

class Person {
  val name: String
  var age: Int

  public:
    def this(n: String, a: Int) {
      this.name = n
      this.age = a
    }

    def greet: String = "Hello, I'm " + this.name
}

new でオブジェクトを生成します:

val person: Person = new Person("Alice", 30)
println(person.greet())  // "Hello, I'm Alice"

プライマリコンストラクタ

クラス名の後ろにパラメータを書くと、プライマリコンストラクタになります。val/var パラメータはそのままパブリックフィールドになります:

class Point(val x: Int, val y: Int) {
public:
  def dist(): Int { return this.x * this.x + this.y * this.y }
}

class Conf(val host: String = "localhost", var port: Int = 8080)

class Animal(val name: String)
class Dog(name: String, val breed: String) extends Animal(name)

val p = new Point(3, 4)        // p.x、p.y が使える
val c = new Conf(port = 9090)  // host はデフォルト値 "localhost"

プライマリコンストラクタを持つクラスにも def this でコンストラクタを追加できます。 その場合、各 def this は : this(...) でプライマリコンストラクタに委譲します。 詳しくは次節を参照してください。

コンストラクタ

Onion のコンストラクタは Scala / Kotlin と同じ primary / secondary モデルです。

  • プライマリコンストラクタは、クラス名の後ろのパラメータリストと extends 節の 引数を合わせたものです。親クラスのコンストラクタを呼ぶのはここだけで、val/var パラメータをフィールドに格納するのもここです。
  • セカンダリコンストラクタはクラス本体の def this(...) です。プライマリ コンストラクタを持つクラスでは、すべてのセカンダリは : this(...) で(直接、または 別のセカンダリ経由で)プライマリに委譲します。この規則があるおかげで、プライマリの val フィールドが未初期化のまま観測されることはなく、親クラスは常に extends 節に 書いた通りに構築されます。
class Point(val x: Int, val y: Int) {
public:
  def this(x: Int) : this(x, 0) { }      // セカンダリ: プライマリに委譲
  def this : this(0) { }                 // セカンダリ同士でチェインしてもよい
  def dist(): Int { return x * x + y * y }
}

val a = new Point(3, 4)
val b = new Point(3)                     // y == 0
val c = new Point()                      // x == 0, y == 0

プライマリコンストラクタを持つクラスで、委譲しない def this はエラーです(E0087)。 この規則ができる前は class P(val x: Int) { def this { } } がコンパイルでき、 new P().x は 0 でした。

親クラスへの引数

親クラスのコンストラクタ引数は extends 節に書き、書けるのはそこだけです。 セカンダリコンストラクタはプライマリに委譲することで親に到達します。

class Animal(val name: String)

class Dog(name: String, val breed: String) extends Animal(name) {
public:
  def this(name: String) : this(name, "mixed") { }
}

パラメータリストが無くても extends に引数があれば、それを親に渡す引数なしの プライマリコンストラクタになります(Kotlin / Scala と同じ読み方です)。

class Rex extends Animal("Rex") { }      // new Rex().name == "Rex"

プライマリコンストラクタを持たないクラス

パラメータリストも extends の引数も無いクラスにはプライマリコンストラクタがありません。 その def this は暗黙に親クラスの引数なしコンストラクタを呼び、互いに委譲できます。

class Rectangle {
  val width: Int
  val height: Int
public:
  def this(w: Int, h: Int) {
    this.width = w
    this.height = h
  }
  def this(size: Int) : this(size, size) { }
  def this : this(0) { }
}

このようなクラスから親クラスに引数を渡すことはできません。必要ならプライマリ コンストラクタを書いてください。(以前は def this(x) : (x) { } という形でそれが できましたが、この形はもうありません。コンパイラは extends 節を書くよう案内します。)

フィールド初期化子

フィールド初期化子は、プライマリコンストラクタがパラメータを格納した後に実行される ので、val/var パラメータのフィールドを読めます。

class Account(val opening: Int) {
public:
  var balance: Int = opening        // プライマリが格納したばかりのフィールドを読む
  var history: List[Int] = [opening]
}

初期化子は new がどのコンストラクタを通ったかに関係なく、プライマリの中で 1 回だけ 実行されます。val/var の付かないプライマリパラメータはフィールドではないので、 初期化子からは見えません。

コンストラクタ引数の中でできないこと

委譲引数を評価している時点ではオブジェクトがまだ存在しないので、: this(...) や extends B(...) の引数の中で this やフィールドを読むことはできません(E0090)。 パラメータか定数を渡してください。クロージャは this を捕捉して後で実行されるので 問題ありません。互いに委譲し合って循環するコンストラクタは、new でスタックオーバー フローする代わりに拒否されます(E0088)。record と enum は正準コンストラクタを持つため def this を宣言できません(E0089)。

プライマリコンストラクタは常に public です。private なコンストラクタ(ファクトリメソッド パターン)が欲しいクラスは、プライマリを宣言せず、デフォルト(private)セクションに def this を書きます。

フィールドとアクセス修飾子

フィールドはデフォルトでprivateです。public: セクションで公開します:

class BankAccount {
  var balance: Double    // private
  val accountNumber: String  // private

  public:
    val owner: String  // public

    def this(owner: String, initial: Double) {
      this.owner = owner
      this.balance = initial
      this.accountNumber = "UNKNOWN"
    }

    def deposit(amount: Double) {
      this.balance = this.balance + amount
    }

    def getBalance: Double = this.balance
}

メソッド

インスタンスメソッド

インスタンスのデータを操作するメソッド:

class Circle {
  val radius: Double

  public:
    def this(r: Double) {
      this.radius = r
    }

    def area: Double = 3.14159 * this.radius * this.radius

    def circumference: Double = 2.0 * 3.14159 * this.radius
}

val circle: Circle = new Circle(5.0)
println("Area: " + circle.area())

メソッドのオーバーロード

同じ名前で引数の異なる複数のメソッドを定義できます:

class Printer {
  public:
    def print(value :Int) {
      println("Int: " + value)
    }

    def print(value :String) {
      println("String: " + value)
    }

    def print(value :Double) {
      println("Double: " + value)
    }
}

val printer: Printer = new Printer
printer.print(42)
printer.print("Hello")
printer.print(3.14)

ゲッターとセッターメソッド

class Person {
  var name: String
  var age: Int

  public:
    def getName: String = this.name

    def setName(name :String) {
      this.name = name
    }

    def getAge: Int = this.age

    def setAge(age :Int) {
      if age >= 0 {
        this.age = age
      }
    }
}

self 参照

現在のインスタンスは self でも参照できます:

import {
  javax.swing.JButton;
  java.awt.event.ActionEvent;
  java.awt.event.ActionListener;
}

class ButtonHandler conforms ActionListener {
  public:
    def actionPerformed(event :ActionEvent) {
      val button: JButton = event.getSource() as JButton
      button.addActionListener(self)  // このインスタンスへの参照
    }
}

this と self が使えるのはインスタンスのコンテキストだけです。static メソッドや static フィールドから参照することはできません。

staticメンバー

static キーワードでクラスレベルのメンバーを宣言します。アクセスには :: を使います:

class MathUtils {
  static val PI: Double = 3.14159

  public:
    static def square(x: Double): Double = x * x
}

val area: Double = MathUtils::PI * MathUtils::square(5.0)

Records

Recordsはequals、hashCode、toString、copyが自動生成される不変データクラスです:

record Point(x: Int, y: Int)

val p = new Point(1, 2)
p.x()            // コンポーネントアクセス(メソッド呼び出し)
p.copy(y = 9)    // 名前付き引数で部分コピー
p.copy()         // フルクローン

型パラメータも使えます:

record Pair[A, B](first: A, second: B)

val p = new Pair[String, Integer]("gen", 9)
val (s, n) = p              // 分割代入
p.copy(second = 42)         // 名前付き引数でコピー

recordはクラスやenumと同じく、{ ... } のメソッド本体——インスタンスメソッド・静的 ファクトリ・privateヘルパー・演算子メソッド——を持てます。メソッドは自動生成された コンポーネントアクセサを参照できます:

record Fraction(num: Int, den: Int) {
public:
  static def of(n: Int, d: Int): Fraction {
    val g = gcd(Math::abs(n), d)
    return new Fraction(n / g, d / g)
  }
  def plus(o: Fraction): Fraction =         // `+` 演算子を実装
    Fraction::of(num() * o.den() + o.num() * den(), den() * o.den())
private:
  static def gcd(a: Int, b: Int): Int { ... }
}

val third = Fraction::of(1, 3)
val one = third + third + third            // ちょうど 1/1

演算子オーバーロード

二項演算子は左オペランドのメソッドにディスパッチされます(Kotlinスタイル):a + b は a.plus(b) を呼び出します:

class Vec {
  val x: Int
  val y: Int
public:
  def this(x: Int, y: Int) { this.x = x; this.y = y }
  def plus(o: Vec): Vec { return new Vec(this.x + o.x, this.y + o.y) }
  def times(k: Int): Vec { return new Vec(this.x * k, this.y * k) }
}

val v = new Vec(1, 2) + new Vec(3, 4)   // Vec(4, 6)
val w = new Vec(1, 2) * 3               // Vec(3, 6)

Enums

Enumsは標準JVM enumにコンパイルされます。データを持つenumも定義できます:

enum Color { RED, GREEN, BLUE }

enum Planet(mass: Double) {
  MERCURY(3.3e23),
  EARTH(5.97e24)
}

println("" + Planet::EARTH.mass())
foreach p: Planet in Planet::values() {
  println(p.name() + " = " + p.mass())
}
Planet::valueOf("EARTH")     // java.lang.Enum.valueOf と同様に動作します

Enumsは定数リストの後のアクセスセクションでメソッドを宣言できます—— インスタンスメソッドは各定数のデータを、staticメソッドは values() を参照できます:

enum Planet(mass: Double) {
  MERCURY(3.3e23),
  EARTH(5.97e24)
public:
  def heavierThan(other: Planet): Boolean {
    return this.mass() > other.mass()
  }
}

代数的データ型(case case)

case が case キーワードを使うと、各 case は自分自身のフィールドを持てます。 enum が積の和(sum-of-products)になり、代数的データ型を書くのに sealed interface + record を手書きする必要がなくなります:

enum Shape {
  case Circle(radius: Double)
  case Square(side: Double)
  case Origin
public:
  def area(): Double = select this {
    case c is Circle: c.radius() * c.radius() * 3.14
    case s is Square: s.side() * s.side()
    case o is Origin: 0.0
  }
}

val c: Shape = new Circle(2.0)
c.area()                      // 12.56

product case(case Circle(radius: Double))はアクセサ付きの型付きフィールドを 持ち、singleton case(case Origin)は new Origin() で使う 0 フィールド case です。 enum は case ごとに record を持つ sealed interface に desugar されるので、 網羅性チェック(E0042)と select パターンマッチが自動で効きます。

case 形式の enum は java.lang.Enum ではなく sealed 階層なので、 values()/valueOf()/ordinal() は付きません。それらが必要なときは上の定数形式を 使ってください。

sealed 階層であるがゆえに、case 形式の enum は型パラメータを取れます。 Option 相当の型が書けるのはこのためです。

enum Opt[T] {
  case Some(value: T)
  case Nothing
}

def describe(o: Opt[String]): String = select o {
  case s is Some:    "some: " + s.value()   // ここでの s は Some[String]
  case n is Nothing: "none"
}

型パラメータは生成される interface と各 case の record に引き継がれます。また型 パターンはスクルーティニーの型引数を復元するので、Opt[String] から Some を マッチすると Some[String] に束縛され、s.value() は裸の T ではなく String に なります。定数形式の enum は型パラメータを取れません(java.lang.Enum になり、 JVM がジェネリックな enum を許さないためです)。

ジェネリッククラス

クラスは [] で型パラメータを取れます。型パラメータは本体で通常の型として使えます:

class Box[T] {
  val v: T
public:
  def this(x: T) { v = x }
  def get(): T = v
}

コンストラクタは型引数を推論するので new Box(...) に [T] は不要です(ダイヤモンド)。期待される型があればそこから、無ければコンストラクタ引数から推論されます(ジェネリックメソッド呼び出しが引数から推論するのと同じ仕組みです)。明示指定も可能で、いずれも同じ結果になります:

val b: Box[String] = new Box("x")           // 期待型から T を推論
val b2: Box[String] = new Box[String]("y")  // 明示指定 — 同じ結果
val b3 = new Box("z")                       // 引数から T を推論

引数からの推論は、すべての型パラメータが引数によって決まる場合にのみ働きます。決め手が無い場合、raw なジェネリック型は従来どおりエラーになります(E0066)。期待型を注釈するか、型引数を明示してください:

class Empty[T] { public: def this {} }

// val bad = new Empty()    // E0066: T を推論する材料が無い。
                            // `new Empty[String]()` と書くか、期待型を注釈する

型引数は不変です(Box[Dog] は Box[Animal] ではありません。変数と型を参照)。

次のステップ