(パフォーマンスは別として) 特性の代わりに抽象クラスを使用する利点は何ですか? ほとんどの場合、抽象クラスは特性に置き換えることができるようです。
8 に答える
私は2つの違いを考えることができます
- 抽象クラスには、コンストラクター パラメーターと型パラメーターを含めることができます。特性は型パラメーターのみを持つことができます。将来的には、トレイトでさえコンストラクターパラメーターを持つことができるといういくつかの議論がありました
- 抽象クラスは、Java と完全に相互運用できます。ラッパーなしで Java コードから呼び出すことができます。特性は、実装コードが含まれていない場合にのみ完全に相互運用可能です
Programming in Scala には、「To trait または not to trait?」というセクションがあります。これはこの質問に対処します。第 1 版はオンラインで入手できるので、ここですべてを引用してもよろしいかと思います。(本格的な Scala プログラマーは本を購入する必要があります):
再利用可能な動作のコレクションを実装するときはいつでも、トレイトを使用するか抽象クラスを使用するかを決定する必要があります。明確なルールはありませんが、このセクションには考慮すべきガイドラインがいくつか含まれています。
動作が再利用されない場合は、具象クラスにします。結局のところ、再利用可能な動作ではありません。
無関係な複数のクラスで再利用される可能性がある場合は、特性にします。クラス階層のさまざまな部分に混在できるのは、特性だけです。
Java コードで継承する場合は、抽象クラスを使用します。コードを含むトレイトには Java に類似したものがないため、Java クラスのトレイトから継承するのは扱いにくい傾向があります。一方、Scala クラスからの継承は、Java クラスからの継承とまったく同じです。1 つの例外として、抽象メンバーのみを持つ Scala トレイトは Java インターフェースに直接変換されるため、Java コードがそれを継承すると予想される場合でも、そのようなトレイトを自由に定義する必要があります。Java と Scala を一緒に使用する方法の詳細については、第 29 章を参照してください。
コンパイル済みの形式で配布する予定があり、外部のグループがそれを継承するクラスを作成することが予想される場合は、抽象クラスの使用に傾倒する可能性があります。問題は、特性がメンバーを獲得または喪失した場合、それを継承するクラスは、変更されていなくても再コンパイルする必要があることです。外部のクライアントが動作を継承するのではなく、呼び出しのみを行う場合は、トレイトを使用しても問題ありません。
効率が非常に重要な場合は、クラスの使用に傾倒してください。ほとんどの Java ランタイムは、クラス メンバーの仮想メソッド呼び出しを、インターフェイス メソッド呼び出しよりも高速な操作にします。トレイトはインターフェイスにコンパイルされるため、わずかなパフォーマンス オーバーヘッドが発生する可能性があります。ただし、この選択は、問題の特性がパフォーマンスのボトルネックになることがわかっており、代わりにクラスを使用することで実際に問題が解決されるという証拠がある場合にのみ行う必要があります。
それでもわからない場合は、上記を考慮した上で、特性として作成することから始めます。後でいつでも変更できます。一般に、特性を使用すると、より多くのオプションが開かれます。
@Mushtaq Ahmed が述べたように、特性はクラスのプライマリ コンストラクターに渡されるパラメーターを持つことはできません。
もう 1 つの違いは、 の処理ですsuper
。
クラスとトレイトのその他の違いは、クラスで
super
は呼び出しが静的にバインドされるのに対し、トレイトでは動的にバインドされることです。クラスで記述super.toString
すると、どのメソッド実装が呼び出されるかが正確にわかります。ただし、同じことをトレイトに記述すると、トレイトを定義するときに、スーパー コールに対して呼び出すメソッドの実装が未定義になります。
詳細については、第 12 章の残りを参照してください。
編集 1 (2013):
抽象クラスがトレイトと比較して振る舞う方法には微妙な違いがあります。線形化ルールの 1 つは、クラスの継承階層を保持することです。これは、チェーンの後半に抽象クラスをプッシュする傾向がありますが、特性をうまく混在させることができます。特定の状況では、クラスの線形化の後の位置にいることが実際には望ましいです。 、そのために抽象クラスを使用できます。Scala でのクラスの線形化 (ミックスイン順序) の制約を参照してください。
編集 2 (2018):
Scala 2.12 の時点で、trait のバイナリ互換動作が変更されました。2.12 より前では、メンバーをトレイトに追加または削除するには、クラスが変更されていなくても、トレイトを継承するすべてのクラスを再コンパイルする必要がありました。これは、特性が JVM でエンコードされた方法によるものです。
Scala 2.12 の時点で、トレイトは Java インターフェイスにコンパイルされるため、要件が少し緩和されました。トレイトが次のいずれかを行う場合、そのサブクラスには再コンパイルが必要です。
- フィールドの定義 (
val
またはvar
、ただし定数は問題ありません –final val
結果の型なし)- 呼び出し
super
- 本体の初期化ステートメント
- クラスの拡張
- 適切なスーパートレイトの実装を見つけるために線形化に頼る
ただし、トレイトがそうでない場合は、バイナリ互換性を損なうことなく更新できるようになりました。
何にせよ、Odersky らのProgramming in Scalaは、疑問がある場合はトレイトを使用することを推奨しています。必要に応じて、後でいつでもそれらを抽象クラスに変更できます。
複数の抽象クラスを直接拡張することはできませんが、複数のトレイトをクラスに混在させることができるという事実を除けば、トレイト内のスーパー呼び出しは動的にバインドされるため、トレイトはスタック可能であることに言及する価値があります (以前に混合されたクラスまたはトレイトを参照しています)。現在のもの)。
Abstract Class と Trait の違いにおける Thomas の回答から:
trait A{
def a = 1
}
trait X extends A{
override def a = {
println("X")
super.a
}
}
trait Y extends A{
override def a = {
println("Y")
super.a
}
}
scala> val xy = new AnyRef with X with Y
xy: java.lang.Object with X with Y = $anon$1@6e9b6a
scala> xy.a
Y
X
res0: Int = 1
scala> val yx = new AnyRef with Y with X
yx: java.lang.Object with Y with X = $anon$1@188c838
scala> yx.a
X
Y
res1: Int = 1
抽象クラスを拡張する場合、これはサブクラスが同様の種類であることを示しています。これは、特性を使用する場合には必ずしも当てはまらないと思います。
プログラミング Scalaでは、著者は、抽象クラスは古典的なオブジェクト指向の「is-a」関係を作り、特性はスカラ方式の構成であると述べています。
抽象クラスには動作を含めることができます - コンストラクターの引数 (これはトレイトではできません) でパラメーター化でき、作業エンティティを表すことができます。代わりに、特性は 1 つの機能、1 つの機能のインターフェイスを表すだけです。