27

Given Type a and Type b, how can I, at runtime, determine whether there's an implicit conversion from a to b?

If that doesn't make sense, consider the following method:

public PropertyInfo GetCompatibleProperty<T>(object instance, string propertyName)
{
   var property = instance.GetType().GetProperty(propertyName);

   bool isCompatibleProperty = !property.PropertyType.IsAssignableFrom(typeof(T));
   if (!isCompatibleProperty) throw new Exception("OH NOES!!!");

   return property;   
}

And here's the calling code that I want to work:

// Since string.Length is an int property, and ints are convertible
// to double, this should work, but it doesn't. :-(
var property = GetCompatibleProperty<double>("someStringHere", "Length");
Judah Gabriel Himango
  • 58,906
  • 38
  • 158
  • 212

5 Answers5

25

Note that IsAssignableFrom does NOT solve your problem. You have to use Reflection like so. Note the explicit need to handle the primitive types; these lists are per §6.1.2 (Implicit numeric conversions) of the specification.

static class TypeExtensions { 
    static Dictionary<Type, List<Type>> dict = new Dictionary<Type, List<Type>>() {
        { typeof(decimal), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
        { typeof(double), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
        { typeof(float), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
        { typeof(ulong), new List<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
        { typeof(long), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
        { typeof(uint), new List<Type> { typeof(byte), typeof(ushort), typeof(char) } },
        { typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
        { typeof(ushort), new List<Type> { typeof(byte), typeof(char) } },
        { typeof(short), new List<Type> { typeof(byte) } }
    };
    public static bool IsCastableTo(this Type from, Type to) { 
        if (to.IsAssignableFrom(from)) { 
            return true; 
        }
        if (dict.ContainsKey(to) && dict[to].Contains(from)) {
            return true;
        }
        bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static) 
                        .Any( 
                            m => m.ReturnType == to &&  
                            (m.Name == "op_Implicit" ||  
                            m.Name == "op_Explicit")
                        ); 
        return castable; 
    } 
} 

Usage:

bool b = typeof(A).IsCastableTo(typeof(B));
Chris Hynes
  • 9,999
  • 2
  • 44
  • 54
jason
  • 236,483
  • 35
  • 423
  • 525
6

Implicit conversions you'll need to consider:

  • Identity
  • sbyte to short, int, long, float, double, or decimal
  • byte to short, ushort, int, uint, long, ulong, float, double, or decimal
  • short to int, long, float, double, or decimal
  • ushort to int, uint, long, ulong, float, double, or decimal
  • int to long, float, double, or decimal
  • uint to long, ulong, float, double, or decimal
  • long to float, double, or decimal
  • ulong to float, double, or decimal
  • char to ushort, int, uint, long, ulong, float, double, or decimal
  • float to double
  • Nullable type conversion
  • Reference type to object
  • Derived class to base class
  • Class to implemented interface
  • Interface to base interface
  • Array to array when arrays have the same number of dimensions, there is an implicit conversion from the source element type to the destination element type and the source element type and the destination element type are reference types
  • Array type to System.Array
  • Array type to IList<> and its base interfaces
  • Delegate type to System.Delegate
  • Boxing conversion
  • Enum type to System.Enum
  • User defined conversion (op_implicit)

I assume you're looking for the latter. You'll need to write something resembling a compiler to cover all of them. Notable is that System.Linq.Expressions.Expression didn't attempt this feat.

jason
  • 236,483
  • 35
  • 423
  • 525
Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Heh. Interesting. I'm really surprised there's no baked-in way to say, "This type can be converted to this other type". – Judah Gabriel Himango Feb 08 '10 at 20:10
  • "Array to array when arrays are same length and element has implicit conversion" Are you sure? I don't think so. In fact, I don't think there's an explicit conversion. As for the rest, I think my method covers them all. Thus, I must be misunderstanding what you mean by "you'll need to write something resembling a compiler to cover all of them." – jason Feb 08 '10 at 20:25
  • Yeah, I'm sure. Derived[] is implicitly convertible to Base[]. – Hans Passant Feb 08 '10 at 21:41
  • Okay yes, I agree but that's slightly different than your initial statement. There is an implicit conversion from `int` to `double` but there is not an implicit conversion from `int[]` to `double[]`. – jason Feb 08 '10 at 21:50
  • Well, of course. The exact rulez are spelled out in the language spec, chapter 6.1 – Hans Passant Feb 08 '10 at 22:56
  • 1
    All of the above except op_Implicit and the "numeric" conversions is covered by Type.IsAssignableFrom – Eamon Nerbonne Dec 08 '10 at 09:45
6

The accepted answer to this question handles many cases, but not all. For example, here are just a few valid casts/conversions which are not handled correctly:

// explicit
var a = (byte)2;
var b = (decimal?)2M;

// implicit
double? c = (byte)2;
decimal? d = 4L;

Below, I've posted an alternate version of this function which specifically answers the question of IMPLICIT casts and conversions. For more details, the test suite I used to verify it, and the EXPLICIT cast version, please check out my post on the subject.

public static bool IsImplicitlyCastableTo(this Type from, Type to)
{
    if (from == null) { throw new ArgumentNullException(nameof(from)); }
    if (to == null) { throw new ArgumentNullException(nameof(to)); }

    // not strictly necessary, but speeds things up
    if (to.IsAssignableFrom(from))
    {
        return true;
    }

    try
    {
        // overload of GetMethod() from http://www.codeducky.org/10-utilities-c-developers-should-know-part-two/ 
        // that takes Expression<Action>
        ReflectionHelpers.GetMethod(() => AttemptImplicitCast<object, object>())
            .GetGenericMethodDefinition()
            .MakeGenericMethod(from, to)
            .Invoke(null, new object[0]);
        return true;
    }
    catch (TargetInvocationException ex)
    {
        return = !(
            ex.InnerException is RuntimeBinderException
            // if the code runs in an environment where this message is localized, we could attempt a known failure first and base the regex on it's message
            && Regex.IsMatch(ex.InnerException.Message, @"^The best overloaded method match for 'System.Collections.Generic.List<.*>.Add(.*)' has some invalid arguments$")
        );
    }
}

private static void AttemptImplicitCast<TFrom, TTo>()
{
    // based on the IL produced by:
    // dynamic list = new List<TTo>();
    // list.Add(default(TFrom));
    // We can't use the above code because it will mimic a cast in a generic method
    // which doesn't have the same semantics as a cast in a non-generic method

    var list = new List<TTo>(capacity: 1);
    var binder = Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(
        flags: CSharpBinderFlags.ResultDiscarded, 
        name: "Add", 
        typeArguments: null, 
        context: typeof(TypeHelpers), // the current type
        argumentInfo: new[] 
        { 
            CSharpArgumentInfo.Create(flags: CSharpArgumentInfoFlags.None, name: null), 
            CSharpArgumentInfo.Create(
                flags: CSharpArgumentInfoFlags.UseCompileTimeType, 
                name: null
            ),
        }
    );
    var callSite = CallSite<Action<CallSite, object, TFrom>>.Create(binder);
    callSite.Target.Invoke(callSite, list, default(TFrom));
}
ChaseMedallion
  • 20,860
  • 17
  • 88
  • 152
  • Alas, your post on the subject no longer exists at that link – Chris F Carroll Sep 28 '18 at 23:40
  • The post [Conversions.md](https://github.com/steaks/codeducky/blob/master/blogs/Conversions.md) still exists, but the article describing `ReflectionHelpers.GetMethod` has apparently been taken over by a cybersquatter. – Qwertie Jan 29 '20 at 22:39
  • you can find ReflectionHelpers from [here](http://web.archive.org/web/20170418203454/http://www.codeducky.org/10-utilities-c-developers-should-know-part-two/). –  Nov 29 '21 at 09:59
  • Try/catch is a really inefficient way to attempt this. – MgSam May 24 '22 at 13:20
  • @MgSam agreed but you can easily wrap this in a caching layer to mitigate the impact. – ChaseMedallion May 25 '22 at 20:19
1

Here is a method that passes all of the following tests:

[Test] public void TestImplicitlyCastable()
{
    Assert.That( typeof(byte)    .IsImplicitlyCastableTo(typeof(short)));
    Assert.That( typeof(byte)    .IsImplicitlyCastableTo(typeof(byte?)));
    Assert.That( typeof(byte)    .IsImplicitlyCastableTo(typeof(long?)));
    Assert.That(!typeof(short)   .IsImplicitlyCastableTo(typeof(uint)));
    Assert.That( typeof(long)    .IsImplicitlyCastableTo(typeof(float)));
    Assert.That( typeof(long)    .IsImplicitlyCastableTo(typeof(decimal)));
    Assert.That(!typeof(double)  .IsImplicitlyCastableTo(typeof(decimal)));
    Assert.That(!typeof(decimal) .IsImplicitlyCastableTo(typeof(double)));
    Assert.That( typeof(List<int>).IsImplicitlyCastableTo(typeof(object)));
    Assert.That( typeof(float)   .IsImplicitlyCastableTo(typeof(IComparable<float>)));
    Assert.That( typeof(long?)   .IsImplicitlyCastableTo(typeof(IComparable<long>)));
    Assert.That(!typeof(object)  .IsImplicitlyCastableTo(typeof(string)));
    Assert.That( typeof(string[]).IsImplicitlyCastableTo(typeof(object[])));
    Assert.That( typeof(Foo)     .IsImplicitlyCastableTo(typeof(int)));
    Assert.That(!typeof(Foo)     .IsImplicitlyCastableTo(typeof(uint)));
    Assert.That( typeof(Foo)     .IsImplicitlyCastableTo(typeof(long)));
    Assert.That( typeof(Foo)     .IsImplicitlyCastableTo(typeof(long?)));
}
class Foo
{
    public static implicit operator int(Foo f) => 42;
}

It is based on a trick with dynamic inspired by ChaseMedallion's answer:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.CSharp.RuntimeBinder;

public static class ReflectionHelpers
{
    [ThreadStatic]
    static readonly Dictionary<KeyValuePair<Type, Type>, bool> ImplicitCastCache;

    /// <summary>Returns true iff casting between values of the specified 
    /// types is possible based on the rules of C#.</summary>
    public static bool IsImplicitlyCastableTo(this Type from, Type to)
    {
        if (from == to)
            return true;

        var key = new KeyValuePair<Type, Type>(from, to);
        ImplicitCastCache ??= new Dictionary<KeyValuePair<Type, Type>, bool>();
        if (ImplicitCastCache.TryGetValue(key, out bool result))
            return result;

        if (to.IsAssignableFrom(from))
            return ImplicitCastCache[key] = true;

        var method = GetMethodInfo(() => IsImplicitlyCastableCore<int, int>())
            .GetGenericMethodDefinition().MakeGenericMethod(from, to);
        return ImplicitCastCache[key] = (bool)method.Invoke(null, Array.Empty<object>());
    }

    static bool IsImplicitlyCastableCore<TFrom,TTo>()
    {
        var testObject = new LinkedListNode<TTo>(default(TTo));
        try {
            ((dynamic)testObject).Value = default(TFrom);
            return true;
        } catch (Exception e) {
            // e.g. "Cannot implicitly convert type 'A' to 'B'. An explicit conversion exists (are you missing a cast?)"
            // The exception may be caused either because no conversion is available,
            // OR because it IS available but the conversion method threw something.
            // Assume RuntimeBinderException means the conversion does not exist.
            return !(e is RuntimeBinderException); 
        }
    }

    /// <summary><c>GetMethodInfo(() => M(args))</c> gets the MethodInfo object corresponding to M.</summary>
    public static MethodInfo GetMethodInfo(Expression<Action> shape) => ((MethodCallExpression)shape.Body).Method;
}
Qwertie
  • 16,354
  • 20
  • 105
  • 148
0

While jason's answer is a good start- it does not cover nullables. I needed a slightly more generic solution that both covered nullables, and given two types, finds the best common type, including implicit and explicit conversions.

(If you need just implicit conversions, the below can probably be modified for such).


/// <summary>Finds the best common type among the given types.</summary>
/// <param name="type1">The first type to check.</param>
/// <param name="type2">The second type to check.</param>
/// <returns>The best common type.</returns>
public static Type FindBestCommonType(Type type1, Type type2)
{
    if (type1 == null && type2 == null) throw new ArgumentNullException("One of the two types must be non-null.");
    if (type1 == null) return ensureNullable(type2);
    if (type2 == null) return ensureNullable(type1);
    
    if (type1 == type2) return type1;
    if (type1.IsAssignableFrom(type2)) return type1;
    if (type2.IsAssignableFrom(type1)) return type2;

    Type bestCommonType = null;
    var type1Underlying = Nullable.GetUnderlyingType(type1);
    var type2Underlying = Nullable.GetUnderlyingType(type2);
    var type1Nullable = type1Underlying != null;
    var type2Nullable = type2Underlying != null;
    var resultMustBeNullable = type1Nullable || type2Nullable;

    type1 = type1Underlying ?? type1;
    type2 = type2Underlying ?? type2;

    //If our nullable-stripped types are equivalent, send back the nullable version
    if (type1 == type2)
        return typeof(Nullable<>).MakeGenericType(type1);

    var type1Convertibles = _convertibleTypes.GetValueOrDefault(type1);
    var type2Convertibles = _convertibleTypes.GetValueOrDefault(type2);

    bestCommonType = type1Convertibles?.Contains(type2) == true ? type1
        : type2Convertibles?.Contains(type1) == true ? type2
        : null;

    //Check for implicit or explicit conversion
    if (bestCommonType == null)
    {
        if (type1.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .Any(m => m.ReturnType == type2 && (m.Name == "op_Implicit" || m.Name == "op_Explicit")))
            bestCommonType = type2;
        else if (type2.GetMethods(BindingFlags.Public | BindingFlags.Static)
                .Any(m => m.ReturnType == type1 && (m.Name == "op_Implicit" || m.Name == "op_Explicit")))
            bestCommonType = type1;
    }

    if (resultMustBeNullable && bestCommonType != null && bestCommonType != typeof(object))
        bestCommonType = typeof(Nullable<>).MakeGenericType(bestCommonType);

    return bestCommonType ?? typeof(object);

    //Function to ensure that the given type can hold nulls - if its a reference type it does nothing - if it's a value type it ensures it is Nullable<T>
    static Type ensureNullable(Type t) => t.IsValueType && Nullable.GetUnderlyingType(t) == null ? typeof(Nullable<>).MakeGenericType(t) : t;
}

private static readonly Dictionary<Type, HashSet<Type>> _convertibleTypes = new Dictionary<Type, HashSet<Type>>() {
        { typeof(decimal), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
        { typeof(double), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
        { typeof(float), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
        { typeof(ulong), new HashSet<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
        { typeof(long), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
        { typeof(uint), new HashSet<Type> { typeof(byte), typeof(ushort), typeof(char) } },
        { typeof(int), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
        { typeof(ushort), new HashSet<Type> { typeof(byte), typeof(char) } },
        { typeof(short), new HashSet<Type> { typeof(byte) } }
    };
}

This can be overloaded to find the best common type for an arbitrarily large set of types:

/// <summary>Finds the best common type among the given types.</summary>
/// <param name="types">The types to check.</param>
/// <returns>The best common type.</returns>
public static Type FindBestCommonType(params Type[] types)
{
    if (types == null) throw new ArgumentNullException(nameof(types));

    var filteredTypes = types.Distinct().ToList();
    if (filteredTypes.Count == 0) throw new InvalidOperationException("No types were provided");

    var bestCommonType = filteredTypes[0];
    foreach (var type in filteredTypes.Skip(1))
    {
        bestCommonType = FindBestCommonType(type, bestCommonType);
    }

    return bestCommonType;
}

We can then use this to widen unknown types at runtime:

/// <summary>
/// Attempts to widen the given objects so that they are both compatible types.
/// </summary>
/// <param name="o1">The first object, passed by reference.</param>
/// <param name="o2">The second object, passed by reference.</param>
public static void WidenToEqualTypes(ref object o1, ref object o2)
{
    var type1 = o1.GetType();
    var type2 = o2.GetType();

    var bestCommonType = FindBestCommonType(type1, type2);

    o1 = Convert.ChangeType(o1, bestCommonType);
    o2 = Convert.ChangeType(o2, bestCommonType);
}

Unit tests:

[TestCase(typeof(long),      new[] { typeof(int), typeof(long) })]
[TestCase(typeof(long?),     new[] {typeof(int), typeof(long?)})]
[TestCase(typeof(long?),     new[] {typeof(int?), typeof(long)})]
[TestCase(typeof(double?),   new[] {typeof(int?), typeof(double)})]
[TestCase(typeof(decimal),   new[] {typeof(long), typeof(decimal)})]
[TestCase(typeof(double), new[] { typeof(float), typeof(double) })]
[TestCase(typeof(bool?),     new[] {typeof(bool?), typeof(bool)})]
[TestCase(typeof(bool?), new[] { null, typeof(bool) })]
[TestCase(typeof(string), new[] { typeof(string), null })]
[TestCase(typeof(DateTime),  new[] {typeof(DateOnly), typeof(DateTime)})]
[TestCase(typeof(DateTime?), new[] {typeof(DateOnly?), typeof(DateTime)})]
[TestCase(typeof(DateTime?), new[] {typeof(DateTime?), typeof(DateOnly)})]
[TestCase(typeof(object),    new[] {typeof(string), typeof(int)})]
[TestCase(typeof(Guid?),      new[] {typeof(Guid), typeof(Guid?)})]
[TestCase(typeof(double?), new[] { typeof(int), typeof(long?), typeof(double) })]
[TestCase(typeof(DateTime?), new[] { typeof(DateTime), typeof(DateOnly?), typeof(DateOnly) })]
[TestCase(typeof(IEnumerable<int>), new[] { typeof(IEnumerable<int>), typeof(List<int>) })]
[Description("Finds the best common type that all the supplied types are convertible to.")]
public void BestCommonTypesTests(Type expected, Type[] types)
{
    Assert.AreEqual(expected, Util.FindBestCommonType(types));
}
MgSam
  • 12,139
  • 19
  • 64
  • 95