如何动态创建 .NET C# ValueType 元组?
How to create a .NET C# ValueType Tuple dynamically?
我想从值集合中动态创建一个值类型元组。
示例:我有一个给定的 IEnumerable<T>
,我想基于该集合创建一个元组。
我怎样才能做到这一点?
It seems that the access within a value type tuple can be achieved dynamically 但没有任何迹象表明可以为创建值类型元组做同样的事情。
我的目的之一是利用 article
中描述的此类元组的 Equality 和 HashCode 的属性
问题仍然不清楚,但我假设您想将 a
集合转换为 (a[0], a[1], a[2], …)
形式的值元组。这不受任何内置功能的支持。此外,您很快就会 运行 进入限制,因为 .NET Framework 最多只定义 ValueTuple<T1, …, T7>
– going beyond that would require you to construct unwieldy nested value tuples using 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));
}
正如 canton7 提到的,您可以直接使用 ValueTuple<T1, …, T7, TRest>()
构造函数来解决它,如 .
所示
回答实际问题,对于任何感兴趣的人...
正如其他人所说,如果您只是想确定两个序列是否相等,或者获取两个序列的哈希码,不要这样做。有更好、更便宜的方法可以做到这一点。
它有点复杂。 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;
}
}
仅供参考,我这样做是为了在我的 EntityFrameworkCore 模拟库中生成一个密钥 here。
但正如 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);
我想从值集合中动态创建一个值类型元组。
示例:我有一个给定的 IEnumerable<T>
,我想基于该集合创建一个元组。
我怎样才能做到这一点?
It seems that the access within a value type tuple can be achieved dynamically 但没有任何迹象表明可以为创建值类型元组做同样的事情。
我的目的之一是利用 article
中描述的此类元组的 Equality 和 HashCode 的属性问题仍然不清楚,但我假设您想将 a
集合转换为 (a[0], a[1], a[2], …)
形式的值元组。这不受任何内置功能的支持。此外,您很快就会 运行 进入限制,因为 .NET Framework 最多只定义 ValueTuple<T1, …, T7>
– going beyond that would require you to construct unwieldy nested value tuples using 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));
}
正如 canton7 提到的,您可以直接使用 ValueTuple<T1, …, T7, TRest>()
构造函数来解决它,如
回答实际问题,对于任何感兴趣的人...
正如其他人所说,如果您只是想确定两个序列是否相等,或者获取两个序列的哈希码,不要这样做。有更好、更便宜的方法可以做到这一点。
它有点复杂。 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;
}
}
仅供参考,我这样做是为了在我的 EntityFrameworkCore 模拟库中生成一个密钥 here。
但正如 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);