13

How do I check if a type T fits the unmanaged type constraint, such that it could be used in a context like this: class Foo<T> where T : unmanaged? My first idea was typeof(T).IsUnmanaged or something similar, but that isn't a property/field of the Type class

adjan
  • 13,371
  • 2
  • 31
  • 48
John
  • 598
  • 2
  • 7
  • 22
  • That's a type constraint for a generic. I'm asking for a given type `T`, at runtime, how can I check if it is unmanaged or not? – John Dec 29 '18 at 11:04

2 Answers2

10

According to unmanaged constraint documentations:

An unmanaged type is a type that is not a reference type and doesn't contain reference type fields at any level of nesting.

Also it's mentioned in C# language design documentations about unmanaged type constraint:

In order to satisfy this constraint a type must be a struct and all the fields of the type must fall into one of the following categories:

  • Have the type sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, IntPtr or UIntPtr.
  • Be any enum type.
  • Be a pointer type.
  • Be a user defined struct that satisfies the unmanaged constraint.

Considerations

Usually calling MakeGenericType is the most reliable solution for validating generic type constraints which are enforced by CRL. Usually trying to implement validation by yourself is not a good idea because there may be a lot of rules which you should consider and there is always a chance for missing some of them. But be informed, at least at time of writing this answer, it's not working well for unmanaged constraint.

.NET Core have a RuntimeHelpers.IsReferenceOrContainsReferences but at the time of writing this answer, .NET Framework doesn't have such function. I should mention that even using IsReferenceOrContainsReferences is not completely reliable for this task.

For example see the issue which I posted here about two structure which doesn't have any reference type but one of them evaluated as managed, one of them unmanaged (maybe a compiler bug).

Anyway, for now depending to your preference and requirements, use one of the following solutions to detect which type can satisfy unmanaged generic type constraint.

Option 1 - Using MakeGenericType

As an option, to check if the type can satisfy the unmanaged constraint, you can use the following IsUnmanaged extension method'.

C# 7.3: It is supposed to be more reliable, but I should say, it's not. It seems for unmanaged constraint, CLR is not respecting the constraint and it's just a C# compiler feature. So at least for now, I recommend using the second option.

C# 8.0: Works as expected in C# 8.0

using System;
using System.Reflection;
public static class UnmanagedTypeExtensions
{
    class U<T> where T : unmanaged { }
    public static bool IsUnManaged(this Type t)
    {
        try { typeof(U<>).MakeGenericType(t); return true; }
        catch (Exception){ return false; }
    }
}

Option 2 - Writing your own method checking the documented rules

As another option, you can write your method checking documented rules for unmanaged constraint. The following code has more rules rather than other answer to be able to handle cases like int? or (int,int):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
public static class UnmanagedTypeExtensions
{
    private static Dictionary<Type, bool> cachedTypes =
    new Dictionary<Type, bool>();
    public static bool IsUnManaged(this Type t)
    {
        var result = false;
        if (cachedTypes.ContainsKey(t))
            return cachedTypes[t];
        else if (t.IsPrimitive || t.IsPointer || t.IsEnum)
            result = true;
        else if (t.IsGenericType || !t.IsValueType)
            result = false;
        else
            result = t.GetFields(BindingFlags.Public | 
               BindingFlags.NonPublic | BindingFlags.Instance)
                .All(x => x.FieldType.IsUnManaged());
        cachedTypes.Add(t, result);
        return result;
    }
}

More Information

You may find the following links useful:

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • Answer as it much quicker than the other answer, even if slightly more complex than the other. Thanks for the answer :) – John Dec 29 '18 at 12:24
  • I also like the other answer, but to me, the code which is shared in my answer is more reliable and future proof as it relies on `unmanaged` constraints. – Reza Aghaei Dec 29 '18 at 12:27
  • My edit was refused [ :( ], but i suggest you include the early check of whether it is primitive/pointer/enum below (it must be unmanaged) and then if it isnt a value type (in which case it can't be managed). Saves the expensive reflection for simple cases. Also worth adding - in .NET Core there is `RuntimeHelpers.IsReferenceOrContainsReferences()`, an inbuilt function for this purpose – John Dec 29 '18 at 19:14
  • @John I didn't see your edit, but will consider your comment and after some investigation I'll probably update the answer. Thanks for your comment :) – Reza Aghaei Dec 29 '18 at 19:16
  • Option 1 doesn't work. I just tested it on a struct that has a string field and it returned `true`. Because the CLR doesn't enforce the constraint, it won't throw an exception if you provide a match for the constraint it *can* check: `T : struct`. And if you inspect the constraint info, you'll see that it's *identical* for `T : struct` and `T : unmanaged`. The 'unmanaged' specifics are lost at runtime. Reminds me of generics in Java. – madreflection Jan 01 '19 at 06:34
  • @madreflection I see, at least at compile time `unmanaged` is more than `struct`, but at runtime it acts like `strucct` constraint. It's expected that all generic constraint be respected at run-time, At least C#/CLR used to do that. that's why I shared the first solution (at least to be more future-proof) .But it seems there are some CRL/Compiler bug/ about it. Cases like `int?` or `(int,int)` or structure containing "string" pushed me to the other solution. – Reza Aghaei Jan 01 '19 at 06:42
  • @madreflection During writing the code for the second option I faces with a strage behavior which is not limited to `unmanaged` constraint and I believe it's more related to tuple and the way that compiler/run-time treat it. I posted the [issue here](https://stackoverflow.com/q/53992855/3110834). The post is more about `unmanaged` constraint, but there is another problem in the post as well. Pointers. `(int,int)*` is not valid pointer, but having `struct MyStruct{(int,int) i;}` then `MyStruct*` is valid pointer! – Reza Aghaei Jan 01 '19 at 06:46
  • It's not a bug, it's by design, and documented quite clearly in the linked design document. C# 7.3, by introducing the added restrictions of the unmanaged constraint, made that expectation no longer entirely valid. The unmanaged constraint cannot be relied upon at runtime because it would have required CLR changes to support it at runtime and they didn't have the latitude to do so, so they did as much as they could do with just the compiler. – madreflection Jan 01 '19 at 06:59
  • Yes, they declared it as a feature of language. But the compiler implementation has some problems. See the [linked post](https://stackoverflow.com/q/53992855/3110834). – Reza Aghaei Jan 01 '19 at 07:05
  • 1
    FYI - this doesn't work perfectly, and will return `false` for any pointer types at the moment. Pointer types don't inherit from `System.Object` and therefore do not inherit from `System.ValueType`, so `t.IsPointer` must be checked before `!t.IsValueType` to ensure correct results – John Jan 01 '19 at 10:20
  • @John, thanks, fixed. (But still is not perfect because of tuple case, which I asked in the linked post. It seems to have a robust solution for it, we should not rely on run-time. See [this post](https://github.com/dotnet/roslyn/issues/27474#issuecomment-394941170). – Reza Aghaei Jan 01 '19 at 11:26
3

I am not sure if something like this already exists, but you could implement your own extension method similar to:

public static bool IsUnmanaged(this Type type)
{
    // primitive, pointer or enum -> true
    if (type.IsPrimitive || type.IsPointer || type.IsEnum)
        return true;

    // not a struct -> false
    if (!type.IsValueType)
        return false;

    // otherwise check recursively
    return type
        .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
        .All(f => IsUnmanaged(f.FieldType));
}

(update) For completeness, since recursion will be slow for structs with many nested members, the function can be made faster by caching the results:

private static readonly ConcurrentDictionary<Type, bool> _memoized = 
    new ConcurrentDictionary<Type, bool>();

public static bool IsUnmanaged(this Type type)
{
    bool answer;

    // check if we already know the answer
    if (!_memoized.TryGetValue(type, out answer))
    {

        if (!type.IsValueType)
        {
            // not a struct -> false
            answer = false;
        }
        else if (type.IsPrimitive || type.IsPointer || type.IsEnum)
        {
            // primitive, pointer or enum -> true
            answer = true;
        }
        else
        {
            // otherwise check recursively
            answer = type
                .GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
                .All(f => IsUnmanaged(f.FieldType));
        }

        _memoized[type] = answer;
    }

    return answer;
}
vgru
  • 49,838
  • 16
  • 120
  • 201
  • Thanks for the answer. I chose the other one, as while I think this one is prettier, it is much slower (my test got it as 10-20x slower), and it only gets slower with larger objects. – John Dec 29 '18 at 12:23
  • @John: hi, yes, the recursion if probably a killer here. I would assume that a cached/memoized version would be faster than the accepted one (i.e. one that uses a dictionary to store known results), especially since Reza's answer uses reflection (which is usually slow) and exceptions (which are slow -- although only if they are thrown). – vgru Dec 29 '18 at 13:31
  • @John: I've added the updated method, just to clarify what I meant. It should be as fast as the accepted answer, but it will use a bit more memory for the duration of the program (since the dictionary is static). I have used a ConcurrentDictionary to make the method thread safe, but if you know that this will be used from a single thread it can be made even faster using a plain dictionary. Of course, the accepted answer can use the same tactic to get the answer faster, if it's shown that it's needed. :) – vgru Dec 29 '18 at 14:05
  • this is not an O(1) algorithm, whereas the other is. This is, i think, O(n), as you need to check everything – John Dec 29 '18 at 14:07
  • @John: yes, this is `O(n)` on the first call, where `n` is the total number of fields within the struct (including all nested structs). Next call to this struct (and all members that were cached on the first call) is `O(1)`, likely faster than reflection, and very likely faster than the case when the original code throws. But the best way to make sure this is true is to call both functions a million times and compare the elapsed time. Calling it once or twice will certainly be slower. – vgru Dec 29 '18 at 14:25
  • The accepted answer is good because it is certain to work with all edge cases which my answer possibly might not cover. Perhaps the documentation isn't fully correct, perhaps I made a bug in my implementation; you won't have these problems with the accepted answer. And the other code is faster, unless you use memoization with the recursive approach. On the other hand, it uses `try`/`catch` as a method of checking whether instantiation will succeed, which is usually considered bad practice, but sometimes the only reliable way. It's possible a future version of .NET might include this property. – vgru Dec 29 '18 at 14:30
  • 1
    @Groo I added more details to the answer because it seems `unmanaged` constraint is just supported by C# compiler not the run-time, so despite all other constraints, it's not respected by `MakeGenericType` method. So using a method like what you also have is an option (which is not perfect, because of exceptional case that I mentioned in my answer). And by the way, your code needs small fix, the method should return false when `t.IsGenericType`, it's because of a limitation in the current version of language. Anyway, you have my vote :) – Reza Aghaei Jan 01 '19 at 14:21