[JAVA] Ich habe Scala ~ [Trate] ~ berührt

Introduction

image.png

Es ist eine direkte Migration des Dwango-Tutorials, in dem Sie es studieren, bearbeiten und durch Ihre eigenen Begriffe ersetzen.

Trate

Unsere Programme haben oft Zehntausende von Zeilen, oft Hunderttausende oder mehr. Es ist schwierig, alle auf einmal im Auge zu behalten, daher müssen Sie Ihr Programm in aussagekräftige und leicht verständliche Einheiten aufteilen. Außerdem wäre es schön, wenn die geteilten Teile so flexibel wie möglich zusammengesetzt werden könnten, um ein großes Programm zu erstellen.

Programmpartitionierung (Modularisierung) und Assemblierung (Synthese) sind wichtige Entwurfskonzepte sowohl in der objektorientierten als auch in der funktionalen Programmierung. Und Merkmale spielen eine zentrale Rolle für das Modularisierungskonzept in Scalas objektorientierter Programmierung.

In diesem Abschnitt werfen wir einen Blick auf die Merkmale von Scalas Merkmalen.

Trate Definition

Das Scala-Merkmal ist wie das Entfernen der Fähigkeit, einen Konstruktor aus einer Klasse zu definieren, die grob definiert werden kann als:


trait <Trate Name> {
  (<Felddefinition> | <Methodendefinition>)*
}

Felddefinitionen und Methodendefinitionen müssen keinen Body haben. Der im Merkmalnamen angegebene Name wird als Merkmal definiert.

Trate Grundlagen

Das Scala-Merkmal weist im Vergleich zur Klasse die folgenden Merkmale auf.

Sie können mehrere Merkmale in eine Klasse oder ein Merkmal mischen Kann nicht direkt instanziiert werden Klassenparameter können nicht übernommen werden (Argumente berücksichtigen) Im Folgenden werden wir jede Funktion vorstellen.

Sie können mehrere Merkmale in eine Klasse oder ein Merkmal mischen Im Gegensatz zu Klassen können Sie mit Scala-Merkmalen mehrere Merkmale zu einer Klasse oder einem Merkmal mischen.


trait TraitA

trait TraitB

class ClassA

class ClassB

//Kann kompiliert werden
class ClassC extends ClassA with TraitA with TraitB

scala> //Kompilierungsfehler!
     | class ClassD extends ClassA with ClassB
<console>:15: error: class ClassB needs to be a trait to be mixed in
       class ClassD extends ClassA with ClassB
                                        ^

Im obigen Beispiel können Sie ClassC erstellen, das ClassA, TraitA und TraitB erbt, aber Sie können keine ClassD erstellen, die ClassA und ClassB erbt. Ich erhalte die Fehlermeldung "Klasse ClassB muss ein Merkmal sein, in das gemischt werden soll", was bedeutet, "muss Merkmale sein, die in Klasse B gemischt werden müssen". Wenn Sie mehrere Klassen erben möchten, legen Sie die Klassenmerkmale fest.

Kann nicht direkt instanziiert werden

Scala-Merkmale können im Gegensatz zu Klassen nicht direkt instanziiert werden.


scala> trait TraitA
defined trait TraitA
scala> object ObjectA {
     |   //Kompilierungsfehler!
     |   val a = new TraitA
     | }
<console>:15: error: trait TraitA is abstract; cannot be instantiated
         val a = new TraitA
                 ^

Dies ist eine Einschränkung, da das Merkmal nicht in erster Linie allein verwendet werden soll. Wenn Sie ein Merkmal verwenden, erstellen Sie normalerweise eine Klasse, die es erbt.



trait TraitA

class ClassA extends TraitA

object ObjectA {
  //Kann instanziiert werden, indem man es zu einer Klasse macht
  val a = new ClassA

}

Unter Verwendung der Notation new Trait {} scheint es außerdem möglich zu sein, das Merkmal zu instanziieren. Da dies jedoch eine Syntax ist, die eine anonyme Klasse erstellt, die das Merkmal erbt und diese Instanz erstellt, wird das Merkmal selbst verwendet. Es wird nicht instanziiert.

Klassenparameter können nicht übernommen werden (Argumente berücksichtigen)

Im Gegensatz zu Klassen haben Scala-Merkmale die Einschränkung, dass sie keine Parameter annehmen können (Argumente berücksichtigen) 1.


//Programm korrigieren
class ClassA(name: String) {
  def printName() = println(name)
}
scala> //Kompilierungsfehler!
     | trait TraitA(name: String)
<console>:3: error: traits or objects may not have parameters
       trait TraitA(name: String)
                   ^

Dies ist auch kein allzu großes Problem. Sie können einen Wert übergeben, indem Sie dem Merkmal ein abstraktes Mitglied geben. Sie können Werte an Merkmale übergeben, indem Sie die Klasse wie bei einem Problem erben lassen, das nicht instanziiert werden kann, oder indem Sie zum Zeitpunkt der Instanziierung ein abstraktes Element implementieren.


trait TraitA {
  val name: String
  def printName(): Unit = println(name)
}

//Machen Sie es zu einer Klasse und überschreiben Sie den Namen
class ClassA(val name: String) extends TraitA

object ObjectA {
  val a = new ClassA("dwango")

  //Sie können eine Implementierung angeben, die den Namen überschreibt
  val a2 = new TraitA { val name = "kadokawa" }
}

Wie Sie sehen können, sind Merkmalsbeschränkungen in der Praxis kein wirkliches Problem, und sie können auf die gleiche Weise wie Klassen in anderer Hinsicht verwendet werden. Mit anderen Worten, Sie können praktisch dasselbe tun wie Mehrfachvererbung. Und das Trait-Mix-In bringt große Vorteile für die Modularität. Gewöhnen wir uns daran.

Über den Begriff "Merkmal"

In diesem Abschnitt werden objektorientierte Begriffe wie Merkmale und Mix-Ins verwendet. Beachten Sie jedoch, dass diese möglicherweise geringfügig andere Bedeutungen haben als die in anderen Sprachen verwendeten.

Die Merkmale basieren auf dem von Schärli et al. In der ECOOP 2003 angenommenen Artikel "Merkmale: Zusammensetzbare Verhaltenseinheiten". Die Definition der Merkmale und die Spezifikationen der Scala-Merkmale in diesem Artikel basieren jedoch auf dem Verhalten während der Synthese. Der Umgang mit Zustandsvariablen usw. sieht anders aus.

Die Begriffe Merkmal und Einmischung variieren jedoch von Sprache zu Sprache, und in der offiziellen Scala-Dokumentation, auf die wir verweisen, und in Scala Scalable Programming wird hier auch der Ausdruck "Einmischmerkmale" verwendet. Dann möchte ich es nachahmen.

Verschiedene Merkmale des Merkmals

[Rhombus-Vererbungsproblem]

Wie wir oben gesehen haben, sind Merkmale nützlich, weil sie klassenähnliche Funktionen haben, aber effektiv mehrfach vererbt werden können, aber es gibt eine Sache zu beachten. Dies ist das "Diamantvererbungsproblem", mit dem Programmiersprachen mit Mehrfachvererbung konfrontiert sind.

Betrachten Sie die folgende Vererbungsbeziehung. TraitA, das die Greet-Methode definiert, TraitB und TraitC, die Greet implementieren, und ClassA, das sowohl TraitB als auch TraitC erbt.


trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  def greet(): Unit = println("Good morning!")
}

trait TraitC extends TraitA {
  def greet(): Unit = println("Good evening!")
}
class ClassA extends TraitB with TraitC

TraitB- und TraitC-Implementierungen der Greet-Methode stehen in Konflikt. Was sollte die Begrüßung der Klasse A in diesem Fall tun? Sollte die Begrüßungsmethode von TraitB ausgeführt werden oder sollte die Begrüßungsmethode von TraitC ausgeführt werden? Jede Sprache, die Mehrfachvererbung unterstützt, weist dieses Mehrdeutigkeitsproblem auf und muss behoben werden.

Übrigens, wenn ich das obige Beispiel mit Scala kompiliere, erhalte ich den folgenden Fehler.


scala> class ClassA extends TraitB with TraitC
<console>:14: error: class ClassA inherits conflicting members:
  method greet in trait TraitB of type ()Unit  and
  method greet in trait TraitC of type ()Unit
(Note: this can be resolved by declaring an override in class ClassA.)
       class ClassA extends TraitB with TraitC
             ^

Wenn in Scala keine Überschreibung angegeben ist, führt ein Methodendefinitionskonflikt zu einem Fehler.

Eine Lösung in diesem Fall besteht darin, die Begrüßung mit Klasse A zu überschreiben, da im Kompilierungsfehler "Hinweis: Dies kann durch Deklarieren einer Überschreibung in Klasse A behoben werden."


class ClassA extends TraitB with TraitC {
  override def greet(): Unit = println("How are you?")
}

Zu diesem Zeitpunkt können Sie auch die Methode von TraitB oder TraitC angeben und sie verwenden, indem Sie die Methode aufrufen, indem Sie den Typ für super in ClassA angeben.



class ClassB extends TraitB with TraitC {
  override def greet(): Unit = super[TraitB].greet()
}

Das Ausführungsergebnis ist wie folgt.


scala> (new ClassA).greet()
How are you?

scala> (new ClassB).greet()
Good morning!

Aber was ist, wenn Sie sowohl TraitB- als auch TraitC-Methoden aufrufen möchten? Eine Möglichkeit besteht darin, die TraitB- und TraitC-Klassen wie oben explizit aufzurufen.


class ClassA extends TraitB with TraitC {
  override def greet(): Unit = {
    super[TraitB].greet()
    super[TraitC].greet()
  }
}

Es ist jedoch schwierig, alles explizit aufzurufen, wenn die Vererbungsbeziehung kompliziert wird. Es gibt auch Methoden, die immer aufgerufen werden, z. B. Konstruktoren.

Scalas Eigenschaften haben eine Funktion namens "Linearisierung", um dieses Problem zu lösen.

Linearisierung

Die Funktion zur Linearisierung von Merkmalen von Scala behandelt die Reihenfolge, in der Merkmale gemischt werden, als Reihenfolge der Vererbung von Merkmalen.

Betrachten Sie als nächstes das folgende Beispiel. Der Unterschied zum vorherigen Beispiel besteht darin, dass die Definitionen der Behandlungsmethoden für TraitB und TraitC den Überschreibungsmodifikator haben.


trait TraitA {
  def greet(): Unit
}

trait TraitB extends TraitA {
  override def greet(): Unit = println("Good morning!")
}

trait TraitC extends TraitA {
  override def greet(): Unit = println("Good evening!")
}



class ClassA extends TraitB with TraitC

In diesem Fall tritt kein Kompilierungsfehler auf. Was genau sehen Sie, wenn Sie die Greet-Methode von ClassA aufrufen? Lassen Sie es uns tatsächlich ausführen.


scala> (new ClassA).greet()
Good evening!

Der Aufruf der Greet-Methode von ClassA führte die Greet-Methode von TraitC aus. Dies liegt daran, dass die Vererbungsreihenfolge der Merkmale linearisiert ist und das später eingemischte Merkmal C Vorrang hat. Mit anderen Worten, wenn Sie die Reihenfolge der Trait-Mix-Ins umkehren, hat Trait B Vorrang. Versuchen Sie, die Einmischreihenfolge wie folgt zu ändern.


class ClassB extends TraitC with TraitB
Dann wird die Begrüßungsmethode von classB aufgerufen, und diesmal wird die Begrüßungsmethode von TraitB ausgeführt.

scala> (new ClassB).greet()
Good morning!

Sie können auch ein linearisiertes Elternmerkmal verwenden, indem Sie super verwenden

trait TraitA {
  def greet(): Unit = println("Hello!")
}

trait TraitB extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("My name is Terebi-chan.")
  }
}

trait TraitC extends TraitA {
  override def greet(): Unit = {
    super.greet()
    println("I like niconico.")
  }
}

class ClassA extends TraitB with TraitC
class ClassB extends TraitC with TraitB

Das Ergebnis dieser Begrüßungsmethode ändert sich auch in der Reihenfolge der Vererbung.


scala> (new ClassA).greet()
Hello!
My name is Terebi-chan.
I like niconico.

scala> (new ClassB).greet()
Hello!
I like niconico.
My name is Terebi-chan.

Die Linearisierungsfunktion erleichtert das Abrufen der Verarbeitung aller eingemischten Merkmale. Der Prozess des Stapelns von Merkmalen durch eine solche Linearisierung wird in Scalas Terminologie manchmal als stapelbares Merkmal bezeichnet.

Diese Linearisierung ist die Lösung für das Diamantvererbungsproblem von Scala.

Fallstricke: Reihenfolge der Merkmalsinitialisierung

Die Val-Initialisierungsreihenfolge für Scala-Merkmale ist eine große Gefahr bei der Verwendung von Merkmalen. Betrachten Sie das folgende Beispiel. Wir deklarieren die Variable foo in Merkmal A, Merkmal B verwendet foo, um die Variablenleiste zu erstellen, weist foo in Klasse C einen Wert zu und verwendet dann bar.


trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World"
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

Rufen wir die printBar-Methode der Klasse C in REPL auf.


scala> (new C).printBar()
nullWorld

Es wird als nullWorld angezeigt. Es scheint, dass der Wert, der foo in Klasse C zugewiesen wurde, nicht wiedergegeben wird. Der Grund dafür ist, dass Scalas Klassen und Merkmale in der Reihenfolge der Oberklasse initialisiert werden. In diesem Beispiel erbt Klasse C Merkmal B und Merkmal B Merkmal A. Mit anderen Worten, die Initialisierung ist null, da Merkmal A zuerst ausgeführt wird, die Variable foo deklariert wird und dem Inhalt nichts zugewiesen wird. Als nächstes wird die Variablenleiste in Merkmal B deklariert und die Zeichenfolge "nullWorld" aus dem Null-Foo und den Zeichenfolgen "World" erstellt und der Variablenleiste zugewiesen. Dies ist die zuvor angezeigte Zeichenfolge.

So vermeiden Sie die Initialisierungsreihenfolge von Trait Val

Wie kann diese Falle vermieden werden? Im obigen Beispiel wird die Initialisierung des Balkens verzögert, sodass foo vor der Verwendung ordnungsgemäß initialisiert wird. Verwenden Sie Lazy Val oder Def, um die Verarbeitung zu verzögern.

Schauen wir uns den spezifischen Code an.


trait A {
  val foo: String
}

trait B extends A {
  lazy val bar = foo + "World" //Oder def bar
}

class C extends B {
  val foo = "Hello"

  def printBar(): Unit = println(bar)
}

Im Gegensatz zum vorherigen Beispiel, in dem nullWorld angezeigt wurde, wird Lazy Val jetzt zum Initialisieren des Balkens verwendet. Dies verzögert die Initialisierung des Balkens, bis er tatsächlich verwendet wird. In der Zwischenzeit wird foo in Klasse C initialisiert, daher wird foo vor der Initialisierung nicht verwendet.

Dieses Mal wird HelloWorld korrekt angezeigt, auch wenn ich die printBar-Methode der Klasse C aufrufe.


scala> (new C).printBar()
HelloWorld

Lazy Val ist etwas schwerer als Val und kann bei komplexen Anrufen zu Deadlocks führen. Das Problem ist, dass der Wert jedes Mal berechnet wird, wenn Sie def anstelle von val verwenden. Beides sind jedoch häufig keine großen Probleme. Verwenden Sie daher Lazy Val oder Def, insbesondere wenn Sie den Wert von Val verwenden möchten, um den Wert von Val zu erstellen.

Eine andere Möglichkeit, die Reihenfolge der Initialisierung des Merkmalswerts zu vermeiden, besteht in der Verwendung früher Definitionen. Die Vordefinition ist eine Methode zum Initialisieren von Feldern vor der Oberklasse.


trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World" //Sie können es als val belassen
}

class C extends {
  val foo = "Hello" //Wird vor der Superklasseninitialisierung aufgerufen
} with B {
  def printBar(): Unit = println(bar)
}

Auch wenn ich oben die printBar von C aufrufe, wird HelloWorld korrekt angezeigt.

Diese Vordefinition ist eine Problemumgehung von Seiten des Benutzers. In diesem Beispiel gibt es jedoch ein Problem mit Merkmal B (Initialisierungsprobleme treten bei normaler Verwendung auf), sodass Merkmal B korrigiert wurde. Es kann besser sein.

Diese vordefinierte Funktion ist im realen Code möglicherweise nicht sehr häufig, da Probleme bei der Initialisierung von Merkmalen auf der Seite der geerbten Merkmale häufig besser gelöst werden.

Ende

Nächstes Mal werden wir Typparameter und Verschiebungsspezifikationen untersuchen.

Referenz

Dieses Dokument ist CC BY-NC-SA 3.0

image.png Es wird unter verteilt.

https://dwango.github.io/scala_text/

Recommended Posts

Ich habe Scala ~ [Trate] ~ berührt
Ich habe Scala berührt
Ich habe Scala ~ [Klasse] ~ berührt
Ich habe Scala ~ [Objekt] ~ berührt
Ich habe Scala ~ [Steuerungssyntax] ~ berührt
Ich habe zuerst Java touched berührt
Ich habe zuerst Java touched berührt
Ich habe zuerst Java touched berührt
Ich habe Scala ~ [Typparameter und Verschiebungsspezifikation] ~ berührt
Ich habe zuerst Java berührt
Ich ging zur Scala Fukuoka 2019!
Ich habe einen Öko-Server mit Scala gemacht
Beachten Sie, dass ich SQLiteOpenHelper unter Android leicht berührt habe
Ich habe ODM für Entwickler ② Regelentwicklung berührt