Detect if an object is a ValueTuple
Asked Answered
N

5

19

I have a use case where I need to check if a value is a C# 7 ValueTuple, and if so, loop through each of the items. I tried checking with obj is ValueTuple and obj is (object, object) but both of those return false. I found that I could use obj.GetType().Name and check if it starts with "ValueTuple" but that seems lame to me. Any alternatives would be welcomed.

I also have the issue of getting each item. I attempted to get Item1 with the solution found here: How do I check if a property exists on a dynamic anonymous type in c#? but ((dynamic)obj).GetType().GetProperty("Item1") returns null. My hope was that I could then do a while to get each item. But this does not work. How can I get each item?

Update - more code

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
    }
}
Nonresistance answered 12/10, 2017 at 10:40 Comment(6)
ValueTuple is a Structure, that's why you need GetType(). Can you post more code ?Deductive
Calling ((dynamic)item).GetType().GetProperties() returns an empty array... :(Nonresistance
Those Item1 Item2 etc are not properties - they are fields. So you have to do GetType().GetField("Item1").... Casting to dynamic is not needed.Glasscock
I can't help but think that this might be an X-Y problem.Epirus
You may want to consider having a specific method to deal with your type restriction. You can then make it a generic with a restriction like where T : struct.Hypozeugma
Also, "Importantly, the tuple field names aren't part of the runtime representation of tuples, but are tracked only by the compiler. As a result, the field names will not be available to a 3rd party observer of a tuple instance - such as reflection or dynamic code." github.com/dotnet/roslyn/blob/master/docs/features/…Hypozeugma
N
6

This is my solution to the problem. A PCL compatible extension class. Special thanks to @dasblinkenlight and @Evk for helping me out!

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;
    }
}
Nonresistance answered 12/10, 2017 at 11:54 Comment(1)
better solution is solved here: #43341677Ene
I
13

Structures do not inherit in C#, so ValueTuple<T1>, ValueTuple<T1,T2>, ValueTuple<T1,T2,T3> and so on are distinct types that do not inherit from ValueTuple as their base. Hence, obj is ValueTuple check fails.

If you are looking for ValueTuple with arbitrary type arguments, you can check if the class is ValueTuple<,...,> as follows:

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

To get sub-items based on the type you could use an approach that is not particularly fast, but should do the trick:

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

This would let you do this:

object[] items = null;
var type = obj.GetType();
if (type.IsGeneric && GetItems.TryGetValue(type.GetGenericTypeDefinition(), out var itemGetter)) {
    items = itemGetter(obj);
}
Isotope answered 12/10, 2017 at 10:52 Comment(7)
Thanks for the pointer, although I will probably stick to the type name check, as it gets any tuple. Do you know how I can get each item though?Nonresistance
@JamesEsh Did you try ((dynamic)obj).Item1?Isotope
Yes! Any ideas on how to do it dynamically?Nonresistance
@JamesEsh See if the little trick with the dictionary works for you.Isotope
I wonder if doing ((dynamic)obj).Item1 is faster\slower\about the same as doing obj.GetType().GetField("Item1").GetValue(obj)?Glasscock
@Glasscock I think one will get roughly the same low speed in both cases. dynamic may be a bit faster because the compiler could skip a few checks in the sequence of calls that retrieve the value, but the common overhead of getting a field by name would probably dominate anyway.Isotope
dasblinkenlight & @Glasscock see my answer for my PCL extension. Thanks for yall's help!Nonresistance
F
9

Regarding the part of the question "How can I get each item?"...

Both ValueTuple and Tuple implement ITuple, which has a length property and an indexer property. So a the following console app code lists the values to the console:

// 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}'");
}

Console output:

Values = '4'
Value = 'abc'
Value = '123'  
Value = 'False'  
Value = '00000000-0000-0000-0000-000000000000'
Flaky answered 3/11, 2017 at 12:56 Comment(1)
It's worth mentioning that ITuple is only available since 4.7.1.Avant
N
6

This is my solution to the problem. A PCL compatible extension class. Special thanks to @dasblinkenlight and @Evk for helping me out!

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;
    }
}
Nonresistance answered 12/10, 2017 at 11:54 Comment(1)
better solution is solved here: #43341677Ene
T
5

If you dont like building hash tables upfront, something like this will work:

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

Efficient, and easier to read and maintain.

Towney answered 27/3, 2023 at 4:10 Comment(0)
Y
0

hackish one liner

type.Name.StartsWith("ValueTuple`")

(can be extended to check the digit at the end)

Yesteryear answered 8/11, 2021 at 22:20 Comment(2)
This was already stated in the question: I found that I could use obj.GetType().Name and check if it starts with "ValueTuple" but that seems lame to me.Centurion
Ah I see sorry I missed that, tbh while hackish it is probably totally solidYesteryear

© 2022 - 2024 — McMap. All rights reserved.