如何动态创建一个.NET C#值类型元组?

3
我想从一个值的集合动态地创建一个值类型元组。
例如:给定一个 IEnumerable<T>,我想基于该集合创建一个元组。
我该如何实现? 似乎可以通过反射动态访问值类型元组,但没有任何迹象表明可以对值类型元组进行创建。
我的一个目的是利用这些元组的相等和哈希码属性,就像在这篇文章中所描述的那样。

你能举个例子说明你想要实现什么吗?动态创建值元组非常简单;例如:(myInt, myString) - Douglas
这与 ValueTuple 的目的完全不同。如果您只想窃取实现多个值上的 .Equals().GetHashCode() 的代码,尽管拿去 -- 将这样的代码适应到集合上并不特别复杂。这肯定比尝试动态创建 ValueTuple 更加简洁,对于超过7个项目,这种方法会变得非常不舒服。毫无疑问,也有很多库以某种方式实现了这一点(例如用于单元测试)。 - Jeroen Mostert
这是我其中一个目的,不是唯一的目的,其他目的包括好奇心、扭曲的反思等等。 - Natalie Perret
@EhouarnPerret - 你是想这样做来判断两个值序列是否相等吗? - Enigmativity
@Enigmativity 这只是一个示例,因为我知道在SO上,没有“实际”目的的问题很少会得到答案。 - Natalie Perret
显示剩余4条评论
3个回答

5
问题仍然不清楚,但我认为你想将你的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>包装嵌套的值元组。这是.NET Framework的ValueTuple.Create<T1, …, T8>方法实现的一个不幸的设计缺陷,即使使用值元组语法(例如(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));
}
ValueTuple<T1, …, T7, TRest>()构造函数来解决此问题,如他们的答案所示。


由于某种原因,我感到不爽的是没有一种简单、干净的方法来编写一个表达式,表示“具有最高泛型数的ValueTuple类型”,这让我们陷入了使用魔法常量7的困境。(我知道这并不是很理性,因为整个东西本来就不应该被使用。)typeof(ValueTuple).Assembly.GetTypes().Where(t => t.Name.StartsWith("ValueTuple`")).Select(t => t.GetGenericArguments().Length).Max() - 1 让我不开心。C# 9 最好有元泛型构造,或者其他什么东西! - Jeroen Mostert
等等,不对,基于 switch 的那个不对。试试用 new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } 来创建一个 (1, 2, 3, 4, 5, 6, 7, (8, 9))。这是因为它创建了一个 ValueTuple<int, int, int, int, int, int, int, object> 而不是 ValueTuple<int, int, int, int, int, int, int, ValueTuple<int, int>>。看起来也没有一个 ValueTuple.Create 重载可以做到这一点。 - canton7
嗯,看起来你的第一个解决方案也有同样的问题。你需要在带有TRest的那个元组中使用new ValueTuple<......>,而不是ValueTuple.Create。虽然这对于生成相等性/哈希码没有任何影响,但这是原则问题... - canton7
@canton7:你说的关于冗余的 ValueTuple<T> 包装器是正确的;这是 ValueTuple.Create 的一个令人惊讶的设计缺陷。即使对于原生语法 (var valueTuple = (1, 2, 3, 4, 5, 6, 7, (8, 9));),微软显然也不太在意它。至于基于 switch 的方法产生的 ValueTuple<T1, …, T7, object>,是的,这就是我所说的“装箱”。相等比较仍然有效(尽管效率稍低),因为 ValueTuple 覆盖了 Equals(object) - Douglas
@canton7:没错,这是更好的描述。 - Douglas
显示剩余2条评论

2
为了回答实际问题,对于任何感兴趣的人...
正如其他人所说,如果您只想确定两个序列是否相等或获取两个序列的哈希码,请不要这样做。有更好、更便宜的方法来处理这个问题。
这有点复杂。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;
    }
}

1
那肯定比我想的要好。作为读者的练习,尝试使用递归来解决这个问题。这将会非常有趣。(对于某些“有趣”的值) - Jeroen Mostert
哇,那是很多代码。可能和我回答中的代码一样吧? - huysentruitw
没关系,我看到你正在解决元组成员数量的限制问题。 - huysentruitw
@huysentruitw 请使用 new[] {1, 2, 3, 4, 5, 6, 7, 8} 进行尝试。你的代码会抛出异常,而我的(以及 @Douglas 的)则可以正常工作。 - canton7
@huysentruitw 取决于你如何解释这个问题。严格来说,(1, 2, 3, 4, 5, 6, 7, 8)(1, (2, (3, (4, (5, (6, (7, 8)))))) 是不同的。 - canton7
显示剩余2条评论

0

仅供参考,我正在这里生成我的EntityFrameworkCore模拟库链接中的密钥。

但正如Douglas所指出的那样,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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接