CAST vs DYNAMIC-CAST

Posted by Lieven De Foor on 24-Mar-2016 06:53

Hi,

Why is the compiler preventing me from casting an object from type a to type b when those two types are unrelated, while a dynamic-cast is working without problems?

Such a compile-time check isn't happening in c# afaik (or it might be implemented differently, see stackoverflow.com/.../compile-time-and-runtime-casting-c-sharp)

I think the compiler is incorrectly assuming this isn't a valid cast, which is proven by the dynamic-cast working without problems.

E.g.

INTERFACE IButton:

END INTERFACE.

CLASS ComponentFactory:

    METHOD PUBLIC STATIC IButton CreateButton():

        RETURN NEW CustomButton().

    END METHOD.

END CLASS.

CLASS CustomButton
    INHERITS System.Windows.Forms.Button
    IMPLEMENTS IButton:

END CLASS.

CLASS MyForm
    INHERITS Progress.Windows.Form:

    CONSTRUCTOR MyForm():

        DEFINE VARIABLE OkButton AS IButton NO-UNDO.

        OkButton = ComponentFactory:CreateButton().
        Controls:Add(CAST(OkButton, System.Windows.Forms.Control)). /* Isn't compiling */
        Controls:Add(DYNAMIC-CAST(OkButton, "System.Windows.Forms.Control")). /* Works */

    END CONSTRUCTOR.

END CLASS.

Posted by Laura Stern on 24-Mar-2016 09:25

Well, at least I made Mike happy this morning!  

Yes, this is definitely a bug; thanks for logging it.  I remember now (!!) that the compiler can sometimes determine that the CAST can never possibly work at runtime because the 2 objects bear no relationship with each other whatsoever. In that case it will fail to compile.  But, for example, if one object is in the other's hierarchy, in either direction, the CAST will compile successfully.  And as I said, for an interface, the compiler cannot make that judgment and should never error.

All Replies

Posted by Fernando Souza on 24-Mar-2016 07:40

DYNAMIC-CAST uses the actual type of the object instance passed in. The compiler uses the type defined for the variable that will hold the object reference.

Posted by Laura Stern on 24-Mar-2016 07:46

Actually, this is the correct behavior.  The compiler only knows that OkButton is an IButton as that's how it's defined.  An IButton cannot be cast to a Control.  You can cast a Button to a Control because it has all the methods and and properties that Control has.  But an IButton does not have any of those.  

But at runtime the instance you get is an actual button which is a Conrol.  The DYNAMIC-CAST is basing the cast on that, i.e., what the instance actually is not on how it's defined.

Posted by marian.edu on 24-Mar-2016 08:06

Maybe it's 'by-design' but that doesn't necessarily make it correct... since one can cast a Progress.Lang.Object to anything with no complain from the compiler and that is the root class for any living object in AVM your statement hardly holds true :(

IMHO it's just  the compiler that tries to be a bit smarter... at most that would make it as a 'unchecked cast' warning and that's all.

Posted by Mike Fechner on 24-Mar-2016 08:09

I got used to

CAST (CAST(oSomething, Progress.Lang.Object), TheRealTargetType)

in those cases ...

Posted by Laura Stern on 24-Mar-2016 08:10

Your statement "one can cast a Progress.Lang.Object to anything " is not correct.  You can cast anything to a Progress.Lang.Object, but not the other way around.  Think of it this way.  Something defined as a P.L.O can be anything.  Let's say you have a class B defined as a P.L.O.  You're implying that you should be able to cast it to a C.  Of course you should not be able to do that.  And you can't.

Posted by marian.edu on 24-Mar-2016 08:16

yeah, it might throw a runtime error but the compiler does not complain... thanks for the crash course on OO though :)

def var o as Progress.Lang.Object.

cast(o, Progress.Lang.AppError).

Posted by Lieven De Foor on 24-Mar-2016 08:17

My gripe is that the compiler shouldn't catch this here, it should only get caught at run-time.

Mike's example will always compile, yet not always work at run-time, and that's how I would expect CAST to work.

You should always be able to do the CAST with a type instead of needing a DYNAMIC-CAST with type name string like in this case.

Posted by Mike Fechner on 24-Mar-2016 08:18

But once it's referenced by a PLO you can CAST anything to anything - at least the compiler will let you. When the types are incompatible at runtime an runtime error will be thrown. And that is expected.

This behavior is needed, for instance when referencing objects in a temp-table field of type PLO. The compiler will allow any CAST from a PLO reference and it's our responsibility that the types match. Which is absolutely o.k.

So the question is really if the compiler check for CAST is too strict and a warning like suggested by Marian would be enough.

I must admit in the majority of cases, when the compiler creates an error, he is right. So I can live with the fact that when I know better I do the CAST(CAST( )) thing.

It's typically when the reference I hold to the original object is an Interface type, not a real class, that I need this.

Posted by Lieven De Foor on 24-Mar-2016 08:23

[quote user="Mike Fechner"]

It's typically when the reference I hold to the original object is an Interface type, not a real class, that I need this.

[/quote]
That's the case here too and is where we noticed it almost exclusively...

Posted by Laura Stern on 24-Mar-2016 08:23

The CAST statement is often used to check at runtime if you can cast something to something else and run conditional code if not.  If the code never compiled you would not be able to do that.  So that is how it is designed.

Posted by Mike Fechner on 24-Mar-2016 08:26

I'd rather use TYPE-OF to see if I can do things.

So maybe a bit more relaxed compile time checking, when the current reference is an Interface? But stay strict with Class references?

Posted by Lieven De Foor on 24-Mar-2016 08:32

Equivalent code in c# works fine:

   public class Form1 : Form

   {

       public Form1()

       {

           IButton myButton;

           myButton = ControlFactory.CreateButton();

           Controls.Add((Control)myButton);

       }

   }

Posted by Laura Stern on 24-Mar-2016 08:50

Ah yes.  Whew!  And I see that I contradicted myself!  The CAST should not make the compilation fail - especially when the type is an interface - since we of course won't know what that type is at runtime.  And that is the point of doing the cast - to tell the compiler to shut up and let you do this because you claim to know what you're doing!

Posted by Lieven De Foor on 24-Mar-2016 08:53

So should I escalate this one to technical support? Sounds like a valid bug?

Posted by Fernando Souza on 24-Mar-2016 08:56

You should log a call with support and we will discuss this internally based on your use case.

Posted by Mike Fechner on 24-Mar-2016 08:56

I love Laura's explanation of the CAST statement. I also tell student this is the "shut up I know what I'm doing" keyword. :-)

Posted by Lieven De Foor on 24-Mar-2016 09:01

Case 00342730 logged, thanks.

Posted by Laura Stern on 24-Mar-2016 09:25

Well, at least I made Mike happy this morning!  

Yes, this is definitely a bug; thanks for logging it.  I remember now (!!) that the compiler can sometimes determine that the CAST can never possibly work at runtime because the 2 objects bear no relationship with each other whatsoever. In that case it will fail to compile.  But, for example, if one object is in the other's hierarchy, in either direction, the CAST will compile successfully.  And as I said, for an interface, the compiler cannot make that judgment and should never error.

Posted by Peter Judge on 24-Mar-2016 09:32

FWIW me too :)
 

Posted by Simon L. Prinsloo on 24-Mar-2016 10:17

A type conversion like CAST(), INT() LOGICAL(), DECIMAL(), DATE(), is used to signal to the compiler that I know that data types are not compatible, but that I believe that it will be possible to convert it. The compiler say "Ok." and let it go. The runtime, on the other hand, will check again and throw an error if I was wrong.

Sometimes the compiler knows for sure it WILL NEVER WORK, and stop me:

DEF VAR AS myVar LOGICAL INIT TRUE NO-UNDO.
MESSAGE DATE(myVar).

Other times the compiler knows it COULD work and leave it up to the runtime catch me if I make a mistake:

DEF VAR myVar AS CHARACTER INIT "Hi" NO-UNDO. 
MESSAGE DATE(myVar).

The same should be true when I use CAST(), but it is not. CAST() is inconsistent with the rest of the language. Thus I argue that this is a bug. With CAST() the compiler does not allow every version of POSSIBLE, as it should, but block us even when the CAST() might be possible.

When both the source and the target are CLASS types, the compiler knows for sure that the CAST will only succeed if either:

  • The source type is in the inheritance hierarchy of target type.
  • The target type is in the inheritance hierarchy of target type.

However, as soon as either one or both of the source and the target types is an interface, the compiler has no way to know if the runtime instance held in an interface type will be from a specific class hierarchy, or that any instance in a class type will not be a (possibly derived) type implementing the interface. It only knows it COULD be. Thus, similar to other type conversion, CAST() should signal to the compiler to "skip this check" and the runtime must double check.

For example, the compiler can stop me from trying to convert an Eagle to a Cat, or a LOGICAL to a DATE, but it cannot stop me from converting STRING to a DATE or Mammal to a Dog. It is the runtime's job to stop me if my Mammal is actually a Cat. Yet the compiler it knows Cat is not a Dog, as neither one derives from the other. This works correct.

On the other hand, it is should be up to the runtime to figure out that the Bat in IFlying can be assigned to the Mammal variable or that the Eagle in IFlying can be assigned to a Bird type, but not a Mammal type. Also, the runtime must determine that the Bat in the Mammal variable or the Eagle in the Bird variable is an IFlying but the Ostrich in the Bird variable or the Elephant in the Mammal variable are NOT IFlying. The compile cannot know and should thus allow it.

Posted by Simon L. Prinsloo on 24-Mar-2016 10:20

Laura,

next time you agree with me, please post before I see the thread, rather while I am hammering out a no longer needed, long winded, response. :-)

Posted by Lieven De Foor on 24-Mar-2016 10:23

[quote user="Simon L. Prinsloo"]

Laura,

next time you agree with me, please post before I see the thread, rather while I am hammering out a no longer needed, long winded, response. :-)

[/quote]
You post isn't redundant.
I will add your response to the case I made, since it's a perfect explanation of the problem.

Posted by Thomas Mercer-Hursh on 24-Mar-2016 10:54

And, Simon, it was such a nice example too!

Posted by ymaisonn on 29-Mar-2016 05:40

I've logged defect PSC00346310 and created KB article 000067678.

Posted by Lieven De Foor on 06-Apr-2016 08:26

[quote user="ymaisonn"]

A fix for issue PSC00346310 will be released with 11.6.2.

OpenEdge 11.6.2. is planned to be released on July 27th, subject to change.

[/quote]

This thread is closed