问题
I was reading through http://oldfashionedsoftware.com/2008/08/26/variance-basics-in-java-and-scala/
and am looking at the code
class CoVar[+T](param1: T) {
def method1(param2: T) = { }
def method2: T = { param1 }
def method3: List[T] = { List[T](param1) }
def method4[U >: T]: List[U] = { List[U](param1) }
val val1: T = method2
val val2: Any = param1
var var1: T = method2
var var2: Any = param1
}
then if I were to have a
val covar1 = new CoVar(new Car)
val covar2: CoVar[Vehicle] = covar1 //completely legal with covariant
Now, let's walk through the methods
- method1 - I don't get why this doesn't compile and that is my main question
- method2 - param1 is a car and method2 returns a Vehicle which is fine since Car is a Vehicle
- method3 - since List[Vehicle] is returned and Car is a Vehicle this is fine
- var1 - same question I believe and not too different
I would think this would be ok with method1(param: Vehicle) since I can pass in a new Vehicle just fine or a new Car just fine
but the original CoVar class does not compile since it says method1 is contravariant position. I thought contravariant would mean I could pass in a
Now, walking through this with ContraVar, and method1 again we have
class ContraVar[-T](param1: T) {
def method1(param2: T) = { }
val val2: Any = param1
var var2: Any = param1
}
val temp1 = new ContraVar(new Car)
val temp2: ContraVar[Ford] = temp1
temp2.method1(new Ford)
temp2.method1(new FordMustang)
temp2.method1(new Car) //fails to compile(good)
which work just fine. Can someone please explain why method1 breaks on CoVar? Perhaps I am headed down the completely wrong path on what would go wrong with letting method1 compile just fine?
thanks, Dean
回答1:
Your question boils down to why the following is illegal
trait Tool[+A] {
def treat(c: A): Unit
}
Just imagine it would compile… Let's look at a use case from the outside:
def apply[A](tool: Tool[A], car: A): Unit = tool.treat(car)
Say there are two possible types for A
:
trait Car
trait Mustang extends Car { def awe(): Unit }
Covariance would mean Tool[Mustang] <: Tool[Car]
. So whenever a Tool[Car]
is asked for, you could use a Tool[Mustang]
. Now imagine a Tool[Mustang]
:
val tm = new Tool[Mustang] { def treat(m: Mustang) = m.awe() }
And now you would be able to call:
apply[Car](tm, new Car {})
This would mean, tm
could access a non-existing method awe
in a generic car. Obviously this is not a sound type relation. Therefore, whenever a type is used in argument position, it must be invariant or contravariant.
回答2:
Let's give your method1 a body:
class CoVar[+T] {
var listOfT: List[T] = Nil
// method1 prepends the given element to listOfT
def method1(param2: T) = { listOfT = param2 :: ListOfT }
}
Now we actually do something with the parameter passed into method1, it will be easy to see how something wrong can happen if this were allowed.
// Lets construct one of these that holds Ints. and add something to it
val covarInt = new CoVar[Int]
covarInt.method1(1)
// lets assign to a more general value, this is no problem because of the covariance
val covarAny: CoVar[Any] = covarInt
// now lets do a bad thing:
covarAny.method1("this is a string not an Int")
the last line shows the bad thing that would be allowed. Since covarAny
is type CoVar[Any]
, that means that in the CoVar
class, type T = Any
, so the type expected as input to method1
is Any
, so passing a String
into this function should be allowed since String
is an Any
. However the method body would then try to prepend our passed String
to a list of Int
s which should not be allowed.
回答3:
I have a new answer that I thought helped me alot on why params are contravariant rather than covariant. This example can be done all without generics too.
Let's use the function objects instead and define an Animal, Bird, and Duck class
class Animal {
def makeSound() = "animalsound"
def walk()
}
class Bird extends Animal {
def makeSound() = "tweeeeet"
def fly()
}
class Duck extends Bird {
def makeSound() = "quack"
def paddle()
}
Now, let's try to define a covariant function
val doSomething: (Bird => String) = { d:Duck => d.paddle() }
Naturally, when a Bird is passed in, we can't cast to a Duck as it may not be a Duck and it certainly can't paddle with no paddle method in the Bird class.
Now, let's try to define a contravariant function
val doSomething: (Bird => String) = { a:Animal => a.walk() }
This now compiles and works because a bird is an Animal so it will have a walk method. This really cleared things up even more for me and naturally methods are very much like functions or you can always convert the method to a function in which case it needs to be contravariant for the params.
Now, is there one more way we could do something here as in T >: Bird maybe? I wonder if I were to do a ( T >: Bird => String ) = .....
well, I think I am beyond my knowledge there at this point and leave that as a TODO on my list of things to learn about.
来源:https://stackoverflow.com/questions/26109318/scala-contravariant-position-on-method