0

Please have a look at the following code. Why do I get a compile-error?

I don't get it!

Casting is a way of telling the compiler that I know more about the objects than it does. And in this case, I know for fact, that "x" does actually contain an instance of "SomeClass". But the compiler seems to be unwilling to accept that information.

https://dotnetfiddle.net/0DlmXf

public class StrangeConversion 
{
    public class SomeClass { }
    public interface ISomeInterface { }
    public class Implementation : SomeClass, ISomeInterface { }
    public void Foo<T>() where T : class 
    {
        T x = (T)Factory();
        //Compile-error: Cannot convert type 'T' to 'SomeClass'
        SomeClass a = (SomeClass)x;
        //This is perfectly fine:
        SomeClass b = (SomeClass)(object)x;

        if (x is SomeClass c) 
        {
            //This works as well and 'c' contains the reference.
        }
    }
    private object Factory() 
    {
        return new Implementation();
    }
}

Edit: @Charles Mager has the correct answer in the comment: There does not seem to be a valid reason. The language designers just didn't want to allow this cast.

Andreas
  • 1,997
  • 21
  • 35
  • 2
    So when I call `Foo()` you, somehow, know that an `Implementation` can be cast to `SomeCompletelyDifferentClassNotRelatedToSomeClass`? No, you don't, because that's wrong. When you're writing generic code, it's *meant to be generic*. – Damien_The_Unbeliever Apr 01 '22 at 13:56
  • This is just some sample-Code of course, to show the issue. In the real world, I am calling `T DispatchProxy.Create()`. This method guarantees to return an object that implements the interface T and inherits from the class TProxy. And the compiler won't let me cast T to TProxy. – Andreas Apr 01 '22 at 13:59
  • Then you haven't given enough information to the compiler for it to know that `T` and `TProxy` are *related* types. As here, if you had type constrained `T` to be derived from `SomeType`, your sample would compile fine. – Damien_The_Unbeliever Apr 01 '22 at 14:04
  • 1
    The [language rules](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/conversions#1035-explicit-reference-conversions) specify this isn't allowed unless `T` is known to be a base class of `SomeClass` (I imagine because generally it's a bad idea), but you can convert from `object`. – Charles Mager Apr 01 '22 at 14:04
  • @Damien_The_Unbeliever That's the whole point of a casting: telling the compiler what it does not know. – Andreas Apr 01 '22 at 14:08
  • @Andreas the language designers [disagree with you](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/conversions#1035-explicit-reference-conversions). You have to go via `object` if you want to do this. – Charles Mager Apr 01 '22 at 14:10
  • @CharlesMager Thank you. Seems like a fundamentally wrong decision to me. But if the language designers made this decision, I need to live with it. Edit: your second answer arrived while I as typing. Edit2: I would definitely love to see the protocol of the meeting where they decided that. – Andreas Apr 01 '22 at 14:11
  • You may wish to read Eric Lippert's [Representation and Identity](https://ericlippert.com/2009/03/03/representation-and-identity/) which more clearly lays out the two purposes of casting and where one is "this object of type B will actually always be of derived type D" which is the sort of cast you're attempting, but as I say, you haven't yet established with the compiler that `B` and `D` do have such a relationship. – Damien_The_Unbeliever Apr 01 '22 at 14:14
  • @CharlesMager [section 10.3.8](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/conversions#1038-explicit-conversions-involving-type-parameters) is probably more relevant. Andreas, this list feels pretty reasonable to me. Are you suggesting that the compiler should allow you to do anything you want by casting? That seems like very error-prone to me - not safe at all. – Sweeper Apr 01 '22 at 14:15
  • @CharlesMager Not everything. But the compiler should allow all castings that have a chance of being successful. That's what castings are there for: telling the compiler what it does not know. With the risk of getting a cast-exception. I just saw a nice quote from the language designers: "We can and should offer features, the users can use to shoot themselves in the foot. But we should not build something that has no other purpose." – Andreas Apr 01 '22 at 14:22

4 Answers4

0

I fixed using the as casting e.g.

SomeClass a = x as SomeClass;

This Answer explains is very well https://stackoverflow.com/a/132467/16690008

Essentially it's because it would throw an exception if T is not type of that class

Winjas Force
  • 46
  • 1
  • 3
  • This is fundamentally the same as `if (x is SomeClass c)`, which is already present in the OP's code. – gunr2171 Apr 01 '22 at 14:02
  • It a normal and expected behaviour that a casting might result in an exception when the value is not of the appropriate type. The following code compiles just fine but is guaranteed to fail: `object o = new object(); string s = (string) o;`. – Andreas Apr 01 '22 at 14:03
0

It's hard to make sense of exactly what you're trying to achieve, but it seems like a generic constraint is what you're after

public void Foo<T>()
    where T : SomeClass // constrain T to be inheriting from SomeClass
{
    T x = Factory<T>(); // x is guaranted to be SomeClass
}

private T Factory<T>()
    where T : SomeClass // same here
{
    return new Implementation();
}
Arthur Rey
  • 2,990
  • 3
  • 19
  • 42
  • The code above is not trying to achieve anything. It's just a condensed version of an issue I had while coding. The workarounds I need are already in the code above. But I would like to understand, why the compiler rejects the code. – Andreas Apr 01 '22 at 14:15
  • Maybe you wouldn't need a workaround if you told the compiler what that generic method should expect. I can't see a single scenario where you'd want to cast in a generic method, if so, why not work with an `object` instead? – Arthur Rey Apr 01 '22 at 19:46
0

You constrain the generic to only reference types by specifying where T : class, but the compiler needs to know with certainty if the cast is possible. This means you are asking the compiler to trust that SomeClass can be cast from any reference type you pass to it, which is something it won't do. The microsoft docs state that for the class generic type constraint:

The type argument must be a reference type. This constraint applies also to any class, interface, delegate, or array type.

Its important to note that SomeClass b = (SomeClass)(object)x; works because of the cast to object which is the root of the object hierarchy. But as you can see from the list of supported reference types, SomeClass a = (SomeClass)x; has to account for things such as delegates, array types, etc., at which point the compiler will throw you the error

Don't do SomeClass b = (SomeClass)(object)x;, it is much cleaner to make proper use of type constraints along with the as & is operators which were designed for this exact purpose of type checking and safe casting

Narish
  • 607
  • 4
  • 18
  • "This means you are asking the compiler to trust that SomeClass can be cast from any reference type you pass to it, which is something it won't do." No. The cast-statements tells the runtime to check whether the cast is possible or not and to throw an exception in the latter case. So there is always the possibility that a cast might fail. If failing was impossible, I would not need the cast in the first place. "it is much cleaner to make proper use of type constraints" Of course it is. But there are many circumstances where this is just not possible. – Andreas Apr 01 '22 at 16:20
  • Apologies if my wording was unclear but the main point here is that the compiler does not know what flavor of reference type you are using here and therefore is refusing the proceed in the name of type safety. Yes, casts are meant to behave the way you describe but C# is a strongly, statically typed language at the end of the day so the compiler needs proper reassurances that you are not going to attempt to cast a non-object reference type in your statement. The core of the issue here is that not every reference type is an object, even though every object is a reference type – Narish Apr 01 '22 at 16:51
  • Generic constraints can also live in a lot more places that it may seem, for example you can have a [generic method with type constraints inside a non-generic class](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generic-methods) – Narish Apr 01 '22 at 16:52
  • "compiler needs proper reassurances that you are not going to attempt to cast a non-object reference type in your statement" Why does it need that? It's in the nature of a cast that it might fail. "The core of the issue here is that not every reference type is an object, even though every object is a reference type" Please name one reference-type that is not an object. AFAIK it is always possible to assign anything that hide behind "T where T:class" to an object. The compiler does this without any issues or warnings. – Andreas Apr 01 '22 at 18:14
  • Already linked the docs that explains what is covered by `class` constraint. The CLI considers more than just objects to be ref types, and this is partially rooted in the language's design, ex. all array types inherit from System.Array, and since there is no inheritance from value types, there were defaulted to being reference types. More importantly, they are on the managed heap and they get copied by reference. I understand that these are specific to .NET and not particularly the case for what a value and ref type are theoretically, hence the confusion. But this is what is going wrong here – Narish Apr 01 '22 at 19:00
  • I think what you are trying to say is that not all types that fulfill the class-constraint are actually classes. They could also be interfaces, arrays, delegates, etc. Correct? So we are back to your statement from earlier: "so the compiler needs proper reassurances that you are not going to attempt to cast a non-object reference type in your statement." That's why I added the class-constraint in my example: To make clear that T is a reference type that is guaranteed to be always assignable to a reference from type "object". – Andreas Apr 01 '22 at 20:59
  • Sorry about that, "non-object" was the wrong term to use there. I should really have said "not supported" since the class constraint is objects + other things that have been implemented as reference types within .NET particularly – Narish Apr 04 '22 at 14:31
0

Short answer:

This behaviour is correct according to the spec. The spec is just bad here since this might convert a compile-error into a runtime-error.

Long answer:

I did some more research on the matter. This is an oversight in the language's spec. C# uses the same syntax for two totally different things:

int i = (int)1.9

This converts the double 1.9 to an integer. The value is actually changed.

object o = "abc";
string s = (string) o;

This looks the same, but does not change the object referenced by "o" at all. It does only convert the type of the reference.

When it comes to generics, this kind of ambiguity is an issue:

function f(T x) {
    var x = (string) x;
}

What should the language do if T is "int"?

That's why the spec forces the developer to cast to object first:

function f(T x) {
    var x = (string)(object)x;
}

Now, the behaviour is clear: X might still be a value-type. But if it is, it will be converted to a reference-type first.

This ambiguity does not exist in my example, since T is guaranteed to be a reference type:

public void Foo<T>() where T : class 

Thus the cast to object is not necessary. It could even be harmful if the "where" specifies an actual type. In that case, the forced cast to object might convert a compile-time-error (impossible cast) to a runtime-error.

Unfortunately, the people who created the spec, did not see this issue and did not include it.

Andreas
  • 1,997
  • 21
  • 35