6

I'd like to automatically wrap a value in a generic container on return (I am aware that this is not always desirable, but it makes sense for my case). For example, I'd like to write:

public static Wrapper<string> Load() {
    return "";
}

I'm able to do this by adding the following to my Wrapper class:

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

Unfortunately, this fails when I attempt to convert an IEnumerable, complete code here (and at ideone):

public class Test {
    public static void Main() {
        string x = "";
        Wrapper<string> xx = x;

        string[] y = new[] { "" };
        Wrapper<string[]> yy = y;

        IEnumerable<string> z = new[] { "" };
        Wrapper<IEnumerable<string>> zz = z; // (!)
    }
}
public sealed class Wrapper<T> {
    private readonly object _value;
    public Wrapper(T value) {
        this._value = value;
    }
    public static implicit operator Wrapper<T>(T val) { return new Wrapper<T>(val); }
}

The compilation error I get is:

Cannot implicitly convert type 'System.Collections.Generic.IEnumerable<string>' to '...Wrapper<System.Collections.Generic.IEnumerable<string>>'

What exactly is going on, and how can I fix it?

mk.
  • 11,360
  • 6
  • 40
  • 54
  • 1
    Please show an MCVE. You omitted the important code. – David Heffernan Aug 07 '15 at 19:09
  • Why are you keeping the code from us? – David Heffernan Aug 07 '15 at 19:31
  • @mk: The code from your ideone example is indeed breaking, but it's different than what you've got posted here. Please copy that code into this question. – StriplingWarrior Aug 07 '15 at 19:33
  • 1
    @StriplingWarrior Sufficient? – mk. Aug 07 '15 at 19:39
  • 2
    Replacing `IEnumerable z = new [] {""};` to `var z = new [] {""}` worked.. [Ideone sample](https://ideone.com/9mV0dt). – vendettamit Aug 07 '15 at 19:46
  • 2
    @vendettamit To be clear, that's because `var` implicitly types to `string[]` instead of `IEnumerable`. – 31eee384 Aug 07 '15 at 19:48
  • 1
    Indeed!! Because simply returning array had no issues. – vendettamit Aug 07 '15 at 19:51
  • 2
    31eee384 did a great job of explaining why this happens. If you want a simpler syntax workaround, you can create a static, generic method on `Wrapper` to help people avoid specifying all the generics. I did this in my [Maybe](https://bitbucket.org/j2jensen/callmemaybe) type, so you can say `var zz = Maybe.From(z);`, e.g. – StriplingWarrior Aug 07 '15 at 19:56
  • 2
    I agree with @StriplingWarrior, and in general I think implicit operators should be very rare in your code. They're hard to spot when reading code, hard to look up in an IDE, and apparently can't be used with interfaces! – 31eee384 Aug 07 '15 at 19:59

1 Answers1

6

The reason is part of the C# spec, as noted in this answer:

A class or struct is permitted to declare a conversion from a source type S to a target type T provided all of the following are true:

  • ...
  • Neither S nor T is object or an interface-type.

and

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.

Source

Your implicit conversion works when used differently, like in the following code:

using System;
using System.Collections.Generic;

public class Wrapper<T>
{
    public T Val { get; private set; }

    public Wrapper(T val)
    {
        Val = val;
    }

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

public class Test
{
    public static Wrapper<IEnumerable<int>> GetIt()
    {
        // The array is typed as int[], not IEnumerable<int>, so the
        // implicit operator can be used.
        return new int[] { 1, 2, 3 };
    }

    public static void Main()
    {
        // Prints 1, 2, 3
        foreach (var i in GetIt().Val)
        {
            Console.WriteLine(i);
        }
    }
}

The specific issue you're running into is because you store your array in a IEnumerable<string> local variable before returning it. It's the type of the variable passed into the implicit operator that matters: because the source type S is IEnumerable<int> on your local variable, the operator can't be used. int[] isn't an interface, so it works.

Community
  • 1
  • 1
31eee384
  • 2,748
  • 2
  • 18
  • 27
  • It worked for you because you must be on .Net 4.6. It will not for .Net 4.5 target framework. I think this is new enhancement done in C# 6.0. – vendettamit Aug 07 '15 at 19:26
  • @vendettamit: Works for me in LINQPad, and I don't have 4.6 installed on my machine. http://share.linqpad.net/85xotk.linq – StriplingWarrior Aug 07 '15 at 19:29
  • 1
    Added a comment--it turns out I could reproduce if I stored it in a local variable. This is odd... don't know what causes it, but a fix is to not use a local! – 31eee384 Aug 07 '15 at 19:33
  • Now I'm Wondering cuz that variable thing also failing with 4.6. – vendettamit Aug 07 '15 at 19:39
  • Ideone uses Mono 4.0.2. Not sure which features it has that might affect this, but I'm pretty sure the spec contains the answer. – 31eee384 Aug 07 '15 at 19:41
  • 1
    I'm removing my +1 vote for now, both because the question has now changed to show how to reproduce, and because the portion of the spec you're referring to does not explain this behavior. (PS--it's not a mono issue: LINQPad yields the same results.) – StriplingWarrior Aug 07 '15 at 19:41
  • 1
    If we use `var a = new int[] { 1, 2, 3 };` it's working. Looks like the restriction are specific to interfaces as per the specs. – vendettamit Aug 07 '15 at 19:42
  • I'm not sure how the spec doesn't specify this. With `new int[]` the type is `int[]`, it just is convertible to `IEnumerable`. The conversion can happen within the implicit conversion. When you put it in a local interface-typed variable first, it's typed as an interface and the operator can't happen. It might not be intuitive, but I think it makes sense. – 31eee384 Aug 07 '15 at 19:44
  • 2
    "A class or struct is permitted to declare a conversion from a source type `S` to a target type `T` provided [...] Neither `S` nor `T` is object or an interface-type." In the repro case, `S` is `IEnumerable`. Is this wrong? (Comment I responded to looks to have been deleted, sorry for lack of context.) – 31eee384 Aug 07 '15 at 19:46
  • Yes, you're correct. I understand now. Apparently I'm just tired. +1 – StriplingWarrior Aug 07 '15 at 19:48
  • 1
    No worries, as you can tell by the feverish edits I just learned this today. :) By the way, good call on the edit, it didn't make as much sense with the question containing a repro. – 31eee384 Aug 07 '15 at 19:54
  • you might clarify that "target type `T`" would be `Wrapper` in this case, which is why `string[] y = new[] { "" }; Wrapper> yy = y;` is permissible, though at first glance it looks like there's an illicit conversion from `string[]` to `IEnumerable` – mk. Aug 07 '15 at 20:03
  • Hmm, I'm not sure I follow. First of all, I don't mean to be pedantic, but what do you mean by saying the target type is `Wrapper`? It has to have some ``, and I'm not sure which one you're saying it is. (Honestly, I'm not entirely sure which one it is myself and where exactly the `string[]` starts being referenced as `IEnumerable`, but I don't know if that's in the scope of this question.) Also, I just noticed I completely ignored `string` in your post and went for `int`, sorry for any confusion that may have caused. – 31eee384 Aug 07 '15 at 20:11