Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Odd C# behavior when implementing generic interface

Given this "IHandle" interface and two classes to be handled:

interface IHandle<T>
{
  void Handle(T m);
}


class M1
{
  public int Id;
}


class MReset
{
}

I want to create a generic base that takes care of "resetting" as well as managing M1 instances:

class HandlerBase<T> :
  IHandle<MReset>,
  IHandle<T> where T : M1
{
  protected int Count;

  void IHandle<T>.Handle(T m)
  {
    ++Count;
    Console.WriteLine("{0}: Count = {0}", m.Id, Count);
  }


  void IHandle<MReset>.Handle(MReset m)
  {
    Count = 0;
  }
}

This does not compile since the compiler believes T could be "MReset" so it outputs:

error CS0695: 'HandlerBase' cannot implement both 'IHandle' and 'IHandle' because they may unify for some type parameter substitutions

That in itself is slightly odd since I cannot see how T could possibly be of type MReset since it must be of type M1. But okay, I can accept that the compiler is smarter than me :-)

Edit: The compiler is not smarter than me :-) According to a comment on Why does this result in CS0695? we have "Constraint declarations are not considered when determining all possible constructed types".

Now I swap the interface declarations:

class HandlerBase<T> :
  IHandle<T> where T : M1,
  IHandle<MReset>
{
  ... same as before ..
}

And suddenly I get a different error message stating that I cannot implement IHandle.Handle(MReset m) since the class declaration does not state that it is implementing that interface:

error CS0540: 'HandlerBase.IHandle<...>.Handle(MReset)': containing type does not implement interface 'IHandle'

Question: why does the order of declarations make such a difference? What is going wrong in the second example?

In the end it turns out that there is a solution:

class HandlerBase :
  IHandle<MReset>
{
  protected int Count;


  void IHandle<MReset>.Handle(MReset m)
  {
    Count = 0;
  }
}


class Handler<T> : HandlerBase,
  IHandle<T> where T : M1
{
  void IHandle<T>.Handle(T m)
  {
    ++Count;
    Console.WriteLine("{0}: Count = {0}", m.Id, Count);
  }
}

But the solution only works if HandlerBase implements IHandle<MReset> - not if the generic interface IHandle<T> is implemented in HandlerBase first. Why?

Edit: Implementing IHandle<T> in HandlerBase does work (and if I had shown the code someone might have seen it). This works:

class HandlerBase<T> :
  IHandle<T> where T : M1
{
  protected int Count;

  void IHandle<T>.Handle(T m)
  {
    ++Count;
    Console.WriteLine("Type = {0}, Id = {1}, Count = {2}", GetType(), m.Id, Count);
  }
}


class Handler<T> : HandlerBase<T>,
  IHandle<MReset>
  where T : M1
{
  void IHandle<MReset>.Handle(MReset m)
  {
    Count = 0;
    Console.WriteLine("RESET");
  }
}

Unfortunately my second class declaration was this:

class Handler<T> : HandlerBase<T> where T : M1,
  IHandle<MReset>
{
  void IHandle<MReset>.Handle(MReset m)
  {
    Count = 0;
    Console.WriteLine("RESET");
  }
}

Notice the subtle difference in the location of where T : M1 :-) The last example declares that T must implement IHandle<MReset> (in addition to M1). Duh!

like image 389
Jørn Wildt Avatar asked Apr 24 '15 09:04

Jørn Wildt


2 Answers

Problem solved - I found the subtle difference. When the order of declarations is swapped I should not move where T : M1 since the IHandle<MReset> constraint then ends up being applied to T instead of the class declaration:

class HandlerBase<T> :
  IHandle<T> where T : M1,
  IHandle<MReset>
{
  ... same as before ..
}

The correct re-ordering should have been:

class HandlerBase<T> :
  IHandle<T>,
  IHandle<MReset>
  where T : M1
{
  ... same as before ..
}
like image 116
Jørn Wildt Avatar answered Nov 10 '22 15:11

Jørn Wildt


@Siram pointed out that the uniqueness problem (but not the order aspect) has been answered in Why does this result in CS0695?:

The C# language spec (https://www.microsoft.com/en-us/download/confirmation.aspx?id=7029) discusses the "Uniqueness of implemented interfaces" in 13.4.2.: "The interfaces implemented by a generic type declaration must remain unique for all possible constructed types." And later, when describing the details of the check: "Constraint declarations are not considered when determining all possible constructed types."

Why that is so I am not sure; perhaps one can construct nested or chained constraints which make it impossible for the compiler to prove uniqueness, or not all constraints can be communicated via assemblies (which, I think, would be necessary for the general language rule).

like image 35
Peter - Reinstate Monica Avatar answered Nov 10 '22 14:11

Peter - Reinstate Monica