Как динамически создать .NET C # ValueType Tuple? - PullRequest
2 голосов
/ 19 марта 2019

Я хотел бы создать кортеж типа значения динамически, скажем, из коллекции значений.

Пример: у меня есть данный IEnumerable<T>, и я хотел бы создать кортеж на основе этой коллекции.

Как мне этого добиться?

Кажется, что доступ в кортеже типа значения может быть достигнут динамически , но ничто не указывает на то, что то же самое можно сделать для создания кортежа типа значения.

Одной из моих целей было бы использование свойств Equality и HashCode таких кортежей, как описано в этой статье

Ответы [ 3 ]

4 голосов
/ 19 марта 2019

Вопрос все еще неясен, но я предполагаю, что вы хотите преобразовать свою коллекцию a в кортеж значений в форме (a[0], a[1], a[2], …).Это не поддерживается никакими встроенными функциями.Кроме того, вы быстро столкнетесь с ограничениями, поскольку .NET Framework определяет только до ValueTuple<T1, …, T7> - выход за пределы этого потребует от вас создания громоздких вложенных кортежей значений с использованием ValueTuple<T1, …, T7, TRest> (например, ValueTuple<T1, …, T7, ValueTuple<T1, …, T7, ValueTuple<T1, …>>>).

Если вы хотите добиться сравнения равенства коллекций, вы должны использовать вместо него IEqualityComparer<ICollection<T>>.Вот пример реализации из 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;
        }
    }
}

Редактировать : для удовольствия, вот моя реализация вашего реального вопроса, с использованием отражения ирекурсия:

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;
}

Пример использования:

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

Если вы не возражаете против Item8 в штучной упаковке, вы можете покончить с отражением:

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()));
    }
}

Разница в том, что метод на основе отражения генерирует результат типа:

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

… в то время как метод на основе коммутатора генерирует:

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

В каждом случае естьизбыточный однокомпонентный ValueTuple<T>, оборачивающий вложенные значения.Это прискорбный недостаток реализации метода ValueTuple.Create<T1, …, T8> в .NET Framework, и он возникает даже при использовании синтаксиса кортежей значений (например, (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));
}

Как упоминает canton7, вы можете работатьвокруг него, используя непосредственно конструктор ValueTuple<T1, …, T7, TRest>(), как показано в их ответе .

2 голосов
/ 19 марта 2019

Чтобы ответить на актуальный вопрос, для всех, кто заинтересован ...

Как уже говорили другие, не делайте это, если вы просто хотите определить, равны ли две последовательности, или получить хеш-код двух последовательностей. Есть намного лучшие, более дешевые способы сделать это.

Это немного запутано. BCL определяет ValueTuple<T>, ValueTuple<T1, T2> и т. Д. До ValueTuple<T1, T2, T3, T4, T5, T6, T7>. После этого вам нужно использовать ValueTuple<T1, T2, T3, T4, T5, T6, T7, TRest>, где TRest сам по себе является ValueTuple некоторого вида (и они могут быть связаны таким образом).

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;
    }
}
0 голосов
/ 19 марта 2019

Просто для справки, я делаю это, генерирую ключ в моей фиктивной библиотеке EntityFrameworkCore здесь .

Но, как указал Дуглас, определения ValueTuple ограничены 7 параметрами, но для сценария использования библиотеки-макета это просто отлично.

В любом случае, по сути, код будет выглядеть так:

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