9

Given this class:

public class Wrapper<T>
{
    public Wrapper(T value)
    {
        Value = value;
    }

    public T Value { get; }

    public static implicit operator Wrapper<T>(T value)
    {
        return new Wrapper<T>(value);
    }
}

This code snippet doesn't compile:

IEnumerable<int> value = new [] { 1, 2, 3 };
Wrapper<IEnumerable<int>> wrapper = value;

error CS0266: Cannot implicitly convert type 'System.Collections.Generic.IEnumerable< int>' to 'Spikes.Implicit.Wrapper< System.Collections.Generic.IEnumerable< int>>'. An explicit conversion exists (are you missing a cast?)

But the compiler is perfectly happy with this:

Wrapper<IEnumerable<int>> wrapper = new [] { 1, 2, 3 };

Why?

Paul Tierney
  • 101
  • 3
  • This feels like one of those co/contravariance issues. I'm not knowledgeable enough to write an answer though, I struggle with understanding it. – Flater Apr 13 '18 at 10:01
  • @Flater Co/contravariance? The type on the left is the exact same as that one on the right. – MakePeaceGreatAgain Apr 13 '18 at 10:02
  • 3
    It's not an issue with variance, or even generics. `interface IFoo {}; class Bar : IFoo {}; IFoo x = new Bar(); Wrapper xx = x;` will trigger the same issue, and the issue disappears if `x` is declared as `Bar` instead. The problem is the more basic fact that this conversion is not allowed for interfaces. – Jeroen Mostert Apr 13 '18 at 10:05
  • Section 10.10.3: "User-defined conversions are not allowed to convert from or to interface-types. In particular, this restriction ensures that no user-defined transformations occur when converting to an interface-type, and that a conversion to an interface-type succeeds only if the object being converted actually implements the specified interface-type." – Hans Passant Apr 13 '18 at 10:20
  • As an aside, your *real* issue is that you're offering up this conversion (for *any* `T`) as `implicit`. That's a bad idea even if you could get it to work. You can make it an explicit conversion, but it would be even better to simply offer a `Wrapper.Wrap` static method to generate these instances. Conversion is the hairiest part of the C# spec; when you start using it beyond the natural cases of extended numeric types, you quickly run into obscure surprises like this. – Jeroen Mostert Apr 13 '18 at 10:37
  • I actually came across this trying to use the new `Microsoft.AspNetCore.Mvc.ActionResult`. – Paul Tierney Apr 13 '18 at 10:41
  • Well, the MVC team has the excuse that `ActionResult` is only ever intended to be used as a return type... but I'd still argue that relying on implicit conversions for this is a bad idea, and they're much better off requiring people to write `ActionResult.For(value)`. But I'm not a designer on the team, or even an end user. – Jeroen Mostert Apr 13 '18 at 10:48

1 Answers1

7

The C# Language Specification states this clearly:

Section 6.4.1 Permitted user-defined conversions

For a given source type S and target type T, if S or T are nullable types, let S0 and T0 refer to their underlying types, otherwise S0 and T0 are equal to S and T respectively. A class or struct is permitted to declare a conversion from a source type S to a target type T only if all of the following are true:

  • S0 and T0 are different types.
  • Either S0 or T0 is the class or struct type in which the operator declaration takes place.
  • Neither S0 nor T0 is an interface-type.
  • Excluding user-defined conversions, a conversion does not exist from S to T or from T to S.

Your case violates point 3 of the conditions. The source type is an interface!

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • And yet in "compiler is happy with" case from question, implicit operator will be called when `T` is interface (`IEnumerable`). As I read this, I'd expect it fail in that case too (yes, what you pass is not interface, but implicit operator is defined for interface in this case). – Evk Apr 13 '18 at 10:14
  • The compiler can't know what `T` is at the declaration of the implicit conversion. It only complains when it infers that `T` is an interface when you try to _do_ the conversion. Nothing wrong with that, right? @Evk – Sweeper Apr 13 '18 at 10:18
  • When you do `Wrapper> wrapper2 = new[] { 1, 2, 3 };`, compiler uses implicit operator with signature `implicit operator Wrapper>(IEnumerable value)` (because that is the left side of assignment), which is illegal according to the spec, as I understand it. – Evk Apr 13 '18 at 10:22
  • This answer by Eric Lippert states that reality is different from specification for user defined implicit conversion: https://stackoverflow.com/a/39619053/5311735. Maybe this is one of such cases. – Evk Apr 13 '18 at 10:23
  • @Evk Just a speculation, maybe the compiler saw the left part and infers `T` to be `IEnumerable` but then tries to get the source type S from the _right_ part, which it finds is an `int[]`. "Yay! Not an interface!" it thinks, and performs the conversion by plugging the `int[]` into the parameter. – Sweeper Apr 13 '18 at 10:37
  • Probably. I guess it's documented somewhere here: https://github.com/dotnet/roslyn/blob/master/src/Compilers/CSharp/Portable/Binder/Semantics/Conversions/UserDefinedImplicitConversions.cs, under one of "DELIBERATE SPEC VIOLATION" sections, but I'm not in the mood to try to decrypt what is written there :) – Evk Apr 13 '18 at 10:39