27

I have two types, T and U, and I want to know whether an implicit cast operator is defined from T to U.

I'm aware of the existence of IsAssignableFrom, and this is not what I'm looking for, as it doesn't deal with implicit casts.

A bit of googling led me to this solution, but in the author's own words this is ugly code (it tries to cast implicitly and returns false if there's an exception, true otherwise...)

It seems testing for the existence of an op_Implicit method with the correct signature won't work for primitive types.

Is there a cleaner way of determining the existence of an implicit cast operator?

Community
  • 1
  • 1
Brann
  • 31,689
  • 32
  • 113
  • 162
  • Just a hint: Look at [implicit type conversion operators](https://msdn.microsoft.com/en-us/library/z5z9kes2.aspx). I guess there should be a way to find implicit operators through reflection... –  Aug 15 '15 at 13:43
  • Can you please go into more detail about what goal you're trying to accomplish with the result? I've had a similar problem in the past, realized it would pretty much have to look like what CliveDM [linked](https://github.com/CYJB/Cyjb/blob/f4424c4f81cacd09e9ce5d202a03b0c121c09ac2/Cyjb/TypeExt.cs) [below](http://stackoverflow.com/a/32025388/1083771), and decided to just call [Convert.ChangeType](https://msdn.microsoft.com/en-us/library/dtb69x08.aspx) and handle exceptions. I realize that may not be a viable solution in your case, but maybe there's a similar workaround. – Joe Amenta Aug 15 '15 at 14:22

4 Answers4

19

You could use reflection to find the implicit conversion method for the target type:

public static bool HasImplicitConversion(Type baseType, Type targetType)
{
    return baseType.GetMethods(BindingFlags.Public | BindingFlags.Static)
        .Where(mi => mi.Name == "op_Implicit" && mi.ReturnType == targetType)
        .Any(mi => {
            ParameterInfo pi = mi.GetParameters().FirstOrDefault();
            return pi != null && pi.ParameterType == baseType;
        });
}

You can use it like this:

class X {}
class Y
{
    public static implicit operator X (Y y)
    {
        return new X();
    }

    public static implicit operator Y (X x)
    {
        return new Y();
    }
}

// and then:
bool conversionExists = HasImplicitConversion(typeof(Y), typeof(X));

Note that this only checks for an implicit type conversion on the base type (the first passed type). Technically, the type conversion can also be defined on the other type, so you may need to call it again with the types reversed (or build that into the method). Implicit type conversions may not exist on both types though.

Brann
  • 31,689
  • 32
  • 113
  • 162
poke
  • 369,085
  • 72
  • 557
  • 602
  • It doesn't seem to work for primitive types, eg MappingsGetter.HasImplicitConversion(typeof (int), typeof (decimal)) returns false whereas an implicit conversion exists : https://msdn.microsoft.com/en-us/library/y5b434w4.aspx – Brann Aug 15 '15 at 14:00
  • 4
    @Brann Those types don’t have implicit operators; the type conversion is done by the CLR directly. They do implement [`IConvertible`](https://msdn.microsoft.com/en-us/library/system.iconvertible.aspx) though, so you can test for that. – poke Aug 15 '15 at 14:16
  • @Brann For primitive types you'll have to just hard-code a table somewhere. I'm not aware of any built-in mechanism which will allow you to do this programmatically. – Kyle Aug 15 '15 at 15:03
  • @Kyle: I've just done that and posted this as an answer. Not elegant, but it does the job. – Brann Aug 15 '15 at 16:15
7

I ended up handling the primitive types scenario manually. Not very elegant, but it works.

I've also added additional logic to handle nullable types and enums.

I reused Poke's code for the user-defined type scenario.

public class AvailableCastChecker
{
    public static bool CanCast(Type from, Type to)
    {
        if (from.IsAssignableFrom(to))
        {
            return true;
        }
        if (HasImplicitConversion(from, from, to)|| HasImplicitConversion(to, from, to))
        {
            return true;
        }
        List<Type> list;
        if (ImplicitNumericConversions.TryGetValue(from, out list))
        {
            if (list.Contains(to))
                return true;
        }

        if (to.IsEnum)
        {
            return CanCast(from, Enum.GetUnderlyingType(to));
        }
        if (Nullable.GetUnderlyingType(to) != null)
        {
            return CanCast(from, Nullable.GetUnderlyingType(to));
        }

        return false;
    }

    // https://msdn.microsoft.com/en-us/library/y5b434w4.aspx
    static Dictionary<Type,List<Type>> ImplicitNumericConversions = new Dictionary<Type, List<Type>>();

    static AvailableCastChecker()
    {
        ImplicitNumericConversions.Add(typeof(sbyte), new List<Type> {typeof(short), typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(byte), new List<Type> { typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(short), new List<Type> {  typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(ushort), new List<Type> { typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(int), new List<Type> { typeof(long), typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(uint), new List<Type> { typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(long), new List<Type> { typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(char), new List<Type> { typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) });
        ImplicitNumericConversions.Add(typeof(float), new List<Type> { typeof(double) });
        ImplicitNumericConversions.Add(typeof(ulong), new List<Type> { typeof(float), typeof(double), typeof(decimal) });
    }

    static bool HasImplicitConversion(Type definedOn, Type baseType, Type targetType)
    {
        return definedOn.GetMethods(BindingFlags.Public | BindingFlags.Static)
            .Where(mi => mi.Name == "op_Implicit" && mi.ReturnType == targetType)
            .Any(mi =>
            {
                ParameterInfo pi = mi.GetParameters().FirstOrDefault();
                return pi != null && pi.ParameterType == baseType;
            });

    }
}
Community
  • 1
  • 1
Brann
  • 31,689
  • 32
  • 113
  • 162
  • The enum handling was surprising, as it doesn't seem to follow the same rules as everything else. See http://hastebin.com/rexuzaraju for details. Maybe that's OK, but it's minimally something that was left out of the original question. – Joe Amenta Aug 20 '15 at 12:41
  • The correct type to use in the dictionary is HashSet – Mauro Sampietro Nov 21 '16 at 10:27
  • Shouldn't the `if (from.IsAssignableFrom(to))` condition be `if (to.IsAssignableFrom(from))`? I realize this is a common mistake/source of confusion (evidence: see the [proposal for IsAssignableTo method](https://github.com/dotnet/runtime/issues/30714) and observe that such a member present in the .NET 6 api set)... am I mistaken, or is the code mistaken? – kkahl Sep 27 '22 at 05:23
3

Here's a solution I found. The major code shown as bellow (after some simple translation):

public static bool IsImplicitFrom(this Type type, Type fromType) {
    if (type == null || fromType == null) {
        return false;
    }

    // support for reference type
    if (type.IsByRef) { type = type.GetElementType(); }
    if (fromType.IsByRef) { fromType = type.GetElementType(); }

    // could always be convert to object
    if (type.Equals(typeof(object))) {
        return true;
    }

    // check if it could be convert using standard implicit cast
    if (IsStandardImplicitFrom(type, fromType)) {
        return true;
    }

    // determine implicit convert operator
    Type nonNullalbeType, nonNullableFromType;
    if (IsNullableType(type, out nonNullalbeType) && 
        IsNullableType(fromType, out nonNullableFromType)) {
        type = nonNullalbeType;
        fromType = nonNullableFromType;
    }

    return ConversionCache.GetImplicitConversion(fromType, type) != null;
}

internal static bool IsStandardImplicitFrom(this Type type, Type fromType) {
    // support for Nullable<T>
    if (!type.IsValueType || IsNullableType(ref type)) {
        fromType = GetNonNullableType(fromType);
    }

    // determine implicit value type convert
    HashSet<TypeCode> typeSet;
    if (!type.IsEnum && 
        ImplicitNumericConversions.TryGetValue(Type.GetTypeCode(type), out typeSet)) {
        if (!fromType.IsEnum && typeSet.Contains(Type.GetTypeCode(fromType))) {
            return true;
        }
    }

    // determine implicit reference type convert and boxing convert
    return type.IsAssignableFrom(fromType);
}

Update: Here's the whole file.

Clive DM
  • 551
  • 4
  • 13
0

While Brann's solution works for a lot of cases, it does not take into account the cascading effect on implicit castings of non-primitive types.
E.g.: Assigning a float to a Thickness (which has an implicit cast from a double) should return true. I rewrote the code to handle cases like this.

public static class AvailableCastChecker
{
    public static bool CanCast(Type from, Type to)
    {
        if (to.IsAssignableFrom(from))
        {
            return true;
        }
        if (HasImplicitConversion(from, from, to) || HasImplicitConversion(to, from, to))
        {
            return true;
        }
        if (ImplicitNumericConversions.TryGetValue(to, out var list) &&
            (list.Contains(from) || list.Any(t => CanCast(from, t))))
        {
            return true;
        }
        if (to.IsEnum)
        {
            return CanCast(from, Enum.GetUnderlyingType(to));
        }
        return Nullable.GetUnderlyingType(to) != null && CanCast(from, Nullable.GetUnderlyingType(to));
    }

    // https://msdn.microsoft.com/en-us/library/y5b434w4.aspx
    private static Dictionary<Type, List<Type>> _implicitNumericConversions;
    private static Dictionary<Type, List<Type>> ImplicitNumericConversions => _implicitNumericConversions ?? (_implicitNumericConversions = new Dictionary<Type, List<Type>>()
    {
        {typeof(short), new List<Type> { typeof(sbyte), typeof(byte) }},
        {typeof(ushort), new List<Type> { typeof(byte), typeof(char) }},
        {typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(char), typeof(short), typeof(ushort) }},
        {typeof(uint), new List<Type> { typeof(byte), typeof(char), typeof(ushort) }},
        {typeof(long), new List<Type> {  typeof(sbyte), typeof(byte), typeof(char), typeof(short), typeof(ushort), typeof(int), typeof(uint) }},
        {typeof(ulong), new List<Type> { typeof(byte), typeof(char), typeof(ushort), typeof(uint) }},
        {typeof(float), new List<Type> { typeof(sbyte), typeof(byte), typeof(char), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong) }},
        {typeof(double), new List<Type> { typeof(sbyte), typeof(byte), typeof(char), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float) }}
    });

    private static bool HasImplicitPrimitiveConversion(Type from, Type to)
    {
        return ImplicitNumericConversions.TryGetValue(to, out var list) && list.Contains(from);
    }

    private static bool HasImplicitConversion(Type definedOn, Type from, Type to)
    {
        return definedOn.GetMethods(BindingFlags.Public | BindingFlags.Static)
            .Where(mi => mi.Name == "op_Implicit"
                         && (mi.ReturnType == to || HasImplicitPrimitiveConversion(from, to)))
            .Any(mi =>
            {
                var pi = mi.GetParameters().FirstOrDefault();
                return pi != null && (pi.ParameterType == from || HasImplicitPrimitiveConversion(from, pi.ParameterType));
            });
    }
}
Yves
  • 632
  • 1
  • 11
  • 17