OK, ho pensato che avrei dovuto prendere la mia opinione, invece di pubblicare solo commenti. Scusa, questo sarà lungo, se vuoi che il TLDR salti alla fine.
Come ha detto Randall Schulz, qui _
è una scorciatoia per un tipo esistenziale. Vale a dire,
class Foo[T <: List[_]]
è una scorciatoia per
class Foo[T <: List[Z] forSome { type Z }]
nota che, contrariamente a ciò che la risposta di Randall Shulz menziona (full disclosure: ho sbagliato troppo in una versione precedente fo questo post, grazie a Jesper Nordenberg per segnalarlo) non questo lo stesso come:
class Foo[T <: List[Z]] forSome { type Z }
né è la stessa:
class Foo[T <: List[Z forSome { type Z }]
Attenzione, è facile sbagliarlo (come mostra il mio precedente show): l'autore dell'articolo a cui fa riferimento la risposta di Randall Shulz si è sbagliato (vedi commenti) e lo ha corretto in seguito. Il mio problema principale con questo articolo è che nell'esempio mostrato, l'uso di existentials dovrebbe salvarci da un problema di digitazione, ma non è così. Vai a controllare il codice e prova a compilare compileAndRun(helloWorldVM("Test"))
o compileAndRun(intVM(42))
. Sì, non si compila. Semplicemente facendo compileAndRun
generico in A
renderebbe il codice compilare, e sarebbe molto più semplice. In breve, questo non è probabilmente il miglior articolo per conoscere gli esistenziali e per cosa sono utili (l'autore stesso riconosce in un commento che l'articolo "ha bisogno di essere riordinato").
Quindi mi consiglia di leggere questo articolo: http://www.artima.com/scalazine/articles/scalas_type_system.html, in particolare le sezioni denominate "Tipi esistenziali" e "Varianza in Java e Scala".
Il punto importante che dovresti ottenere da questo articolo è che gli elementi esistenziali sono utili (oltre ad essere in grado di gestire classi java generiche) quando si tratta di tipi non covarianti. Ecco un esempio.
case class Greets[T](private val name: T) {
def hello() { println("Hello " + name) }
def getName: T = name
}
Questa classe è generico (si noti, inoltre, che è invariante), ma possiamo vedere che hello
in realtà non fanno uso del parametro type (a differenza getName
), quindi se ho un'istanza di Greets
I dovrebbe sempre essere in grado di chiamarlo, qualunque sia lo T
. Se voglio definire un metodo che accetta un'istanza Greets
e proprio chiama il suo metodo hello
, ho potuto provare questo:
def sayHi1(g: Greets[T]) { g.hello() } // Does not compile
Certo, basta, questo non si compila, come T
viene fuori dal nulla qui.
OK, allora, facciamo il metodo generico:
def sayHi2[T](g: Greets[T]) { g.hello() }
sayHi2(Greets("John"))
sayHi2(Greets('Jack))
Grande, questo funziona. Potremmo anche utilizzare gli elementi esistenziali qui:
def sayHi3(g: Greets[_]) { g.hello() }
sayHi3(Greets("John"))
sayHi3(Greets('Jack))
Anche lavori. Quindi, tutto sommato, non vi è alcun vantaggio reale dall'usare un parametro esistenziale (come in sayHi3
) su tipo (come in sayHi2
).
Tuttavia, questo cambia se Greets
appare come parametro di tipo in un'altra classe generica. Supponiamo, ad esempio, di archiviare più istanze di Greets
(con diverso T
) in un elenco. Proviamo:
val greets1: Greets[String] = Greets("John")
val greets2: Greets[Symbol] = Greets('Jack)
val greetsList1: List[Greets[Any]] = List(greets1, greets2) // Does not compile
L'ultima riga non viene compilato perché Greets
è invariante, quindi un Greets[String]
e Greets[Symbol]
non può essere trattata come un Greets[Any]
anche se String
e Symbol
entrambi estende Any
.
OK, proviamo con una esistenziali, utilizzando la notazione abbreviata _
:
val greetsList2: List[Greets[_]] = List(greets1, greets2) // Compiles fine, yeah
Questo compila bene, e si può fare, come ci si aspettava:
greetsSet foreach (_.hello)
Ora, ricordate che la ragione abbiamo avuto un problema di controllo del tipo in primo luogo perché Greets
è invariante. Se è stato trasformato in una classe covariante (class Greets[+T]
), allora tutto avrebbe funzionato fuori dalla scatola e non avremmo mai avuto bisogno di esistenziali.
Quindi, per riassumere, esistenziali sono utili per affrontare con le classi generiche invarianti, ma se la classe generica non ha bisogno di apparire se stesso come un parametro di tipo ad un altro classe generica, è probabile che non è necessario esistenziali e la semplice aggiunta di un parametro di tipo per il metodo funzionerà
Ora tornare (finalmente, lo so!) alla tua domanda specifica, per quanto riguarda
class Foo[T <: List[_]]
Perché List
è covariante, questo è a tutti gli effetti e purp È come dire:
class Foo[T <: List[Any]]
Quindi, in questo caso, l'uso di entrambe le notazioni è davvero solo una questione di stile.
Tuttavia, se si sostituisce List
con Set
, le cose cambiano:
class Foo[T <: Set[_]]
Set
è invariante, e quindi ci sono nella stessa situazione con la classe Greets
dal mio esempio. Quindi quanto sopra è molto diverso da
class Foo[T <: Set[Any]]
Vincolare un tipo a "<: Any" non cambia mai nulla. Ogni tipo in Scala è '<: Any'. –