检测一个对象是否为 ValueTuple

17
我有一个使用案例,需要检查一个值是否是 C# 7 值元组,如果是,则循环遍历每个项目。我尝试了 obj is ValueTupleobj is (object, object),但这两个都返回 false。我发现我可以使用 obj.GetType().Name 检查它是否以 "ValueTuple" 开头,但这对我来说似乎很糟糕。欢迎任何替代方法。
我还遇到了获取每个项目的问题。我尝试使用此处找到的解决方案获取 Item1如何在 c# 上检查动态匿名类型上是否存在属性? ,但是 ((dynamic)obj).GetType().GetProperty("Item1") 返回 null。我希望我可以使用 while 来获取每个项目。但是这不起作用。我怎样才能获取每个项目?
更新 - 更多代码
if (item is ValueTuple) //this does not work, but I can do a GetType and check the name
{
    object tupleValue;
    int nth = 1;
    while ((tupleValue = ((dynamic)item).GetType().GetProperty($"Item{nth}")) != null && //this does not work
        nth <= 8)      
    {
        nth++;
        //Do stuff
    }
}

1
ValueTuple是一个结构体,这就是为什么你需要使用GetType()的原因。你能贴更多的代码吗? - farbiondriven
调用 ((dynamic)item).GetType().GetProperties() 返回一个空数组... :( - James Esh
3
那些 Item1Item2 等不是属性,而是字段。因此,你需要执行 GetType().GetField("Item1")...。无需进行动态类型转换。 - Evk
3
我不禁思考这可能是一个X-Y问题。 - Matthew Watson
你可能需要考虑创建一个特定的方法来处理类型限制。然后,你可以使用像 where T : struct 这样的限制将其变成通用的。 - BurnsBA
2
此外,“重要的是,元组字段名不是元组的运行时表示的一部分,而仅由编译器跟踪。因此,字段名将不会对元组实例的第三方观察者(例如反射或动态代码)可用。” https://github.com/dotnet/roslyn/blob/master/docs/features/tuples.md#name-erasure-at-runtime- - BurnsBA
5个回答

13

C#中的结构体不会继承,因此ValueTuple<T1>ValueTuple<T1,T2>ValueTuple<T1,T2,T3>等都是不同的类型,它们不会从ValueTuple继承。因此,obj is ValueTuple检查失败。

如果您正在寻找任意类型参数的ValueTuple,可以按以下方式检查类是否为ValueTuple<...,>

private static readonly Set<Type> ValTupleTypes = new HashSet<Type>(
    new Type[] { typeof(ValueTuple<>), typeof(ValueTuple<,>),
                 typeof(ValueTuple<,,>), typeof(ValueTuple<,,,>),
                 typeof(ValueTuple<,,,,>), typeof(ValueTuple<,,,,,>),
                 typeof(ValueTuple<,,,,,,>), typeof(ValueTuple<,,,,,,,>)
    }
);
static bool IsValueTuple2(object obj) {
    var type = obj.GetType();
    return type.IsGenericType
        && ValTupleTypes.Contains(type.GetGenericTypeDefinition());
}

要按类型获取子项,您可以使用一种方法,这种方法并不特别快,但应该能解决问题:

static readonly IDictionary<Type,Func<object,object[]>> GetItems = new Dictionary<Type,Func<object,object[]>> {
    [typeof(ValueTuple<>)] = o => new object[] {((dynamic)o).Item1}
,   [typeof(ValueTuple<,>)] = o => new object[] {((dynamic)o).Item1, ((dynamic)o).Item2}
,   [typeof(ValueTuple<,,>)] = o => new object[] {((dynamic)o).Item1, ((dynamic)o).Item2, ((dynamic)o).Item3}
,   ...
};

这将让您做到这一点:

object[] items = null;
var type = obj.GetType();
if (type.IsGeneric && GetItems.TryGetValue(type.GetGenericTypeDefinition(), out var itemGetter)) {
    items = itemGetter(obj);
}

@JamesEsh 你试过 ((dynamic)obj).Item1 吗? - Sergey Kalinichenko
@JamesEsh 看看字典的小技巧是否适用于你。 - Sergey Kalinichenko
我想知道执行 ((dynamic)obj).Item1 和执行 obj.GetType().GetField("Item1").GetValue(obj) 哪个更快\更慢\大致相同? - Evk
@Evk 我认为在这两种情况下速度差不多。dynamic可能会稍微快一些,因为编译器可以跳过一些检查来获取值的调用序列,但是按名称获取字段的常见开销可能仍然会占主导地位。 - Sergey Kalinichenko
@Evk,请看我的答案,其中包含我的PCL扩展。感谢你们的帮助! - James Esh
显示剩余2条评论

9
关于问题的一部分“如何获取每个项目?”...
ValueTuple和Tuple都实现了ITuple接口,该接口具有长度属性和索引器属性。因此,以下控制台应用程序代码将值列出到控制台:
// SUT (as a local function)
IEnumerable<object> GetValuesFromTuple(System.Runtime.CompilerServices.ITuple tuple) 
{
    for (var i = 0; i < tuple.Length; i++)
        yield return tuple[i];
}

// arrange
var valueTuple = (StringProp: "abc", IntProp: 123, BoolProp: false, GuidProp: Guid.Empty);

// act
var values = GetValuesFromTuple(valueTuple);

// assert (to console)
Console.WriteLine($"Values = '{values.Count()}'");

foreach (var value in values)
{
    Console.WriteLine($"Value = '{value}'");
}

控制台输出:

Values = '4'
Value = 'abc'
Value = '123'  
Value = 'False'  
Value = '00000000-0000-0000-0000-000000000000'

7
值得一提的是,ITuple 仅在4.7.1及以上版本中可用。 - adaskos

6
这是我对问题的解决方案。一个兼容PCL的扩展类。特别感谢@dasblinkenlight和@Evk帮助我!
public static class TupleExtensions
{
    private static readonly HashSet<Type> ValueTupleTypes = new HashSet<Type>(new Type[]
    {
        typeof(ValueTuple<>),
        typeof(ValueTuple<,>),
        typeof(ValueTuple<,,>),
        typeof(ValueTuple<,,,>),
        typeof(ValueTuple<,,,,>),
        typeof(ValueTuple<,,,,,>),
        typeof(ValueTuple<,,,,,,>),
        typeof(ValueTuple<,,,,,,,>)
    });

    public static bool IsValueTuple(this object obj) => IsValueTupleType(obj.GetType());
    public static bool IsValueTupleType(this Type type)
    {
        return type.GetTypeInfo().IsGenericType && ValueTupleTypes.Contains(type.GetGenericTypeDefinition());
    }

    public static List<object> GetValueTupleItemObjects(this object tuple) => GetValueTupleItemFields(tuple.GetType()).Select(f => f.GetValue(tuple)).ToList();
    public static List<Type> GetValueTupleItemTypes(this Type tupleType) => GetValueTupleItemFields(tupleType).Select(f => f.FieldType).ToList();    
    public static List<FieldInfo> GetValueTupleItemFields(this Type tupleType)
    {
        var items = new List<FieldInfo>();

        FieldInfo field;
        int nth = 1;
        while ((field = tupleType.GetRuntimeField($"Item{nth}")) != null)
        {
            nth++;
            items.Add(field);
        }

        return items;
    }
}

更好的解决方案在这里得到解决:https://dev59.com/lVcQ5IYBdhLWcg3wFf49 - greektreat

2
如果你不喜欢事先构建哈希表,可以尝试类似这样的方法:
public static bool IsTupleType(this Type type)
{
    return typeof(ITuple).IsAssignableFrom(type);
}

public static bool IsValueTupleType(this Type type)
{
    return type.IsValueType && type.IsTupleType();
}

public static bool IsReferenceTupleType(this Type type)
{
    return type.IsClass && type.IsTupleType();
}

高效,易于阅读和维护。


0

黑客式一行代码

type.Name.StartsWith("ValueTuple`")

(可以扩展到检查末尾的数字)


2
这已经在问题中说明了:我发现我可以使用 obj.GetType().Name 并检查它是否以 "ValueTuple" 开头,但这似乎很糟糕。 - Lance U. Matthews
啊,我明白了,抱歉我错过了那个。说实话,虽然有点hackish,但可能完全稳定。 - kofifus

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