Search code examples
c#genericscovariancecontravariance

C# enforce set of classes to be used together


Here is my use case. Say I have a game with two modes: CoolMode and NormalMode.

Both game modes are very different but share three common objects: Input, Score, and Judger. Each mode would have different implementations, but could be called from the main game using the same code.

interface Judger{
  Score Judge(Input i);
}
abstract class Score {...}
abstract class Input {...}

I wanted to make sure for example, a CoolMode Input would not be passed to a NormalMode Judger. So my first solution was:

interface GameMode{}
interface Judger<T> where T:GameMode{
  Score<T> Judge(Input<T> i);
}
abstract class Score<T> where T:GameMode {...}
abstract class Input<T> where T:GameMode {...}

interface NormalMode{}
class NormalJudger:Judger<NormalMode>{
  Score<NormalMode> Judge(Input<NormalMode> i);
}
class NormalScore:Score<NormalMode>{...}
class NormalInput:NormalInput<NormalMode>{...}

...

The problem is that now NormalJudger can take any Input, not just NormalInput.

How can I force (at compile time) the classes {NormalJudger, NormalScore, NormalInput} to be used with each other?


Solution

  • The problem is that now NormalJudger can take any Input, not just NormalInput.

    If by this you mean that someone could create their own implementation of Input<NormalMode> and pass that to NormalJudger, then really, you should design NormalJudger so that it can judge any kind of Input<NormalMode>. Isn't that why you have created Input as an abstract class? This is the whole point of abstraction. Judge should not care about what specific kind of Input it is, as long as it is an Input<NormalMode>. It seems like the NormalMode type is just a "marker" for the type system to tell your types apart. Well, how about making it actually contain some data/logic, and make Judge operate on it?


    Or, you can just go the quick and dirty way, and parameterise the Judge type differently. Rather than parameterise on the mode, parameterise on Score and Input:

    public interface Judger<TScore, TInput> 
        where TScore: Score
        where TInput: Input{
      TScore Judge(TInput i);
    }
    public abstract class Score {}
    public abstract class Input {}
    
    interface NormalMode : GameMode {}
    class NormalJudger:Judger<NormalScore, NormalInput>{
      public NormalScore Judge(NormalInput i) {return null;}
    }
    class NormalScore:Score{}
    class NormalInput:Input{}