[JAVA] J'ai touché Scala ~ [Trate] ~

Introduction

image.png

Il s'agit d'une migration directe du didacticiel dwango, où vous allez l'étudier, le modifier et le remplacer par vos propres termes.

Trate

Nos programmes comportent souvent des dizaines de milliers de lignes, souvent des centaines de milliers ou plus. Il est difficile de les suivre tous à la fois, vous devez donc diviser votre programme en unités significatives et faciles à comprendre. De plus, ce serait bien si les pièces divisées pouvaient être assemblées de manière aussi flexible que possible pour créer un grand programme.

Le partitionnement de programme (modularisation) et l'assemblage (synthèse) sont des concepts de conception importants dans la programmation orientée objet et fonctionnelle. Et les traits sont au cœur du concept de modularisation dans la programmation orientée objet de Scala.

Dans cette section, jetons un coup d'œil aux caractéristiques des traits de Scala.

Définition Trate

Le trait Scala est comme une classe sans la possibilité de définir un constructeur, qui peut être défini grossièrement comme:


trait <Nom Trate> {
  (<Définition du champ> | <Définition de la méthode>)*
}

Les définitions de champ et les définitions de méthode ne doivent pas nécessairement avoir de corps. Le nom spécifié dans le nom du trait est défini comme le trait.

Les bases de Trate

Le trait Scala présente les caractéristiques suivantes par rapport à la classe.

Vous pouvez mélanger plusieurs traits dans une classe ou un trait Ne peut pas être instancié directement Impossible de prendre les paramètres de classe (considérer les arguments) Ci-dessous, nous présenterons chaque fonctionnalité.

Vous pouvez mélanger plusieurs traits dans une classe ou un trait Contrairement aux classes, les traits Scala vous permettent de mélanger plusieurs traits en une seule classe ou trait.


trait TraitA

trait TraitB

class ClassA

class ClassB

//Peut être compilé
class ClassC extends ClassA with TraitA with TraitB

scala> //Erreur de compilation!
     | 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
                                        ^

Dans l'exemple ci-dessus, vous pouvez créer ClassC qui hérite de ClassA, TraitA et TraitB, mais vous ne pouvez pas créer ClassD qui hérite de ClassA et ClassB. J'obtiens le message d'erreur "la classe ClassB doit être un trait à mélanger", ce qui signifie "doit être des traits à mélanger dans ClassB". Si vous souhaitez hériter de plusieurs classes, créez les traits des classes.

Ne peut pas être instancié directement

Les traits Scala, contrairement aux classes, ne peuvent pas être instanciés directement.


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

C'est une limitation car le trait n'est pas censé être utilisé seul en premier lieu. Lorsque vous utilisez un trait, vous créez généralement une classe qui en hérite.



trait TraitA

class ClassA extends TraitA

object ObjectA {
  //Peut être instancié en en faisant une classe
  val a = new ClassA

}

De plus, en utilisant la notation new Trait {}, il semble que le trait puisse être instancié, mais comme il s'agit d'une syntaxe qui crée une classe anonyme qui hérite du Trait et crée cette instance, le trait lui-même est utilisé. Il n'est pas instancié.

Impossible de prendre les paramètres de classe (considérer les arguments)

Contrairement aux classes, les traits Scala ont la limitation qu'ils ne peuvent pas prendre de paramètres (considérez les arguments) 1.


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

Ce n'est pas trop un problème non plus. Vous pouvez transmettre une valeur en attribuant au trait un membre abstrait. Vous pouvez transmettre des valeurs aux traits en laissant la classe hériter comme vous le feriez dans un problème qui ne peut pas être instancié, ou en implémentant un membre abstrait au moment de l'instanciation.


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

//Faites-en une classe et écrasez le nom
class ClassA(val name: String) extends TraitA

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

  //Vous pouvez donner une implémentation qui écrase le nom
  val a2 = new TraitA { val name = "kadokawa" }
}

Comme vous pouvez le voir, les restrictions de traits ne sont pas vraiment un problème dans la pratique, et elles peuvent être utilisées de la même manière que les classes à d'autres égards. En d'autres termes, vous pouvez faire pratiquement la même chose que l'héritage multiple. Et le mélange de caractéristiques apporte de grands avantages à la modularité. Habitons-nous-y.

À propos du terme «trait»

Cette section utilise des termes orientés objet tels que les traits et les mélanges, mais sachez qu'ils peuvent avoir des significations légèrement différentes de celles utilisées dans d'autres langues.

Les traits sont basés sur l'article "Traits: Composable Units of Behavior" adopté par Schärli et al. Dans l'ECOOP 2003, mais la définition des traits et les spécifications des traits Scala dans cet article sont basées sur le comportement pendant la synthèse. , La gestion des variables d'état, etc., semble différente.

Cependant, les termes trait et mix-in varient d'une langue à l'autre, et la documentation officielle de Scala à laquelle nous nous référons et Scala Scalable Programming utilisent également l'expression «traits de mix-in» ici. Alors j'aimerais l'imiter.

Diverses caractéristiques du trait

[Problème d'héritage Rhombus]

Comme nous l'avons vu ci-dessus, les traits sont utiles car ils ont des fonctionnalités de classe mais peuvent effectivement être hérités de plusieurs fois, mais il y a une chose à considérer. C'est le "problème d'héritage du diamant" auquel sont confrontés les langages de programmation à héritage multiple.

Considérez la relation d'héritage suivante. TraitA, qui définit la méthode de salutation, TraitB et TraitC, qui implémentent saluer, et ClassA, qui hérite à la fois de TraitB et de TraitC.


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

Les implémentations TraitB et TraitC de la méthode greet sont en conflit. Que doit faire l'accueil de classe A dans ce cas? La méthode de salutation de TraitB doit-elle être exécutée ou la méthode de salutation de TraitC doit-elle être exécutée? Tout langage qui prend en charge l'héritage multiple a ce problème d'ambiguïté et doit être résolu.

À propos, lorsque je compile l'exemple ci-dessus avec Scala, j'obtiens l'erreur suivante.


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
             ^

Dans Scala, si le remplacement n'est pas spécifié, un conflit de définition de méthode entraînera une erreur.

Une solution dans ce cas est de remplacer le message d'accueil avec la classe A, comme le dit l'erreur de compilation "Remarque: cela peut être résolu en déclarant un remplacement dans la classe A."


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

À ce stade, vous pouvez également spécifier la méthode de TraitB ou TraitC et l'utiliser en appelant la méthode en spécifiant le type de super dans ClassA.



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

Le résultat de l'exécution est le suivant.


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

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

Mais que faire si vous souhaitez appeler à la fois les méthodes TraitB et TraitC? Une façon est d'appeler explicitement les classes TraitB et TraitC comme ci-dessus.


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

Cependant, il est difficile de tout appeler explicitement lorsque la relation d'héritage se complique. Il existe également des méthodes qui sont toujours appelées, telles que les constructeurs.

Les traits de Scala ont une fonction appelée "linéarisation" pour résoudre ce problème.

Linéarisation

La fonction de linéarisation des traits de Scala traite l'ordre dans lequel les traits sont mélangés comme l'ordre d'héritage des traits.

Ensuite, considérons l'exemple suivant. La différence avec l'exemple précédent est que les définitions de méthode de traitement pour TraitB et TraitC ont le modificateur de substitution.


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

Dans ce cas, aucune erreur de compilation ne se produira. Alors, que voyez-vous exactement lorsque vous appelez la méthode de salutation de ClassA? Exécutons-le réellement.


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

L'appel à la méthode de salutation de ClassA a exécuté la méthode de salutation de TraitC. C'est parce que l'ordre d'héritage des traits est linéarisé et que le trait C mélangé plus tard a la priorité. En d'autres termes, si vous inversez l'ordre des mélanges de traits, le trait B prévaudra. Essayez de changer l'ordre de mixage comme suit.


class ClassB extends TraitC with TraitB
Ensuite, la méthode de salutation de la classeB est appelée, et cette fois la méthode de salutation de TraitB est exécutée.

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

Vous pouvez également utiliser un trait parent linéarisé en utilisant super

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

Le résultat de cette méthode de salutation change également dans l'ordre d'héritage.


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.

La fonction de linéarisation facilite le rappel du traitement de tous les traits mixtes. Le processus d'empilement des traits par linéarisation est parfois appelé Trait empilable dans la terminologie de Scala.

Cette linéarisation est la solution au problème d'héritage des diamants de Scala.

Pièges: ordre d'initialisation des traits

L'ordre d'initialisation des vals pour les traits Scala est un piège majeur dans l'utilisation des traits. Prenons l'exemple suivant. Nous déclarons la variable foo dans le trait A, le trait B utilise foo pour créer la variable bar, attribue une valeur à foo dans la classe C, puis utilise 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)
}

Appelons la méthode printBar de classe C dans REPL.


scala> (new C).printBar()
nullWorld

Il est affiché sous la forme nullWorld. Il semble que la valeur assignée à foo dans la classe C ne soit pas reflétée. La raison pour laquelle cela se produit est que les classes et les traits de Scala sont initialisés dans l'ordre à partir de la superclasse. Dans cet exemple, la classe C hérite du trait B et le trait B hérite du trait A. En d'autres termes, l'initialisation est nulle car le trait A est exécuté en premier, la variable foo est déclarée et rien n'est affecté au contenu. Ensuite, la barre de variables est déclarée dans le trait B, et la chaîne "nullWorld" est créée à partir du toto nul et des chaînes "World" et assignée à la barre de variables. Il s'agit de la chaîne de caractères affichée précédemment.

Comment éviter l'ordre d'initialisation du trait val

Alors, comment éviter ce piège? Dans l'exemple ci-dessus, retarder l'initialisation de bar pour que foo soit correctement initialisé avant de l'utiliser. Utilisez lazy val ou def pour retarder le traitement.

Regardons le code spécifique.


trait A {
  val foo: String
}

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

class C extends B {
  val foo = "Hello"

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

Contrairement à l'exemple précédent où nullWorld était affiché, lazy val est maintenant utilisé pour initialiser la barre. Cela retardera l'initialisation de la barre jusqu'à ce qu'elle soit réellement utilisée. En attendant, foo est initialisé dans la classe C, donc foo avant l'initialisation n'est pas utilisé.

Cette fois, même si j'appelle la méthode printBar de la classe C, HelloWorld s'affiche correctement.


scala> (new C).printBar()
HelloWorld

lazy val est un peu plus lourd que val et peut provoquer des blocages sur des appels complexes. Le problème est que si vous utilisez def au lieu de val, la valeur sera calculée à chaque fois. Cependant, les deux ne sont souvent pas un gros problème, alors pensez à utiliser lazy val ou def, surtout si vous souhaitez utiliser la valeur de val pour créer la valeur de val.

Une autre façon d'éviter l'ordre d'initialisation des valeurs de trait est d'utiliser les premières définitions. La prédéfinition est une méthode d'initialisation des champs avant la superclasse.


trait A {
  val foo: String
}

trait B extends A {
  val bar = foo + "World" //Vous pouvez le laisser comme val
}

class C extends {
  val foo = "Hello" //Appelé avant l'initialisation de la superclasse
} with B {
  def printBar(): Unit = println(bar)
}

Même si j'appelle la printBar de C ci-dessus, HelloWorld s'affiche correctement.

Cette pré-définition est une solution de contournement du côté de l'utilisateur, mais dans cet exemple, il y a un problème avec le trait B (des problèmes d'initialisation se produisent lorsqu'il est utilisé normalement), donc le trait B a été corrigé. C'est peut-être mieux.

Cette fonctionnalité prédéfinie peut ne pas être très courante dans le code réel, car il est souvent préférable de résoudre les problèmes d'initialisation de trait du côté du trait hérité.

fin

La prochaine fois, nous étudierons les paramètres de type et les spécifications de déplacement.

référence

Ce document est CC BY-NC-SA 3.0

image.png Il est distribué sous.

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

Recommended Posts

J'ai touché Scala ~ [Trate] ~
J'ai touché Scala
J'ai touché Scala ~ [Classe] ~
J'ai touché Scala ~ [Objet] ~
J'ai touché Scala ~ [Syntaxe de contrôle] ~
J'ai d'abord touché Java ②
J'ai d'abord touché Java ③
J'ai d'abord touché Java ④
J'ai touché Scala ~ [Paramètres de type et spécification de déplacement] ~
J'ai d'abord touché Java
Je suis allé à Scala Fukuoka 2019!
J'ai créé un serveur écologique avec scala
Notez que j'ai légèrement touché SQLiteOpenHelper sur Android
J'ai touché ODM for Developer ② Développement de règles