I need a clarification on Delphi Type Casting
I wrote an example with two classes: TClassA and TClassB, TClassB derived from TClassA.
the code is as follows:
program TEST;
{$APPTYPE CONSOLE}
uses
System.SysUtils;
type
TClassA = class(TObject)
public
Member1:Integer;
constructor Create();
function ToString():String; override;
end;
type
TClassB = class(TClassA)
public
Member2:Integer;
constructor Create();
function ToString():String; override;
function MyToString():String;
end;
{ TClassA }
constructor TClassA.Create;
begin
Member1 := 0;
end;
function TClassA.ToString: String;
begin
Result := IntToStr(Member1);
end;
{ TClassB }
constructor TClassB.Create;
begin
Member1 := 0;
Member2 := 10;
end;
function TClassB.MyToString: String;
begin
Result := Format('My Values is: %u AND %u',[Member1,Member2]);
end;
function TClassB.ToString: String;
begin
Result := IntToStr(Member1) + ' - ' + IntToStr(Member2);
end;
procedure ShowInstances();
var
a: TClassA;
b: TClassB;
begin
a := TClassA.Create;
b := TClassB(a); // Casting (B and A point to the same Memory Address)
b.Member1 := 5;
b.Member2 := 150; // why no error? (1)
Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString])); // (2)
Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName])); // (3)
Writeln(Format('Address: a=%p, b=%p',[@a,@b])); // (4)
Writeln(b.MyToString); // why no error? (5)
readln;
end;
begin
try
ShowInstances;
except
on E: Exception do
Writeln(E.ClassName, ': ', E.Message);
end;
end.
The program output is:
ToString: a = 5, a = 5
Class Name: a=TClassA, b=TClassA
Address: a=0012FF44, b=0012FF40
My Values is: 5 AND 150
(1) what's the Member2 address? is this a possible "Access Violation"?
(2) ok, ToString() method point to the same address
(3) why a and b have the same ClassName?
(4) ok, a and b are two different variables
(5) if b is TClassA, why can use "MyToString" method?
A typecast example is the transformation of an integer into a string. This could be used to compare two numbers if one is stored as a string and the other is an integer. If compatible, Java must automatically perform a conversion called Automatic Type Conversion and, if not, it must be specifically cast or converted.
Type casting refers to changing an variable of one data type into another. The compiler will automatically change one type of data into another if it makes sense. For instance, if you assign an integer value to a floating-point variable, the compiler will convert the int to a float.
Type casting is a way of converting data from one data type to another data type. This process of data conversion is also known as type conversion or type coercion. In Java, we can cast both reference and primitive data types. By using casting, data can not be changed but only the data type is changed.
Type casting is the process in which the compiler automatically converts one data type in a program to another one. Type conversion is another name for type casting. For instance, if a programmer wants to store a long variable value into some simple integer in a program, then they can type cast this long into the int.
You're applying a hard type cast over the variable. When you do so, you're telling the compiler you know what you're doing and the compiler trusts you.
(1) what's the Member2 address? is this a possible "Access Violation"?
When you assign a value to a member of the class, the class definition of the variable is used by the compiler to calculate the offset of that member in the memory space, so when you have a class declaration like this:
type
TMyClass = class(TObject)
Member1: Integer; //4 bytes
Member2: Integer; //4 bytes
end;
the in-memory representation of this object looks like this:
reference (Pointer) to the object
|
|
--------> [VMT][Member1][Member 2][Monitor]
Offset 0 4 8 12
When you issue a statement like this:
MyObject.Member2 := 20;
The compiler just uses that information to calculate the memory address to apply that assignment to. In this case, the compiler the assignment may be translated to
PInteger(NativeUInt(MyObject) + 8)^ := 20;
So, your assignment succeeds just because the way the (default) memory manager works. An AV is originated by the operating system when you try to access a memory address that is not part of your program. In this case, your program has taken more memory from the OS than required. IMHO, when you don't get an AV, you, in fact, are unlucky, because your program memory may now be silently corrupted. Any other variable that happens to reside at that address may have changed its value (or meta-data), and it would result in undefined behavior.
(2) ToString() method point to the same address
Since the ToString() method is a virtual one, the address of that method is stored in the VMT and the call is determined at runtime. Take a look at What data does a TObject contain?, and read the referenced book chapter: The Delphi Object Model.
(3) why a and b have the same ClassName?
The class name is also part of the run-time metadata of the object. The fact that you're applying the wrong mold to the object doesn't change the object itself.
(4) a and b are two different variables
Of course, you declared it, look at your code:
var
a: TClassA;
b: TClassB;
Well, two different variables. In Delphi, the object variables are references, so, after some lines of code both reference the same address, but that's a different thing.
(5) if b is TClassA, why can use "MyToString" method?
Because you're telling the compiler that's OK, and as said, the compiler trusts you. It is hacky, but Delphi is also a low level language and you're allowed to do a lot of crazy things, if you want, but:
If you want (and you surely want most of the time) to be on the safe side, don't apply a hard cast like that in your code. Use the as operator:
The as operator performs checked typecasts. The expression
object as class
returns a reference to the same object as object, but with the type given by class. At runtime, object must be an instance of the class denoted by class or one of its descendants, or be nil; otherwise an exception is raised. If the declared type of object is unrelated to class - that is, if the types are distinct and one is not an ancestor of the other - a compilation error results.
So, with the as operator, you're safe, both at compile-time and at run-time.
Change your code to:
procedure ShowInstance(A: TClassA);
var
b: TClassB;
begin
b := A as TClassB; //runtime exception, the rest of the compiled code
//won't be executed if a is not TClassB
b.Member1 := 5;
b.Member2 := 150;
Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString]));
Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName]));
Writeln(Format('Address: a=%p, b=%p',[@a,@b]));
Writeln(b.MyToString);
readln;
end;
procedure ShowInstances();
begin
ShowInstance(TClassB.Create); //success
ShowInstance(TClassA.Create); //runtime failure, no memory corrupted.
end;
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With