Scalaのリッチラッパー
1. 序章
このチュートリアルでは、リッチラッパーとは何か、そしてScalaでそれらをどのように使用できるかを学びます。
「リッチラッパー」の定義は、名前自体にあります。 まず、他のクラスを強化します。つまり、「元の」クラスに欠けている機能を追加します。
次に、他のクラスをラップします。つまり、ラップするクラスと同じように動作する必要があります。 理想的には、ラッパーを使用することと元のクラスを使用することの間に目に見える違いがないはずです。
独自のラッパーを作成する方法を簡単に説明しますが、その言語で使用可能なラッパーに焦点を当てます。
2. 代替アプローチの欠点
何かを豊かにする方法はたくさんあります。 たとえば、元のクラスにアクセスできる場合は、メソッドを追加できます。 それ以外の場合は、継承を使用して、必要な追加のプロパティを持つサブクラスを作成できます。
それにもかかわらず、これらの2つのアプローチは「濃縮」部分を実現しますが、基本的に「ラッパー」部分を欠いています。 そのために、関連のない新しいクラスを作成し、それに新しい機能を追加することができます。 たとえば、countDigitsメソッドを使用して独自のRichIntを作成できます。 残念ながら、IntとRichIntの使用には大きな違いがあり、定義が再び破られます。
幸いなことに、 Scalaにはリッチラッパーを定義して使用する独自の方法があり、いつものように、ボイラープレートを可能な限り減らすことを目的としています。
次のセクションで詳しく見ていきましょう。
3. Scalaのリッチラッパー:ヘリコプタービュー
Scalaによって定義されたリッチラッパーを見る前に、独自のラッパーを作成する方法を見てみましょう。 数値の桁を数える方法でIntを強化したいとします。
class SimpleRichInt(wrapped: Int) {
val digits: Int = wrapped.toString.length
}
SimpleRichIntは本当にシンプルです。 Int を取り、Stringに変換して数値の桁をカウントするメンバーdigitsを定義します。 それをどのように使用できるか見てみましょう:
assert(new SimpleRichInt(105).digits == 3)
SimpleRichIntはIntを強化しましたが、明示的にインスタンス化する必要があるため、ラッパーではありません。
これをどのように解決できるか見てみましょう。
object RichIntImplicits {
implicit class RichInt(wrapped: Int) {
val digits: Int = wrapped.toString.length
}
}
上記の例では、オブジェクトRichIntImplicits内に新しいimplicitクラスRichIntを定義しました。 クラス定義に暗黙的に含まれるキーワードにより、クラスがスコープ内にある場合、クラスのプライマリコンストラクターが暗黙的な変換の対象になります。
これが、オブジェクト内で定義した理由でもあります。必要なときに簡単にインポートできるようにするためです。
import RichIntImplicits._
assert(105.digits == 3)
RichIntImplicits ._ オブジェクトをインポートすることにより、RichIntをスコープに取り込みます。 これで、新しいクラスを明示的にインスタンス化することなく、105でdigitsを呼び出すことができます。 コンパイラは、IntからRichIntへの変換を自動的に処理します。
ただし、暗黙の変換を自分で定義することで、同じ結果を得ることができます。 SimpleRichIntでそれを行う方法を見てみましょう。
object SimpleRichInt {
implicit final def SimpleRichInt(n: Int): SimpleRichInt = new SimpleRichInt(n)
}
そして、それを本当のリッチラッパーとして使用できます。
import SimpleRichInt._
assert(105.digits == 3)
4. Scalaの標準ライブラリのリッチラッパー
このセクションでは、Scalaがリッチラッパーを実装する方法について説明します。 特に、 Char、Int、、 Boolean などの一般的なタイプと、StringおよびArrayタイプについて説明します。
4.1. 一般的なタイプ
ScalaのPredefは、前のセクションで示した2番目のアプローチに従います。 これには、プログラマーがリッチラッパーを簡単に作成できるようにするいくつかの暗黙的な変換が含まれています。
最も一般的なタイプの例をいくつか見てみましょう。
@inline implicit def byteWrapper(x: Byte) = new runtime.RichByte(x)
@inline implicit def shortWrapper(x: Short) = new runtime.RichShort(x)
@inline implicit def intWrapper(x: Int) = new runtime.RichInt(x)
@inline implicit def charWrapper(c: Char) = new runtime.RichChar(c)
@inline implicit def longWrapper(x: Long) = new runtime.RichLong(x)
@inline implicit def floatWrapper(x: Float) = new runtime.RichFloat(x)
@inline implicit def doubleWrapper(x: Double) = new runtime.RichDouble(x)
@inline implicit def booleanWrapper(x: Boolean) = new runtime.RichBoolean(x)
これらの関数は、 Predef 、LowPriorityImplicitsのスーパークラスで定義されています。
Scala 2.8以降、暗黙の解決システムは優先度ベースです。 暗黙的なメソッドの2つの異なる適切な選択肢を比較すると、それぞれがより具体的な引数を持つか、サブクラスで定義されるために1つのポイントを取得します。 コンパイラーは、より多くのポイントを取得する代替案を選択します。 したがって、PredefはLowPriorityImplicitsを拡張するため、 Predef で定義された他の暗黙の変換は、LowPriorityImplicitsで定義された変換よりも優先されます。
Predefで定義されたすべての変換には、それ以上インポートせずにアクセスできます。 リッチラッパーがScalaの組み込みリッチラッパーと競合する場合、コンパイラーは、正しい暗黙の変換をインポートした場合にのみ、バージョンを選択します。 たとえば、前のセクションでは、独自のバージョンの RichInt を定義し、それを明示的にインポートして、コンパイラに必要なバージョンを通知する必要がありました。
もしそれをしていなかったら、コンパイラはPredefの暗黙の変換を適用しようとしたでしょう。 それにもかかわらず、Scalaの RichInt は桁を定義していないため、コンパイラーはエラーで失敗します。
この理論を実践して、数値を2進数および16進数の表現に簡単にフォーマットする方法を見てみましょう。
assert(3.toBinaryString == "11")
assert(10.toHexString == "a")
4.2. 弦
次に見るリッチラッパーはStringOpsです。 Predef で定義されている暗黙の変換は、前の変換と似ています。
@inline implicit def augmentString(x: String): StringOps = new StringOps(x)
@inline implicit def unaugmentString(x: StringOps): String = x.repr
このラッパーによって無料で提供されるいくつかの便利なメソッドを見てみましょう。
assert("test".reverse == "tset")
assert("test"(1) == 'e')
最初のアサーションでは、 reverse を呼び出すだけで、元の文字列の逆を取得します。 一方、2番目の例は、StringOpsが文字列内の特定の位置にある文字を返すapplyメソッドを定義する方法を示しています。
4.3. 配列
最後に、配列には独自のリッチラッパーもあります。
implicit def genericArrayOps[T](xs: Array[T]): ArrayOps[T] = (xs match {
case x: Array[AnyRef] => refArrayOps[AnyRef](x)
case x: Array[Boolean] => booleanArrayOps(x)
case x: Array[Byte] => byteArrayOps(x)
case x: Array[Char] => charArrayOps(x)
case x: Array[Double] => doubleArrayOps(x)
case x: Array[Float] => floatArrayOps(x)
case x: Array[Int] => intArrayOps(x)
case x: Array[Long] => longArrayOps(x)
case x: Array[Short] => shortArrayOps(x)
case x: Array[Unit] => unitArrayOps(x)
case null => null
}).asInstanceOf[ArrayOps[T]]
implicit def booleanArrayOps(xs: Array[Boolean]): ArrayOps.ofBoolean = new ArrayOps.ofBoolean(xs)
implicit def byteArrayOps(xs: Array[Byte]): ArrayOps.ofByte = new ArrayOps.ofByte(xs)
implicit def charArrayOps(xs: Array[Char]): ArrayOps.ofChar = new ArrayOps.ofChar(xs)
implicit def doubleArrayOps(xs: Array[Double]): ArrayOps.ofDouble = new ArrayOps.ofDouble(xs)
implicit def floatArrayOps(xs: Array[Float]): ArrayOps.ofFloat = new ArrayOps.ofFloat(xs)
implicit def intArrayOps(xs: Array[Int]): ArrayOps.ofInt = new ArrayOps.ofInt(xs)
implicit def longArrayOps(xs: Array[Long]): ArrayOps.ofLong = new ArrayOps.ofLong(xs)
implicit def refArrayOps[T <: AnyRef](xs: Array[T]): ArrayOps.ofRef[T] = new ArrayOps.ofRef[T](xs)
implicit def shortArrayOps(xs: Array[Short]): ArrayOps.ofShort = new ArrayOps.ofShort(xs)
implicit def unitArrayOps(xs: Array[Unit]): ArrayOps.ofUnit = new ArrayOps.ofUnit(xs)
Scalaはここで2つの異なる暗黙の変換を定義しています。 まず、 genericArrayOps が汎用配列( Array [T] )を処理します。 この場合、Scalaはパターンマッチングを介して実際のタイプTを解決します。
次に、タイプに基づいて、 booleanArrayOps、 T = Boolean の場合はなど、別の変換関数が呼び出されます。 ただし、booleanArrayOpsも陰関数です。 これは、配列内の要素のタイプがパターンマッチングを必要とせずにわかっている可能性があるためです。
たとえば、値のタイプが Array [Boolean] ( Array [T] ではない)の場合、関数 booleanArrayOps はより具体的であり、コンパイラによって使用されますその一般的な対応物の代わりに。
ArrayOps を使用して、配列に要素を追加したり、配列をスライスしたりする方法を見てみましょう。
assert(Array(1, 2, 3).:+(4).:+(5).sameElements(Array(1, 2, 3, 4, 5)))
assert(Array(1, 2, 3).slice(1, 2).sameElements(Array(2)))
最初の例は、いくつかの暗黙的な変換を連結する方法も示しています。 Array(1、2、3).: +(4)は Arrayを返します。これを再びArrayOpsに変換して、:+を呼び出します。 メソッドをもう一度。
5. 結論
この記事では、Scalaでリッチラッパーがどのように定義されているか、そしてそれらをプログラムでどのように使用できるかを学びました。 さらに、既存のクラスに自然な方法で機能を追加するために、独自のリッチラッパーを定義する方法についても簡単に説明しました。
いつものように、完全なソースコードはGitHubのにあります。