Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why doesn't this use of implicit casts work?

I've defined a generic class "Lazy<T>", for lazy evaluation and caching of the result of a delegate Func<T>.

I also define two implicit cast operators so I can create a Lazy<T> from a Func<T>s, and I can assign a Lazy<T> to a T (gets the Value of the Lazy<T>)

The idea is that you can pass around a Lazy<T> in place of an instance of T, but not do the work to calculate/retrieve the value until it is assigned to an actual instance of T.

// class Lazy<T>
// Encapsulates a value which can be retrieved when first accessed, 
// and is then cached.
class Lazy<T>
{
  private Func<T> _getter;
  private T _cached;
  private bool _isCached;

  // Get/set the getter delegate
  // that 'calculates' the value.
  public Func<T> Getter
  {
    get
    {
      return _getter;
    }
    set 
    {
      _getter = value;
      _cached = default(T);
      _isCached = false;
    }
  }

  // Get/set the value.
  public T Value
  {
    get 
    {
      if (!_isCached) 
      {
        _cached = Getter();
        _isCached = true;
        _getter = null;
      }
      return _cached;
    }
    set
    {
      _cached = value;
      _isCached = true;
      _getter = null;
    }
  }

  // Implicit casts:

  // Create a T from a Lazy<T>
  public static implicit operator T(Lazy<T> lazy) 
  {
    return lazy.Value;
  }

  // Create a Lazy<T> from a Func<T>
  public static implicit operator Lazy<T>(Func<T> getter)
  {
    return new Lazy<T> {Getter = getter};
  }
}

But this class doesn't work as I expected in one case, highlighted in the test app below:

class Program
{
  static void Main()
  {
    // This works okay (1)
    TestLazy(() => MakeStringList());

    // This also works (2)
    Lazy<string> lazyString = new Func<string>(() => "xyz");
    string s = lazyString;

    //This doesn't compile (3)
    //
    Lazy<IList<string>> lazyStrings = new Func<IList<string>>(MakeStringList);
    IList<string> strings = lazyStrings; //ERROR
  }


  static void TestLazy<T>(Func<T> getter)
  {
    Lazy<T> lazy = getter;
    T nonLazy = lazy;
  }

  private static IList<string> MakeStringList()
  {
    return new List<string> { new string('-', 10) };
  }
}

On the line marked with //ERROR, I get a compile error:

error CS0266: Cannot implicitly convert type Lazy<System.Collections.Generic.IList<string>> to System.Collections.Generic.IList<string>. An explicit conversion exists (are you missing a cast?)

This error is confusing as there does exist an implicit cast from the source to the target type in question. And, on the face of it, code chunk (3) is doing the same thing as (1) Also, it differs from (2) only by the type used to specialize the Lazy.

Can anyone explain to me what's going on here?

like image 962
mackenir Avatar asked Oct 02 '09 10:10

mackenir


2 Answers

The problem is that you're trying to convert to IList<T> implicitly, and IList<T> isn't encompassed by IList<T> (even though they're the same type) - only conversions to non-interface types are considered in encompassing. From section 6.4.3 of the C# 3.0 spec:

If a standard implicit conversion (§6.3.1) exists from a type A to a type B, and if neither A nor B are interface-types, then A is said to be encompassed by B, and B is said to encompass A.

In section 6.4.4, talking about user defined conversions, one of the steps is (emphasis mine):

  • Find the set of applicable user-defined and lifted conversion operators, U.

This set consists of the user-defined and lifted implicit conversion operators declared by the classes or structs in D that convert from a type encompassing S to a type encompassed by T. If U is empty, the conversion is undefined and a compile-time error occurs.

IList<T> isn't encompassed by IList<T>, therefore this step fails.

The compiler will do "chained" implicit conversions in other scenarios though - so if you actually had a Lazy<List<T>> you could write:

object strings = lazyStrings;

works, because List<T> is encompassed by object (as both are classes, and there's a standard implicit conversion from List<T> to object).

Now as for why this is the case, I suspect it's to stop odd cases where you'd expect a reference conversion, but you would actually get the implicit conversion. Suppose we had:

class ListLazy : Lazy<IList<string>>, IList<string>
{
    // Stuff
}
...
Lazy<IList<string>> x = new ListLazy();
IList<string> list = x;

Which conversion should be used? There's an implicit reference conversion from the actual type to IList<string>... but the compiler doesn't know that, because the expression is of type Lazy<IList<string>>. Basically interfaces are awkward because they can show up later in the type hierarchy, whereas with a class you always know where you are, if you see what I mean. (Implicit conversions which involve two classes in the same hierarchy are prohibited.)

like image 155
Jon Skeet Avatar answered Nov 13 '22 06:11

Jon Skeet


Could it be a little typo?

Lazy<List<string>> lazyStrings = new Func<List<string>>(MakeStringList);
IList<string> strings = lazyStrings; //ERROR
List<string> strings = lazyStrings; //OK

If you do want an IList<>, it's a 2-step conversion and I suppose the compiler doesn't want to get ahead of itself

IList<string> istrings = strings;
like image 34
Henk Holterman Avatar answered Nov 13 '22 07:11

Henk Holterman