How to tell if Type A is implicitly convertible to Type B
Asked Answered
M

5

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");
Muire answered 8/2, 2010 at 19:30 Comment(0)
A
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));
Astonied answered 8/2, 2010 at 19:51 Comment(7)
This will tell us that it has an implicit or explicit conversion method. Not whether it can be implicitly converted between specific types.Pathic
That's ok, Sam, this will work for my issue. I'm a little surprised there's no built-in way to do this.Muire
Why not use the enumerable Any extension?Uniparous
@Jason: You should name it IsCastableFrom and reverse the arguments to match the naming of similar framework methods.Lexi
@280Z28: You're right; in hindsight IsCastableFrom would have been better.Astonied
This answer is a bit old. An alternative now would be use dynamic.Muire
implicit/explicit operators can be declared not only on from type, but also on to type. Then needed to be checked return type and parameter type of methodinfo of operator on to and from types.Georgy
T
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.

Towland answered 8/2, 2010 at 19:58 Comment(6)
Heh. Interesting. I'm really surprised there's no baked-in way to say, "This type can be converted to this other type".Muire
"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."Astonied
Yeah, I'm sure. Derived[] is implicitly convertible to Base[].Towland
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[].Astonied
Well, of course. The exact rulez are spelled out in the language spec, chapter 6.1Towland
All of the above except op_Implicit and the "numeric" conversions is covered by Type.IsAssignableFromPseudocarp
T
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));
}
Tutuila answered 15/5, 2014 at 11:54 Comment(5)
Alas, your post on the subject no longer exists at that linkSuppression
The post Conversions.md still exists, but the article describing ReflectionHelpers.GetMethod has apparently been taken over by a cybersquatter.Mesomorph
you can find ReflectionHelpers from here.Angell
Try/catch is a really inefficient way to attempt this.Harmonicon
@Harmonicon agreed but you can easily wrap this in a caching layer to mitigate the impact.Tutuila
M
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;
}
Mesomorph answered 30/1, 2020 at 2:30 Comment(0)
H
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));
}
Harmonicon answered 24/5, 2022 at 13:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.