views:

282

answers:

1

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?

+12  A: 

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.)

Jon Skeet
The weird thing is, the compile error still happens when List is replaced with IList (apart from where an instance of List is constructed and returned from a method that returns IList.
mackenir
To be precise, now the compiler is complaining that there's no implicit cast from `Lazy<T>` to `T`, where `T==IList<string>`.Even though I went and edited the question, it may be the case that your answer still holds, in which case I'll need to go an research what 'encompassing' is in order to understand the answer :)
mackenir
But the point is you're still trying to convert to an *interface* - that's what it doesn't like.
Jon Skeet
I'll put the definition of encompassing into the answer... hold on.
Jon Skeet
Thanks! People, upvote this man.
mackenir
@mackenir: Upvotes aren't as useful to me as accepting the answer, hint hint ;)
Jon Skeet
(should it now say Lazy<T> instead of List<T> throughout the answer?)
mackenir
Indeed, your conjecture is spot on as usual Jon. The reason we disallow this kind of conversion on interfaces is because we never want to "replace" a cheap "built in" representation-preserving conversion with an expensive "user-defined" representation-changing conversion. A conversion to an interface should always simply be a type check to see if the interface is implemented.
Eric Lippert
Also, the "encompassing" stuff in this part of the spec is confusing, and not precisely what we actually implemented. I've been meaning to clean that up for some time now but never had the gumption to actually get through it.
Eric Lippert
@mackenir: No, it shouldn't say `Lazy<T>`, but it should use `IList<T>`... will edit.
Jon Skeet
@Eric: If you're ever on a sweep of confusing terminology, can you have a look at the bound/unbound, open/closed, constructed stuff for generics? It gives me headaches...
Jon Skeet
That stuff drives me nuts too. I always have to look it up to make sure that I'm getting it right. The fact that a non-generic type is BOTH a bound and an unbound type is frankly bizarre.
Eric Lippert
Still completely mystified by what 'encompassing' means.
mackenir