Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to cast TElement of lookup; i.e. ILookup<TKey, Derived> to ILookup<TKey, Base>?

Tags:

c#

linq

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 in .NET 4 not covariant - Stack Overflow">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.

like image 908
Kasper van den Berg Avatar asked Nov 20 '25 20:11

Kasper van den Berg


2 Answers

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

like image 192
xanatos Avatar answered Nov 23 '25 23:11

xanatos


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; }
    }
}
like image 45
Paul Suart Avatar answered Nov 24 '25 01:11

Paul Suart



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!