nbelisle11
nbelisle11

Reputation: 182

C# Type Erasure Issues

Putting together a state machine in C#. I have an interface and abstract implementing class as:

public interface IState<SD> where SD : IStateData {
    T EnterState<T>(InputData inputData, SD movementData) where T : IState<SD>;
}

public abstract class AMovementState : IState<MovementData> {
    public abstract AMovementState EnterState<AMovementState>(InputData inputData, MovementData movementData);
}

MovementData implements IStateData so this seems like it should work fine. However I'm getting the compile error:

Error CS0425: The constraints for type parameter AMovementState' of methodAMovementState.EnterState(InputData, MovementData)' must match the constraints for type parameter T' of interface methodIState.EnterState(InputData, MovementData)'. Consider using an explicit interface implementation instead (CS0425) (Assembly-CSharp)

I think that this is an issue with type erasure but I'm not 100%. Do I need to rework my approach or am I missing something stupid? Coming from mostly a Java background.

Upvotes: 2

Views: 5194

Answers (2)

Eric Lippert
Eric Lippert

Reputation: 660159

I think that this is an issue with type erasure

C# does not have type erasure. C# has actual generic types deeply baked into the runtime.

To start with, you have made a common error; you did this:

class C {
  C M<C>() { ... }

Oh the pain. This is so hard to read. The C in the method is the C declared by the method, not the C declared by the class. You did the same thing:

public abstract class AMovementState : IState<MovementData> {
  public abstract AMovementState EnterState<AMovementState>(
    InputData inputData, MovementData movementData);
}

Same thing here. You have declared two things, both called AMovementState, one a class type and one a type parameter. Please don't do that. It is incredibly misleading for the reader of the code.

Second, once you solve that problem: C# requires that a generic method which implements an interface method match exactly the constraints of that interface method.

Now, you may note that in this particular case, there is no logical requirement that the implementing method have the constraint. The type system could have been designed so that in this case, calling via the class gives a smaller constraint and calling via the interface gives a larger constraint, and that would be type safe.

But C# was not designed that way; it was designed so that constraints are required to match, even in cases where that is not strictly speaking necessary.

So, you have two choices:

  • Add the constraint to the abstract method, or
  • Do what the error message says. Create an explicit interface method that calls the class method.

That is either:

public abstract class AMovementState : IState<MovementData> {
    public abstract M EnterState<M>
    (InputData inputData, MovementData movementData)
    where M : IState<MovementData>;
}

or

public abstract class AMovementState : IState<MovementData> {
    public abstract M EnterState<M>
    (InputData inputData, MovementData movementData);

    M IState<MovementData>.EnterState<M> 
    (InputData inputData, MovementData movementData)
    {
      return this.EnterState<M>(inputData, movementData);
    }
}

Ironically, C# does not allow the constraints to be repeated on the explicit interface method; it infers them automatically. I've always thought of that as one of the oddest features of C#.

More generally: you may be suffering from Genericity Happiness Disease, that is the desire to make things generic even though it is unnecessary and hard to understand. Ask yourself whether a thing actually needs to be generic. Generics can be very difficult to reason about; maybe there is a simpler abstraction you can use.

Upvotes: 13

Lucero
Lucero

Reputation: 60190

Your class does not implement the interface, instead only one specific case.

You need to replicate the method generically:

public abstract class AMovementState : IState<MovementData> {
    public abstract T EnterState<T>(InputData inputData, MovementData movementData) where T : IState<MovementData>;
}

The reason is simple: if some code holds a reference to a IState<MovementData>, they will call the method EnterState generically, therefore the implementation must also be generic.

Upvotes: 1

Related Questions