Ich kann in den API-Dokumenten für Predef sehen, dass sie Unterklassen eines generischen Funktionstyps (From) => To sind, aber das ist alles, was es sagt. Ähm, was? Vielleicht gibt es irgendwo eine Dokumentation, aber Suchmaschinen verarbeiten "Namen" wie "<: <" nicht sehr gut, sodass ich sie nicht finden konnte.
Folgefrage: Wann sollte ich diese funky Symbole/Klassen verwenden und warum?
Diese werden als generalisierte Typeinschränkungen bezeichnet. Sie ermöglichen es Ihnen, innerhalb einer typparametrisierten Klasse oder Eigenschaft einen ihrer Typparameter zu weiter einzugrenzen. Hier ist ein Beispiel:
case class Foo[A](a:A) { // 'A' can be substituted with any type
// getStringLength can only be used if this is a Foo[String]
def getStringLength(implicit evidence: A =:= String) = a.length
}
Das implizite Argument evidence
wird vom Compiler geliefert, wenn A
String
ist. Sie können sich das als Beweis vorstellen, dass A
String
ist - das Argument selbst ist nicht wichtig, nur weil Sie wissen, dass es existiert. [edit: naja, technisch gesehen ist es wichtig, weil es eine implizite Konvertierung von A
in String
darstellt, mit der Sie a.length
aufrufen können und nicht soll der Compiler dich anschreien]
Jetzt kann ich es so benutzen:
scala> Foo("blah").getStringLength
res6: Int = 4
Aber wenn ich versucht habe, es mit einem Foo
zu verwenden, das etwas anderes als ein String
enthält:
scala> Foo(123).getStringLength
<console>:9: error: could not find implicit value for parameter evidence: =:=[Int,String]
Sie können diesen Fehler als "konnte keinen Beweis für Int == String finden" lesen ... das ist so, wie es sein sollte! getStringLength
legt weitere Einschränkungen für den Typ von A
fest, als das, was Foo
im Allgemeinen erfordert; Sie können nämlich nur getStringLength
für einen Foo[String]
aufrufen. Diese Einschränkung wird zur Kompilierungszeit erzwungen, was cool ist!
<:<
Und <%<
Funktionieren ähnlich, jedoch mit geringfügigen Abweichungen:
A =:= B
Bedeutet, dass A genau B sein mussA <:< B
Bedeutet, dass A ein Subtyp von B sein muss (analog zur simple Typ-Einschränkung <:
)A <%< B
Bedeutet, dass A einsehbar sein muss als B, möglicherweise über implizite Konvertierung (analog zur einfachen Typbedingung <%
)Dieses Snippet von @retronym ist eine gute Erklärung dafür, wie so etwas früher ausgeführt wurde und wie es jetzt durch verallgemeinerte Typbeschränkungen einfacher wird.
[~ # ~] Nachtrag [~ # ~]
Um Ihre Anschlussfrage zu beantworten, ist das Beispiel, das ich gegeben habe, zugegebenermaßen ziemlich ausgeklügelt und offensichtlich nicht nützlich. Stellen Sie sich vor, Sie definieren damit so etwas wie eine List.sumInts
- Methode, die eine Liste von ganzen Zahlen zusammenfasst. Sie möchten nicht zulassen, dass diese Methode für ein altes List
aufgerufen wird, nur für ein List[Int]
. Der Konstruktor vom Typ List
kann jedoch nicht so eingeschränkt werden. Sie möchten immer noch in der Lage sein, Listen mit Strings, Foos, Bars und Dingsbums zu haben. Wenn Sie also sumInts
eine allgemeine Typeinschränkung zuweisen, können Sie sicherstellen, dass nur diese Methode eine zusätzliche Einschränkung hat, die nur für List[Int]
Verwendet werden kann. Im Wesentlichen schreiben Sie Sonderfallcode für bestimmte Arten von Listen.
Keine vollständige Antwort (andere haben bereits darauf geantwortet), ich wollte nur Folgendes beachten, um die Syntax besser zu verstehen: So, wie Sie diese "Operatoren" normalerweise verwenden, wie zum Beispiel im Beispiel von pelotom:
def getStringLength(implicit evidence: A =:= String)
nutzt Scalas Alternative Infix-Syntax für Typoperatoren .
So, A =:= String
ist das gleiche wie =:=[A, String]
(und =:=
ist nur eine Klasse oder Eigenschaft mit einem ausgefallenen Namen. Beachten Sie, dass diese Syntax auch mit "regulären" Klassen funktioniert. Sie können beispielsweise schreiben:
val a: Tuple2[Int, String] = (1, "one")
so was:
val a: Int Tuple2 String = (1, "one")
Es ähnelt den beiden Syntaxen für Methodenaufrufe, der "normalen" mit .
und ()
und die Operatorsyntax.
Lesen Sie die anderen Antworten, um zu verstehen, was diese Konstrukte sind. Hier ist wann Sie sollten sie verwenden. Sie verwenden sie, wenn Sie eine Methode nur für bestimmte Typen einschränken müssen.
Hier ist ein Beispiel. Angenommen, Sie möchten ein homogenes Paar wie folgt definieren:
class Pair[T](val first: T, val second: T)
Nun möchten Sie eine Methode smaller
wie folgt hinzufügen:
def smaller = if (first < second) first else second
Das funktioniert nur, wenn T
bestellt wird. Sie können die gesamte Klasse einschränken:
class Pair[T <: Ordered[T]](val first: T, val second: T)
Aber das scheint eine Schande zu sein - es könnte Verwendungen für die Klasse geben, wenn T
nicht bestellt wird. Mit einer Typeinschränkung können Sie weiterhin die smaller
-Methode definieren:
def smaller(implicit ev: T <:< Ordered[T]) = if (first < second) first else second
Es ist in Ordnung, beispielsweise einen Pair[File]
zu instanziieren, solange Sie nicht smaller
aufrufen.
Im Fall von Option
wollten die Implementierer eine orNull
-Methode, obwohl dies für Option[Int]
Keinen Sinn ergibt. Wenn Sie eine Typeinschränkung verwenden, ist alles in Ordnung. Sie können orNull
für einen Option[String]
Verwenden und Sie können einen Option[Int]
Bilden und verwenden, solange Sie nicht orNull
für diesen aufrufen. Wenn Sie Some(42).orNull
versuchen, erhalten Sie die charmante Nachricht
error: Cannot prove that Null <:< Int
Es kommt darauf an, wo sie eingesetzt werden. In den meisten Fällen handelt es sich bei der Deklaration impliziter Parameter um Klassen. In seltenen Fällen können sie auch Objekte sein. Schließlich können sie Operatoren für Manifest
Objekte sein. Sie sind in den ersten beiden Fällen in scala.Predef
Definiert, jedoch nicht besonders gut dokumentiert.
Sie sollen eine Möglichkeit bieten, die Beziehung zwischen den Klassen zu testen, genau wie es <:
Und <%
Tun, wenn letztere nicht verwendet werden können.
Was die Frage "Wann soll ich sie verwenden?" Betrifft, lautet die Antwort, dass Sie es nicht tun sollten, es sei denn, Sie wissen, dass Sie es sollten. :-) EDIT : Ok, ok, hier sind einige Beispiele aus der Bibliothek. Auf Either
haben Sie:
/**
* Joins an <code>Either</code> through <code>Right</code>.
*/
def joinRight[A1 >: A, B1 >: B, C](implicit ev: B1 <:< Either[A1, C]): Either[A1, C] = this match {
case Left(a) => Left(a)
case Right(b) => b
}
/**
* Joins an <code>Either</code> through <code>Left</code>.
*/
def joinLeft[A1 >: A, B1 >: B, C](implicit ev: A1 <:< Either[C, B1]): Either[C, B1] = this match {
case Left(a) => a
case Right(b) => Right(b)
}
Auf Option
haben Sie:
def orNull[A1 >: A](implicit ev: Null <:< A1): A1 = this getOrElse null
Weitere Beispiele finden Sie in den Kollektionen.
In Scala 2.13 wurden sie aus Predef
verschoben: Move <: <, =: =, DummyImplicits out of Predef # 7350
Eine Funktion von Typbeschränkungen, die in anderen Antworten möglicherweise nicht explizit angegeben wurden, ist, dass sie verwendet werden können
... beschränken jeden abstrakten Typ
T
, der im Geltungsbereich ist in der Argumentliste einer Methode ( nicht nur die eigenen Typparameter der Methode )
Hier ist ein Beispiel, das den "nicht nur die eigenen Typparameter der Methode" zeigt Aspekt. Sagen wir haben
case class Foo[A, B](f: A => B) {
def bar[C <: A](x: C)(implicit e: B <:< String): B = f(x)
}
Foo[Int, String](x => x.toString).bar(1) // OK.
Foo[Int, Double](x => x.toDouble).bar(1) // error: Cannot prove that Double <:< String.
Beachten Sie, wie wir den Typparameter B
einschränken können, obwohl er nicht in der Typparameter-Klausel [C <: A]
Von bar
enthalten ist. Wenn wir stattdessen versucht haben, B
in der Typparameter-Klausel von bar
wie folgt einzuschränken
def bar[B <: String]
wir würden den Typparameter B
aus dem einschließenden Bereich von Foo[A, B]
abschatten. Ein reales Beispiel hierfür aus der Bibliothek wäre toMap
:
trait IterableOnceOps[+A, +CC[_], +C] extends Any {
...
def toMap[K, V](implicit ev: A <:< (K, V)): immutable.Map[K, V] =
...
}