3

I have an ILookup<int, Derived> and I want to return an ILookup<int, Base> where Derived implements or extends Base.

Currently I use SelectMany() and then ToLookup() to first extract the key value pairs of ILookup<int, Derived> into a flat IEnumerable and then create a new ILookup<int, Base>:

class Base { }

class Derived: Base { }

class Test
{
    ILookup<int, Base> CastLookup(ILookup<int, Derived> existing)
    {
        IEnumerable<KeyValuePair<int, Base>> flattened = existing.SelectMany(
            (x) => x,
            (gr, v) => new KeyValuePair<int, Base>(gr.Key, (Base)v));    // I know the explicit cast can be implicit here; it is just to demonstrate where the up casting is happening.
        ILookup<int, Base> result = flattened.ToLookup(
            (x) => x.Key,
            (x) => x.Value);
        return result;
    }
}

How can I convert an ILookup without iterating its entries and then repacking them?


NOTE: A related question is Shouldn't ILookup<TKey, TElement> be (declared) covariant in TElement? by bigge. Ryszard Dżegan answers that it is mostly for historical reasons: ILookup<TKey, TElement> has been developed before generics with covariance.
Herzmeister asks something similar for a Dictionary<TKey, TValue>. Mehrdad Afshari answers that for the mutable dictionary the covariance would not be safe.
Indeed, if Ilookup<TKey, TElement> was covariant in TElement, I would not have run into this instance of the ILookup<TKey, TElement> casting question; but it is not, so my quest for a better way still continues.

NOTE: I can of course write an extension method to do it, but that does not prevent the required computational work of iterating and repacking.

Kasper van den Berg
  • 8,951
  • 4
  • 48
  • 70
  • Duplicate of https://stackoverflow.com/questions/2149589/idictionarytkey-tvalue-in-net-4-not-covariant ? – Tomer Jul 18 '18 at 09:17
  • @Tomer, I should mention that question but it is similar to the one i linked by bigge. To my opinion not a duplicate; true, if ILookup was covariant I would not have asked my question, but i'm not asking for all ILookup/IDictionary to be covariant I just want to easily cast this particular one where I know it is safe. – Kasper van den Berg Jul 18 '18 at 10:45

2 Answers2

3

You could create a proxy:

public static ILookup<TKey, TValueBase> ToLookupBase<TKey, TValue, TValueBase>(this ILookup<TKey, TValue> lookup)
    where TValue : class, TValueBase
{
    return new LookupProxy<TKey, TValue, TValueBase>(lookup);
}

public class LookupProxy<TKey, TValue, TValueBase> : ILookup<TKey, TValueBase>
    where TValue : class, TValueBase
{
    private readonly ILookup<TKey, TValue> lookup;

    public LookupProxy(ILookup<TKey, TValue> lookup)
    {
        this.lookup = lookup;
    }

    public IEnumerable<TValueBase> this[TKey key] => lookup[key];

    public int Count => lookup.Count;

    public bool Contains(TKey key) => lookup.Contains(key);

    public IEnumerator<IGrouping<TKey, TValueBase>> GetEnumerator() => lookup.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

Note that you'll have to:

var base = existing.ToLookupBase<int, Derived, Base>();

so explicitly tell all the generic parameters. If you want to even support covariance for TKey it is a little more complex, and needs a separate support class and a separate method:

public static ILookup<TKeyBase, TValueBase> ToLookupBase2<TKey, TValue, TKeyBase, TValueBase>(ILookup<TKey, TValue> lookup)
    where TKey : class, TKeyBase
    where TValue : class, TValueBase
{
    return new LookupProxy2<TKey, TValue, TKeyBase, TValueBase>(lookup);
}

public class LookupProxy2<TKey, TValue, TKeyBase, TValueBase> : ILookup<TKeyBase, TValueBase>
    where TKey : class, TKeyBase
    where TValue : class, TValueBase
{
    private readonly ILookup<TKey, TValue> lookup;

    public LookupProxy2(ILookup<TKey, TValue> lookup)
    {
        this.lookup = lookup;
    }

    public IEnumerable<TValueBase> this[TKeyBase key] => key is TKey ? lookup[(TKey)key] : Enumerable.Empty<TValueBase>();

    public int Count => lookup.Count;

    public bool Contains(TKeyBase key) => key is TKey ? lookup.Contains((TKey)key) : false;

    public IEnumerator<IGrouping<TKeyBase, TValueBase>> GetEnumerator() => lookup.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

this because you need to add a where TKey : class, TKeyBase (that won't support value types for the key, like in your example).

xanatos
  • 109,618
  • 12
  • 197
  • 280
  • 1
    @PaulSuart Mine is a more transparent proxy, yours is something mixed, using the `SelectMany()` in the `GetEnumerator()` (but in the end the `GetEnumerator()` is already enumerating the base `ILookup<>` so it isn't a big problem). **The end result isn't very different** – xanatos Jul 18 '18 at 09:36
  • Ah, I realised that wasn't even necessary and could be removed. – Paul Suart Jul 18 '18 at 10:55
1

How about creating your own class that implements ILookup<TKey, TBase>, like this:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            var lookup1 = new List<Derived> { new Derived() { Key = 1, Prop1 = "A", Prop2 = "B"} } .ToLookup(x => x.Key, x => x);

            var baseLookup = new BaseLookup<int, Base, Derived>(lookup1);

            Console.WriteLine(baseLookup[1]);

        }        
    }

    public class BaseLookup<TKey, TBase, TDerived> : ILookup<TKey, TBase> where TDerived : TBase
    {
        private readonly ILookup<TKey, TDerived> _inner;

        public BaseLookup(ILookup<TKey, TDerived> inner)
        {
            _inner = inner;
        }

        IEnumerator<IGrouping<TKey, TBase>> IEnumerable<IGrouping<TKey, TBase>>.GetEnumerator()
        {
            return (IEnumerator<IGrouping<TKey, TBase>>) _inner.GetEnumerator();
        }

        public IEnumerator GetEnumerator()
        {
            return ((IEnumerable) _inner).GetEnumerator();
        }

        public bool Contains(TKey key)
        {
            return _inner.Contains(key);
        }

        public int Count => _inner.Count;

        public IEnumerable<TBase> this[TKey key] => _inner[key].Cast<TBase>();
    }

    public class Base
    {
        public int Key { get; set; }

        public string Prop1 { get; set; }
    }

    public class Derived : Base
    {
        public string Prop2 { get; set; }
    }
}
Paul Suart
  • 6,505
  • 7
  • 44
  • 65