I have two classes (Dog and Cat) which implement a common interface (Mammal). I also have a generic class (Zone) which encapsulates lists of variables of a specific type, and some other functionality.
public interface Mammal{}
public class Cat : Mammal {}
public class Dog : Mammal {}
public class Zone<T> where T : Mammal {}
I want to do something like this:
Zone<Dog> doghouse = new Zone<Dog>();
Zone<Cat> cathouse = new Zone<Cat>();
Zone<Mammal> zoo = new Zone<Mammal>();
Cat meow = new Cat(cathouse); // This cat starts out in the cathouse
cathouse.moveMammalToZone(meow, zoo); // This should move the cat to the zoo
Dog woof = new Dog(zoo); // This dog starts out in the zoo
zoo.moveMammalToZone(woof, cathouse); // This should throw an error/not be possible
Where dogs and cats can both be in a zoo, but only dogs are allowed in the doghouse and only cats in the cathouse. If a dog were to leave the doghouse, he must go to another zone where he is allowed (all dogs and cats must always be in a zone). Therefore, I tried to create a function which moves the animal to a different zone:
public class Zone<T> where T : Mammal {
public bool contains(T mammal){...} // Returns whether the mammal is in this zone
private void remove(T mammal){...} // Removes the mammal from this zone
private void add(T mammal){...} // Adds the mammal to this zone
// NOTE: remove() and add() should never be called unless a mammal is moving
// between zones, otherwise there could be a mammal without a zone, or a mammal with multiple zones
public bool moveMammalToZone<X>(T mammal, Zone<X> zone) where X : Mammal{
// we need to check if the mammal is in this zone, and if it is the right
// type so that it can go to the other zone
if(mammal is X && this.contains(mammal)){
zone.add(mammal as X);
this.remove(mammal);
return true;
}
return false;
}
}
But I'm getting the error: "The type parameter 'X' cannot be used with the 'as' operator because it does not have a class type constraint nor a 'class' constraint" csharp(CS0413).
The error is on the line
zone.add(mammal as X);
What does this mean? How would I do what I am trying to do?
NOTE: I have found out that changing Mammal to a class instead of interface fixes this problem. Why?
CodePudding user response:
The "why" question here is not that interesting. This is just how as
is specified in the spec.
In an operation of the form
E as T
,E
shall be an expression andT
shall be a reference type, a type parameter known to be a reference type, or a nullable value type.
In your code, since Mammal
is an interface, X
is not "known to be a reference type", because structs can also implement interfaces. Therefore, mammal as X
does not compile.
If Mammal
is a class, then you are sure that X
is a class, and as X
is valid. Because only classes can inherit classes. Structs cannot inherit classes. A similar reasoning applies if you use the : class
generic constraint.
The other alternative you might attempt to do is to cast (X)mammal
. This doesn't work because, again, there is no such conversions specified in the spec. You could, however, cast it to object
first, then cast to X
. Being able to cast to object
is specified here:
For a type_parameter
T
that is not known to be a reference type, the following conversions involvingT
are considered to be unboxing conversions at compile-time.
From the effective base class
C
ofT
toT
and from any base class ofC
toT
.Note:
C
will be one of the typesSystem.Object
,System.ValueType
, orSystem.Enum
(otherwise T would be known to be a reference type). end note
"Casting to object
first" is also what pattern matching does under the hood, which I recommend using if you can.
if(mammal is X xMammal && this.Contains(mammal)){
zone.Add(xMammal);
this.Remove(mammal);
return true;
}
As for why the spec is written this way, it isn't usually answerable. I can only speculate that it is related to the deeply rooted divide between value types and reference types. Specifically, value types and reference types have vastly different kinds of conversions and not much in common, so there are not many conversions available on a type parameter which you are not sure whether it is a value type or a reference type.
Finally, also consider doing this check at compile-time instead of runtime.
// I've also fixed the names in your code to follow C#'s conventions
public bool MoveMammalToZone<TMammal, TOther>(TMammal mammal, Zone<TOther> zone)
where TMammal : IMammal, TOther, T
where TOther : IMammal {
if(this.Contains(mammal)){
zone.Add(mammal);
this.Remove(mammal);
return true;
}
return false;
}
The idea is that the method moves a mammal of compile-time type TMammal
to another zone. TMammal
must be from this zone and fit in the destination zone, so : TOther, T
.
Since the type parameters are related in this scenario, conversions exist.
CodePudding user response:
Here's a minimal implementation of your code showing that it works with mammal is X x
:
void Main()
{
Zone<Dog> doghouse = new Zone<Dog>();
Zone<Cat> cathouse = new Zone<Cat>();
Zone<Mammal> zoo = new Zone<Mammal>();
Cat meow = new Cat();
cathouse.Add(meow);
Console.WriteLine(cathouse.MoveMammalToZone(meow, zoo));
Dog woof = new Dog();
doghouse.Add(woof);
Console.WriteLine(zoo.MoveMammalToZone(woof, cathouse));
}
public interface Mammal { }
public class Cat : Mammal { }
public class Dog : Mammal { }
public class Zone<T> : List<T> where T : Mammal
{
public bool MoveMammalToZone<X>(T mammal, Zone<X> zone) where X : Mammal
{
if (mammal is X x && this.Contains(mammal))
{
zone.Add(x);
this.Remove(mammal);
return true;
}
return false;
}
}
That produces:
True
False