13

The Type class has a method IsAssignableFrom() that almost works. Unfortunately it only returns true if the two types are the same or the first is in the hierarchy of the second. It says that decimal is not assignable from int, but I'd like a method that would indicate that decimals are assignable from ints, but ints are not always assignable from decimals. The compiler knows this but I need to figure this out at runtime.

Here's a test for an extension method.

[Test]
public void DecimalsShouldReallyBeAssignableFromInts()
{
    Assert.IsTrue(typeof(decimal).IsReallyAssignableFrom(typeof(int)));
    Assert.IsFalse(typeof(int).IsReallyAssignableFrom(typeof(decimal)));
}

Is there a way to implement IsReallyAssignableFrom() that would work like IsAssignableFrom() but also passes the test case above?

Thanks!

Edit:

This is basically the way it would be used. This example does not compile for me, so I had to set Number to be 0 (instead of 0.0M).

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class MyAttribute : Attribute
{
    public object Default { get; set; }
}

public class MyClass
{
    public MyClass([MyAttribute(Default= 0.0M)] decimal number)
    {
        Console.WriteLine(number);
    }
}

I get this error:

Error 4 An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type

CarenRose
  • 1,266
  • 1
  • 12
  • 24
epicsmile
  • 303
  • 2
  • 9
  • 1
    What would you do with this information? – Gabe Sep 01 '10 at 21:53
  • 1
    This is framework code (for serializing objects). I have an object to be serialized that is a decimal, and I'd like to default it to 0. Can't assign 0.0M to it (fails at compile time: not a constant expression). Would like to use int 0 and need a way to verify this is safe but disallow defaulting a decimal with a DateTime, for instance. – epicsmile Sep 01 '10 at 22:08
  • 1
    Can you show the code that does not compile for you? `const decimal D = 0.0M` compiles just fine here. – Pavel Minaev Sep 01 '10 at 22:33
  • @Pavel Minaev: I've edited my question to include a sample of what would not compile for me. – epicsmile Sep 02 '10 at 14:31
  • Ah yes, attribute arguments. Decimals aren't really a primitive types as far as CLR is concerned, so there is no such thing as a decimal literal on CLR level. For constant fields, C# works around this by using raw bytes, but it's not an option for attributes. On the bright side, I believe that this is only a problem for decimals, though (since only they have not-quite-literal-literals in C#). What you could do is require the type to match exactly for everything except for decimal, and specifically check for "compatible" primitive types (which you know in advance) for the latter. – Pavel Minaev Sep 02 '10 at 20:38
  • ... on the other hand, if you want to go multilingual, you have to consider that other languages may have more literals for various types. E.g. VB has a literal form of `DateTime` (`#01/02/2010 15:20:02#`). It can't be used in attribute context though (because it's not really a constant value, rather just syntactic sugar for `new DateTime(...)`), so the best you can do there is allow a string. Problem is, there is no universal rule for convertability between CLR languages (e.g. VB's `CType()` also uses `IConvertible`), so your library can't assume anything about semantics expected by the user. – Pavel Minaev Sep 02 '10 at 20:44
  • did you ever write the method IsReallyAssignableFrom()? Can you share it? :) – Dirk Boer May 06 '13 at 09:55
  • @DirkBoer We never wrote that method exactly. It turned out that we only needed to handle a few cases and we wrote more specific code just to handle those. – epicsmile May 09 '13 at 18:30

6 Answers6

14

There are actually three ways that a type can be “assignable” to another in the sense that you are looking for.

  • Class hierarchy, interface implementation, covariance and contravariance. This is what .IsAssignableFrom already checks for. (This also includes permissible boxing operations, e.g. int to object or DateTime to ValueType.)

  • User-defined implicit conversions. This is what all the other answers are referring to. You can retrieve these via Reflection, for example the implicit conversion from int to decimal is a static method that looks like this:

    System.Decimal op_Implicit(Int32)
    

    You only need to check the two relevant types (in this case, Int32 and Decimal); if the conversion is not in those, then it doesn’t exist.

  • Built-in implicit conversions which are defined in the C# language specification. Unfortunately Reflection doesn’t show these. You will have to find them in the specification and copy the assignability rules into your code manually. This includes numeric conversions, e.g. int to long as well as float to double, pointer conversions, nullable conversions (int to int?), and lifted conversions.

Furthermore, a user-defined implicit conversion can be chained with a built-in implicit conversion. For example, if a user-defined implicit conversion exists from int to some type T, then it also doubles as a conversion from short to T. Similarly, T to short doubles as T to int.

Timwi
  • 65,159
  • 33
  • 165
  • 230
  • 1
    Thanks, Timwi and other responders. It's true that I can't get the built-in conversions, but I can live with that. Here's the implementation of the method that I ended up with. public static bool IsReallyAssignableFrom(this Type t, Type other) { if (t.IsAssignableFrom(other)) { return true; } return t.GetMethod("op_Implicit", new[] {other}) != null; } – epicsmile Sep 01 '10 at 22:23
  • 2
    @epicsmile: You need to check for `op_Implicit` on the *other* type as well, and for that one, look at its return type rather than its parameter type. – Timwi Sep 01 '10 at 22:41
  • 2
    You omit pointer conversions. You also omit lifted and nullable conversions. Furthermore, even leaving lifting aside, user-defined implicit conversions are considerably more complex than your brief sketch allows for. A user-defined implicit conversion from, say, int to Foo also may be used as a user defined implicit conversion from short to Foo, since short goes to int and int goes to Foo. – Eric Lippert Sep 01 '10 at 22:53
  • 1
    Even with your update things are still considerably more complicated than you let on. Consider a conversion from struct S to decimal? when there is a user-defined conversion from S? to int. -- that is analyzed as a nullable implicit conversion from S to S?, followed by a user-defined conversion from S? to int, followed by a nullable conversion from int to int?, followed by a lifted numeric conversion from int? to decimal?. Or is it from int to decimal and then decimal to decimal? ? Makes you wonder, doesn't it? Conversion logic is *tricky* in C#; there is no easy way to get it right. – Eric Lippert Sep 02 '10 at 00:35
  • 2
    @Eric Lippert: Why don’t you post a more complete answer of your own? I’ll upvote it. – Timwi Sep 02 '10 at 01:54
  • I don't feel this answer is actually terribly helpful. `IsAssignableFrom` is the first thing most people will try after a Google. When it doesn't work how they expect in all cases, the other two suggestions become relevant. However, this answer does not actually show you _how_ you would apply them (or _if_ it is actually practically possible) - to the extent that I don't think this really answers the question – David Roberts Mar 30 '16 at 10:31
2

This one almost works... it's using Linq expressions:

public static bool IsReallyAssignableFrom(this Type type, Type otherType)
{
    if (type.IsAssignableFrom(otherType))
        return true;

    try
    {
        var v = Expression.Variable(otherType);
        var expr = Expression.Convert(v, type);
        return expr.Method == null || expr.Method.Name == "op_Implicit";
    }
    catch(InvalidOperationException ex)
    {
        return false;
    }
}

The only case that doesn't work is for built-in conversions for primitive types: it incorrectly returns true for conversions that should be explicit (e.g. int to short). I guess you could handle those cases manually, as there is a finite (and rather small) number of them.

I don't really like having to catch an exception to detect invalid conversions, but I don't see any other simple way to do it...

Thomas Levesque
  • 286,951
  • 70
  • 623
  • 758
1

Timwi's answer is really complete, but I feel there's an even simpler way that would get you the same semantics (check "real" assignability), without actually defining yourself what this is.

You can just try the assignment in question and look for an InvalidCastException (I know it's obvious). This way you avoid the hassle of checking the three possible meanings of assignability as Timwi mentioned. Here's a sample using xUnit:

[Fact]
public void DecimalsShouldReallyBeAssignableFromInts()
{
    var d = default(decimal);
    var i = default(i);

    Assert.Throws<InvalidCastException)( () => (int)d);
    Assert.DoesNotThrow( () => (decimal)i);
}
Johannes Rudolph
  • 35,298
  • 14
  • 114
  • 172
  • I'd like to know whether the assignment will work for any value of the type, not just default. You can cast some (even many) values of decimal to int, but not all. It's unfortunate that the default decimal value is one of the ones you can cast. – epicsmile Sep 01 '10 at 22:27
  • Why do you think `(int)d` will throw `InvalidCastException`, and why do you think that the latter even has anything to do with assignability? – Pavel Minaev Sep 01 '10 at 22:30
  • 1
    Wrong answer. This only checks for the existence of *explicit* conversions or casts, not *implicit* ones (which is what *assignability* usually means). Furthermore, it doesn’t work for code that needs to handle a `Type` object; it only works if you know the exact types at compile-time. Finally, as epicsmile pointed out, it depends on the actual values you are casting (e.g. it’ll let you cast `object` to `int` if and only if the object you pass in is actually a boxed int). – Timwi Sep 01 '10 at 22:36
0

In order to find out if one type can be assigned to another, you have to look for implicit conversions from one to the other. You can do this with reflection.

As Timwi said, you will also have to know some built-in rules, but those can be hard-coded.

Gabe
  • 84,912
  • 12
  • 139
  • 238
0

What you are looking for is if there's an implicit cast from the one type to the other. I would think that's doable by reflection, though it might be tricky because the implicit cast should be defined as an operator overload which is a static method and I think it could be defined in any class, not just the one that can be implicitly converted.

quentin-starin
  • 26,121
  • 7
  • 68
  • 86
-1

It actually happens to be the case that the decimal type is not "assignable" to the int type, and vice versa. Problems occur when boxing/unboxing gets involved.

Take the example below:

int p = 0;
decimal d = 0m;
object o = d;
object x = p;

// ok
int a = (int)d;

// invalid cast exception
int i = (int)o;

// invalid cast exception
decimal y = (decimal)p;

// compile error
int j = d;

This code looks like it should work, but the type cast from object produces an invalid cast exception, and the last line generates a compile-time error.

The reason the assignment to a works is because the decimal class has an explicit override on the type cast operator to int. There does not exist an implicit type cast operator from decimal to int.

Edit: There does not exist even the implicit operator in reverse. Int32 implements IConvertible, and that is how it converts to decimal End Edit

In other words, the types are not assignable, but convertible.

You could scan assemblies for explicit type cast operators and IConvertible interfaces, but I get the impression that would not serve you as well as programming for the specific few cases you know you will encounter.

Good luck!

kbrimington
  • 25,142
  • 5
  • 62
  • 74