Case expression - bug or limitation?

Hi there,

i tried to write a construct where a “selector-method” returns a instance of a concrete class implementing an interface.

“SomeSelectorMethod” fails with “Type mismatch, cannot assign “SomeBaseClass” to “ISomeInterface”” in the example below.

If i cast the first item to the wanted Interface (like in “JustAnotherSelectorMethod”) it works.

namespace ConsoleApplication562;

type
  ISomeInterface = public interface
    method SomeMethod(someArg: Object);
  end;

  SomeBaseClass = public class
  public
    class method SomeSelectorMethod(a: Int32): ISomeInterface;
    begin
      // E62 Type mismatch, cannot assign "SomeBaseClass" to "ISomeInterface"
      exit case a of
        1: new Implementor1; 
        2: new Implementor2;
        3: new Implementor3;
      end;
    end;

    class method AnotherSelectorMethod(a: Int32): ISomeInterface;
    begin
      // It works, if two cases are commented out
      exit case a of
        1: new Implementor1;
        //2: new Implementor2;
        //3: new Implementor3;
      end;
    end;

    class method JustAnotherSelectorMethod(a: Int32): ISomeInterface;
    begin
      // This works!
      exit case a of
        1: ISomeInterface(new Implementor1); 
        2: new Implementor2;
        3: new Implementor3;
      end;
    end;

    class method SomeOtherSelectorMethod(a: Int32): ISomeInterface;
    begin
      // This also works 
      if a = 1 then
        exit new Implementor1;
      if a = 2 then
        exit new Implementor2;
      if a = 3 then
        exit new Implementor3;
    end;
  end;

  Implementor1 = public class(SomeBaseClass, ISomeInterface)
  public
    method SomeMethod(someArg: Object); empty;
  end;

  Implementor2 = public class(SomeBaseClass, ISomeInterface)
  public
    method SomeMethod(someArg: Object); empty;
  end;

  Implementor3 = public class(SomeBaseClass, ISomeInterface)
  public
    method SomeMethod(someArg: Object); empty;
  end;

  Program = class
  public

    class method Main(args: array of String): Int32;
    begin
      readLn;
    end;

  end;

end.

Bonus observation

JustAnotherSelectorMethod translates to “wired” decompiled code…

public static ISomeInterface JustAnotherSelectorMethod(int a)
{
  int num = a;
  switch (num)
  {
    case 0:
      return (ISomeInterface) null;
    case 1:
      return new Implementor1() as ISomeInterface;
    case 2:
      return (ISomeInterface) new Implementor2();
    case 3:
      return (ISomeInterface) new Implementor3();
    default:
      if (num != 1)
      {
        if (num != 2)
        {
          if (num != 3)
          {
            if (num != 0)
              return (ISomeInterface) null;
            goto case 0;
          }
          else
            goto case 3;
        }
        else
          goto case 2;
      }
      else
        goto case 1;
  }
}

…whereas funnily enough SomeOtherSelectorMethod results in exactly what i would expect using a case expression…

public static ISomeInterface SomeOtherSelectorMethod(int a)
{
  switch (a)
  {
    case 1:
      return (ISomeInterface) new Implementor1();
    case 2:
      return (ISomeInterface) new Implementor2();
    case 3:
      return (ISomeInterface) new Implementor3();
    default:
      ISomeInterface someInterface;
      return someInterface;
  }
}

The problem here is that the case expression infers its type by finding the closest common ancestor of all it’s cases. Assuming your classes have no common ancestor (aside from the interface), that will be Object (or, I suppose, SomeBaseClass)

Common interfaces are not considered for type inference, because there could be more than one, and of so, what would the type be?

you’d be good if SomeBaseClass also implemented the interface; else you need a cast (either on each case, or for the entire case expressions, eg

     exit case a of
        1: new Implementor1; 
        2: new Implementor2;
        3: new Implementor3;
      end as ISomeInterface;

Cool, that does that trick!

Jain, the wanted outcome of the case expression is known in this case (exit/result is a known and typed variable) - so it hasn’t to infer something - it has actually to proof if the results match…

Sort of, yes, but the way expressions work is they are evaluated inside out. case ... end has a specific type driven purely by the expression itself (again driven bye whats inside it), not its surroundings. once that type is determined, only then does the compiler looks at what you want do with it, and if it’s compatible (and in this case it’s not).

Actual type inference also works this way. the below code is logically identical to the above. the compiler determines the type of the case expression. then it seems that x doesn’t specify a type, and here’s where the inference happens, it assumes/decides x will be what it determined the case expression to be — SomeBaseClass.

And just as above, the exit will fail because the compiler cant prove that that the expression, SomeBaseClass, is compatible with the result ISomeInterface.

var x := case a of
        1: new Implementor1; 
        2: new Implementor2;
        3: new Implementor3;
      end;
exit x;

hope that makes sense.

I understand your explanation. Thanks.

Took me quite i while to understand it in my own code. :wink:

(At first there was only on item in the case expression and it worked… as i added a second implementation… error)

1 Like

;). yeah, this certainly has bitten me once or twice, too.

FWIW, today’s fix for Target-typed conditional expression - C# 9.0 specification proposals | Microsoft Docs should address this.