I noticed this while using c#'s switch expressions; I'd like to know if this is by design, or something else altogether.
public interface IBase
{
}
public struct Base1: IBase
{
//fields, methods, etc
}
public struct Base2: IBase
{
//fields, methods, etc
}
public struct BaseContainer: IBase
{
public IBase InnerBase{get;}
public BaseContainer(IBase value)
{
//validation and other goodies...
InnerBase = value;
}
public static implicit operator BaseContainer(Base1 value) => new BaseContainer(value);
public static implicit operator BaseContainer(Base2 value) => new BaseContainer(value);
}
public class Util
{
public static IBase NewBase(int somethingToSwitchOn)
{
return somethingToSwitchOn switch
{
1 => new Base1(),
2 => new Base2(),
_ => default(BaseContainer)
};
}
}
Here's where it gets weird:
public static void Main(string[] args)
{
var @base = Util.NewBase(1);
Console.WriteLine(@base.GetType().Name);
}
The above code outputs "BaseContainer", instead of "Base1" as expected, until ANY of the switch steps is explicitly casted to IBase
, e.g:
1 => new Base1(),
2 => (IBase) new Base2(),
_ => default(BaseContainer)
The compiler needs to figure out a type for the switch expression. Its type can't be
Base1
,Base2
andBaseContainer
at the same time, after all.To do this, it finds the best common type of the expressions in each of switch expression's arms. According to the specification, the type is the same type as the type inferred when calling a generic method like this:
with the expressions of the switch expression's arms passed as arguments.
I won't go into the details of type inference, but in general it is quite intuitive (as it is here). The best common type for
new Base1()
,new Base2()
, anddefault(BaseContainer)
isBaseContainer
, and the best common type fornew Base1()
,(IBase)new Base2()
, anddefault(BaseContainer)
isIBase
.So if you don't cast, then the switch expression produces a
BaseContainer
, which means that the newBase1
object you created has to be converted to aBaseContainer
first. This calls your implicit operator, which returns aBaseContainer
object, and soGetType
returnsBaseContainer
.If you cast any of the arms to
IBase
, then the whole switch expression producesIBase
. Converting fromBase1
toIBase
is a boxing conversion, and so doesn't change its dynamic type (i.e. whatGetType
returns).