3

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

Natalie Perret
  • 8,013
  • 12
  • 66
  • 129
  • Can you give an example of what you're trying to achieve? It is trivial to create value tuples dynamically; e.g. `(myInt, myString)` – Douglas Mar 19 '19 at 10:07
  • @Douglas "dynamically from say a collection of values.". Let's say we have an array `a` of unknown length `l`, how can you create tuple `t` of length `l` based on `a`. About the use I gave one example of purpose that I would like to leverage. – Natalie Perret Mar 19 '19 at 10:22
  • That's not remotely the purpose of `ValueTuple`. If you just want to steal the code for implementing `.Equals()` and `.GetHashCode()` over multiple values, [steal away](https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/ValueTuple.cs#L263) -- it's not particularly complicated to adapt such code to work on collections. That's certainly a lot cleaner than trying to dynamically create `ValueTuple`s, which gets downright uncomfortable for more than 7 items. Doubtless there's also lots of libs implementing this one way or another (e.g. for unit testing). – Jeroen Mostert Mar 19 '19 at 10:31
  • It was one of my purposes, not the only one, other purposes include, curiosity, twisted reflection and whatnot. – Natalie Perret Mar 19 '19 at 10:34
  • @EhouarnPerret - Are you trying to do this so that you can see if two sequences of values are equal? – Enigmativity Mar 19 '19 at 10:34
  • @Enigmativity no, not at all, I gave one example but I am just curious about the feasibility. There is something a bit redundant on a SO, is that when someone asks for X, often people willing to help will come with do you want to actually do Y(often cause that workaround for a different goal is easier)? While I just want to do X. I just want to see if it was possible to create value type tuple without hardcoding it. That's it – Natalie Perret Mar 19 '19 at 10:37
  • @EhouarnPerret - I think in this case you're starting with `W` and are asking for `X`, but `X` is more limiting than the original `W`. – Enigmativity Mar 19 '19 at 10:38
  • It's a natural consequence of trying to head off prolonged discussions about [XY problems](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). Nothing malicious about it -- sometimes being helpful requires going a bit further than just answering the bare question. We can't smell from the question itself that you're just curious. The actual task of creating a `ValueTuple` of arbitrary size dynamically is interesting, but only precisely because the results would be impractical. – Jeroen Mostert Mar 19 '19 at 10:40
  • @Enigmativity it was an example of purpose since I know that on SO such questions without "practical" purposes rarely got answers. – Natalie Perret Mar 19 '19 at 10:40

3 Answers3

5

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.

Douglas
  • 53,759
  • 13
  • 140
  • 188
  • For some reason it irks me that there's no simple, clean way of writing an expression for "the `ValueTuple` type with the highest generic arity", leaving us stuck with the magic constant 7. (I know that's not very rational since the whole thing should never be used anyway.) ``typeof(ValueTuple).Assembly.GetTypes().Where(t => t.Name.StartsWith("ValueTuple`")).Select(t => t.GetGenericArguments().Length).Max() - 1`` does not make me happy. C# 9 better have meta-generic constructs, or something! – Jeroen Mostert Mar 19 '19 at 11:19
  • I like that last switch statement-based one! It doesn't matter that `Item8` gets boxed - it got boxed in the reflection-based solution as well. – canton7 Mar 19 '19 at 13:04
  • Wait, no, the switch-based one isn't right. Try it with `new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }` and it creates a `(1, 2, 3, 4, 5, 6, 7, (8, 9))`. This is because it creates a `ValueTuple` not a `ValueTuple>`. It doesn't look like there is a `ValueTuple.Create` overload which does this, either. – canton7 Mar 19 '19 at 13:09
  • Hmm, looks like your first solution suffers this same problem, as well. You need to use `new ValueTuple<......>` for the one with `TRest`, not `ValueTuple.Create`. Not that it matters a jot for generating equality/hashcodes, but it's the principle of the thing... – canton7 Mar 19 '19 at 13:12
  • @canton7: You're right about the redundant `ValueTuple` wrapper; that is a surprising design flaw of `ValueTuple.Create`. It even occurs for the native syntax (`var valueTuple = (1, 2, 3, 4, 5, 6, 7, (8, 9));`), so Microsoft apparently weren't too bothered by it. As for the `ValueTuple` resulting from the switch-based method, yes, that's what I meant by "boxing". Equality comparisons still work (albeit slightly less efficiently), since `ValueTuple` overrides `Equals(object)`. – Douglas Mar 19 '19 at 13:32
  • Ah I see what you meant with the boxing - not that the `ValueTuple` was boxed in the first place, but that the box stays referenced. – canton7 Mar 19 '19 at 13:37
  • @canton7: Exactly; that's a better description for it. – Douglas Mar 19 '19 at 13:38
2

To answer the actual question, for anyone who's interested...

As others have said, do not do this if you simply want to determine whether two sequences are equal, or get the hashcode of two sequences. There are much better, cheaper ways of doing this.

It gets a bit involved. The BCL defines ValueTuple<T>, ValueTuple<T1, T2>, etc, up to ValueTuple<T1, T2, T3, T4, T5, T6, T7>. After that, you need to use ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>, where TRest is itself a ValueTuple of some kind (and they can be chained like this).

public static class Program
{
    private const int maxTupleMembers = 7;
    private const int maxTupleArity = maxTupleMembers + 1;
    private static readonly Type[] tupleTypes = new[]
    {
        typeof(ValueTuple<>),
        typeof(ValueTuple<,>),
        typeof(ValueTuple<,,>),
        typeof(ValueTuple<,,,>),
        typeof(ValueTuple<,,,,>),
        typeof(ValueTuple<,,,,,>),
        typeof(ValueTuple<,,,,,,>),
        typeof(ValueTuple<,,,,,,,>),
    };

    public static void Main()
    {
        var a = CreateTuple(new[] { 1 });
        var b = CreateTuple(new[] { 1, 2, 3, 4, 5, 6, 7 });
        var c = CreateTuple(new[] { 1, 2, 3, 4, 5, 6, 7, 8 });
        var d = CreateTuple(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 });
        var e = CreateTuple(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 });
    }

    private static object CreateTuple<T>(IReadOnlyList<T> values)
    {
        int numTuples = (int)Math.Ceiling((double)values.Count / maxTupleMembers);

        object currentTuple = null;
        Type currentTupleType = null;

        // We need to work backwards, from the last tuple
        for (int tupleIndex = numTuples - 1; tupleIndex >= 0; tupleIndex--)
        {
            bool hasRest = currentTuple != null;
            int numTupleMembers = hasRest ? maxTupleMembers : values.Count - (maxTupleMembers * tupleIndex);
            int tupleArity = numTupleMembers + (hasRest ? 1 : 0);

            var typeArguments = new Type[tupleArity];
            object[] ctorParameters = new object[tupleArity];
            for (int i = 0; i < numTupleMembers; i++)
            {
                typeArguments[i] = typeof(T);
                ctorParameters[i] = values[tupleIndex * maxTupleMembers + i];
            }
            if (hasRest)
            {
                typeArguments[typeArguments.Length - 1] = currentTupleType;
                ctorParameters[ctorParameters.Length - 1] = currentTuple;
            }

            currentTupleType = tupleTypes[tupleArity - 1].MakeGenericType(typeArguments);
            currentTuple = currentTupleType.GetConstructors()[0].Invoke(ctorParameters);
        }

        return currentTuple;
    }
}
canton7
  • 37,633
  • 3
  • 64
  • 77
  • 1
    Well, that's certainly better than what I was cooking up. As an exercise to the reader, try solving this with recursion. It's loads of fun. (For certain values of "fun".) – Jeroen Mostert Mar 19 '19 at 11:07
  • Wow, that's a lot of code. Probably does the same as the code in my [answer](https://stackoverflow.com/a/55239715/1300910), no? – huysentruitw Mar 19 '19 at 11:20
  • Never mind, I see you're working around the maximum tuple members. – huysentruitw Mar 19 '19 at 11:23
  • @huysentruitw try yours with `new[] { 1, 2, 3, 4, 5, 6, 7, 8 }`. Yours throws, mine (and @Douglas's) work. – canton7 Mar 19 '19 at 11:23
  • @huysentruitw: it's what we call "this one goes up to 11". – Jeroen Mostert Mar 19 '19 at 11:23
  • Yeah just figured that out. I think someone could combine both snippets to create something neat. If we don't care about performance/memory, we could just make 2 item tuples, recursivly and get rid of the double for-loop. :) – huysentruitw Mar 19 '19 at 11:24
  • @huysentruitw Depends how you interpret the question. Strictly speaking, `(1, 2, 3, 4, 5, 6, 7, 8)` is different to `(1, (2, (3, (4, (5, (6, (7, 8))))))` – canton7 Mar 19 '19 at 11:32
0

Just for reference, I'm doing this do generate a key in my EntityFrameworkCore mock library here.

But as pointed out by Douglas, the ValueTuple definitions are limited to 7 parameters, but for the usecase of the mock library, this is just fine.

Anyway, in essence, the code would look like this:

var valueTupleType = Type.GetType($"System.ValueTuple`{collection.Length}")
    ?? throw new InvalidOperationException($"No ValueTuple type found for {collection.Length} generic arguments");

var itemTypes = collection.Select(x => x.GetType()).ToArray();
var constructor = valueTupleType.MakeGenericType(itemTypes).GetConstructor(itemTypes)
    ?? throw new InvalidOperationException("No ValueTuple constructor found for key values");

var valueTuple = constructor.Invoke(collection);
huysentruitw
  • 27,376
  • 9
  • 90
  • 133