Search code examples
c#.net.net-coredynamically-generatedvaluetuple

How to create a .NET C# ValueType Tuple dynamically?


I would like to create a value type tuple dynamically from say a collection of values.

Example: I have a given IEnumerable<T> and I would like to create a tuple based on that collection.

How can I achieve that?

It seems that the access within a value type tuple can be achieved dynamically but nothing indicates that the same can be done for the creation of a value type tuple.

On of my purposes would be to leverage the properties of the Equality and HashCode of such tuples like described in this article


Solution

  • The question still isn't clear, but I assume you want to convert your a collection into a value tuple of the form (a[0], a[1], a[2], …). This isn't supported through any built-in functionality. Furthermore, you would quickly run into limits, as .NET Framework only defines up to ValueTuple<T1, …, T7> – going beyond that would require you to construct unwieldy nested value tuples using ValueTuple<T1, …, T7, TRest> (e.g. ValueTuple<T1, …, T7, ValueTuple<T1, …, T7, ValueTuple<T1, …>>>).

    If you're seeking to achieve collection equality comparisons, you should use an IEqualityComparer<ICollection<T>> instead. Here is a sample implementation from SequenceEqualityComparer:

    public class SequenceEqualityComparer<TElement> : EqualityComparer<IEnumerable<TElement>>
    {
        private readonly IEqualityComparer<TElement> _elementEqualityComparer;
    
        public SequenceEqualityComparer()
            : this(null)
        { }
    
        public SequenceEqualityComparer(IEqualityComparer<TElement> elementEqualityComparer)
        {
            _elementEqualityComparer = elementEqualityComparer ?? EqualityComparer<TElement>.Default;
        }
    
        public new static SequenceEqualityComparer<TElement> Default { get; } = new SequenceEqualityComparer<TElement>();
    
        public override bool Equals(IEnumerable<TElement> x, IEnumerable<TElement> y)
        {
            if (object.ReferenceEquals(x, y))
                return true;
            if (x == null || y == null)
                return false;
    
            if (x is ICollection<TElement> xCollection &&
                y is ICollection<TElement> yCollection &&
                xCollection.Count != yCollection.Count)
                return false;
    
            return x.SequenceEqual(y, _elementEqualityComparer);
        }
    
        public override int GetHashCode(IEnumerable<TElement> sequence)
        {
            if (sequence == null)
                return 0;
    
            unchecked
            {
                const uint fnvPrime = 16777619;
                uint hash = 2166136261;
    
                foreach (uint item in sequence.Select(_elementEqualityComparer.GetHashCode))
                    hash = (hash ^ item) * fnvPrime;
    
                return (int)hash;
            }
        }
    }
    

    Edit: For the fun of it, here's my implementation to your actual question, using reflection and recursion:

    public static object CreateValueTuple<T>(ICollection<T> collection)
    {
        object[] items;
        Type[] parameterTypes;
    
        if (collection.Count <= 7)
        {
            items = collection.Cast<object>().ToArray();
            parameterTypes = Enumerable.Repeat(typeof(T), collection.Count).ToArray();
        }
        else
        {
            var rest = CreateValueTuple(collection.Skip(7).ToArray());
            items = collection.Take(7).Cast<object>().Append(rest).ToArray();
            parameterTypes = Enumerable.Repeat(typeof(T), 7).Append(rest.GetType()).ToArray();
        }
    
        var createMethod = typeof(ValueTuple).GetMethods()
            .Where(m => m.Name == "Create" && m.GetParameters().Length == items.Length)
            .SingleOrDefault() ?? throw new NotSupportedException("ValueTuple.Create method not found.");
    
        var createGenericMethod = createMethod.MakeGenericMethod(parameterTypes);
    
        var valueTuple = createGenericMethod.Invoke(null, items);
        return valueTuple;
    }
    

    Sample use:

    var collection = new[] { 5, 6, 6, 2, 8, 4, 6, 2, 6, 8, 3, 6, 3, 7, 4, 1, 6 };
    var valueTuple = CreateValueTuple(collection);
    // result: (5, 6, 6, 2, 8, 4, 6, (2, 6, 8, 3, 6, 3, 7, (4, 1, 6)))
    

    If you don't mind Item8 being boxed, you could do away with reflection:

    public static object CreateValueTuple<T>(IList<T> list)
    {
        switch (list.Count)
        {
            case 0: return default(ValueTuple);
            case 1: return (list[0]);
            case 2: return (list[0], list[1]);
            case 3: return (list[0], list[1], list[2]);
            case 4: return (list[0], list[1], list[2], list[3]);
            case 5: return (list[0], list[1], list[2], list[3], list[4]);
            case 6: return (list[0], list[1], list[2], list[3], list[4], list[5]);
            case 7: return (list[0], list[1], list[2], list[3], list[4], list[5], list[6]);
            default: return (list[0], list[1], list[2], list[3], list[4], list[5], list[6], CreateValueTuple(list.Skip(7).ToList()));
        }
    }
    

    The difference is that the reflection-based method generates a result of type:

    ValueTuple<int,int,int,int,int,int,int,ValueTuple<ValueTuple<int,int,int,int,int,int,int,ValueTuple<ValueTuple<int,int,int>>>>>
    

    …whilst the switch-based method generates:

    ValueTuple<int,int,int,int,int,int,int,ValueTuple<object>>
    

    In each case, there is a redundant single-component ValueTuple<T> wrapping the nested value tuples. This is an unfortunate design flaw of the ValueTuple.Create<T1, …, T8> method implementation in the .NET Framework, and occurs even using the value tuple syntax (e.g. (1, 2, 3, 4, 5, 6, 7, (8, 9))).

    public static ValueTuple<T1, T2, T3, T4, T5, T6, T7, ValueTuple<T8>> Create<T1, T2, T3, T4, T5, T6, T7, T8>(T1 item1, T2 item2, T3 item3, T4 item4, T5 item5, T6 item6, T7 item7, T8 item8)
    {
        return new ValueTuple<T1, T2, T3, T4, T5, T6, T7, ValueTuple<T8>>(item1, item2, item3, item4, item5, item6, item7, ValueTuple.Create(item8));
    }
    

    As canton7 mentions, you can work around it by using the ValueTuple<T1, …, T7, TRest>() constructor directly, as shown in their answer.