-6-
Reference and Value Types
Interfaces
An interface is a reference
type, in spite of the fact that it has no code at all. Thus, we cannot
instantiate an interface. We can use it as a construct for the creation of new
types. An interface defines a contract that is left to the class to implement.
An interface can have static
fields. If an interface contains 10 abstract virtual functions, then the class
implementing from that interface has to supply the code for all 10 of them.
Thus, if a class does not provide all the function implementations, then we
cannot use the class. In such a scenario, a class derived from it must provide
the implementation.
The interface keyword in C# is a
class, which the documentation describes as a semantic attribute.
a.il
.assembly mukhi {}
.class zzz
{
.method static void vijay()
{
.entrypoint
ret
}
}
.class interface yyy
{
}
We are not allowed to place any
code in an interface. An interface consists only of the function prototype,
followed by a pair of curly braces {}.
a.il
.assembly mukhi {}
.class zzz
{
.method static void vijay()
{
.entrypoint
ret
}
}
.class interface yyy
{
.method static void vijay1()
{
ret
}
}
Output
***** FAILURE *****
vijay1 is a function created in
the interface yyy. As this is not permitted, the il assembler has the domino
effect as shown above.
a.il
.assembly mukhi {}
.class zzz
{
.method static void vijay()
{
.entrypoint
ret
}
}
.class interface yyy
{
.field int32 i
}
Output
***** FAILURE *****
No variables either can be
placed inside an interface. This rule is similar to that of C#. Even though the
documentation says that we can place static fields in an interface, when we
tried to do so, an error was generated.
a.il
.assembly mukhi {}
.class zzz
{
.method static void vijay()
{
.entrypoint
newobj instance void yyy::.ctor()
ret
}
}
.class interface yyy
{
}
Output
Exception occurred: System.ExecutionEngineException: An
exception of type System.ExecutionEngineException was thrown.
at zzz.vijay()
If we create an object such as
an interface using newobj, the assembler does not generate any error, but the
runtime throws an exception.
a.il
.assembly mukhi {}
.class zzz implements yyy
{
.method static void vijay()
{
.entrypoint
ret
}
}
.class interface yyy
{
.method public hidebysig newslot virtual abstract instance void
a1() il managed
{
}
}
Output
Exception occurred: System.TypeLoadException: Could not load
class 'zzz' because the method 'a1' is not defined.
Exception occurred: System.MissingMethodException: Could not
find the entry point.
The above program has only one
function, a1 in the interface. This function has a pair of curly braces {}, but
as mentioned earlier, we are not allowed to place any code within them.
The class zzz has been derived
from yyy, using the keyword implements. The assembler does not check whether
all code of the interface is implemented as it is done only at runtime, and
hence, an exception is generated. Thus, we can see that most IL errors occur at
run time, and not at compile time.
a.cs
class zzz
{
public static void Main()
{
}
}
interface ddd
{
void a1();
void a2();
}
a.il
.assembly mukhi {}
.class public auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
.class interface private abstract auto ansi ddd
{
.method public hidebysig newslot virtual abstract instance void
a1() il managed
{
}
.method public hidebysig newslot virtual abstract instance void
a2() il managed
{
}
}
To reiterate what we have said
earlier, an interface in C# becomes a class directive in IL with the interface
modifier added to it. The two functions a1 and a2 become actual functions in
the class ddd, and are marked as virtual, newslot as well as abstract, i.e.
having no implementation.
a.il
.assembly mukhi {}
.class public auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
.class interface private abstract auto ansi ddd
{
.method public hidebysig newslot virtual abstract instance void
a2() il managed
{
ret
}
}
Error
***** FAILURE *****
We cannot place any code,
including a ret, in a method that is marked as abstract. This modifier
signifies that the code for the function will be provided from some other
source. Inspite of what the documentation says, a static constructor cannot be
placed in an interface.
a.cs
class zzz : ddd,eee {
public static void Main()
{
}
}
interface ddd
{
}
interface eee
{
}
a.il
.assembly mukhi {}
.class interface private abstract auto ansi ddd
{
}
.class interface private abstract auto ansi eee
{
}
.class private auto ansi zzz extends [mscorlib]System.Object
implements ddd,eee
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
For the purposes of inheritance,
C# does not differentiate between an interface and a class. There is, however,
a subtle difference between them in a sense that, we can derive from more than
one interface, but not from more than one class.
In IL, there is a marked
differentiation between an interface and a class. We extend from a class and
implement an interface. This is the same syntax that the Java programming
language uses.
When one compares C# with Java,
their support for features such as these should be highlighted.
a.cs
class zzz {
public static void Main()
{
yyy a = new yyy();
ddd d = a;
d.a1();
a.a1();
}
}
interface ddd
{
void a1();
}
class yyy : ddd
{
public void a1()
{
System.Console.WriteLine("a1");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object {
.method public hidebysig static void vijay() il managed {
.entrypoint
.locals (class yyy V_0,class ddd V_1)
newobj instance void
yyy::.ctor()
stloc.0
ldloc.0
stloc.1
ldloc.1
callvirt instance void
ddd::a1()
ldloc.0
call instance void yyy::a1()
ret
}
}
.class interface private abstract auto ansi ddd
{
.method public hidebysig newslot virtual abstract instance void
a1() il managed
{
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
implements ddd
{
.method public hidebysig newslot final virtual instance void
a1() il managed
{
ldstr "a1"
call void [mscorlib]System.Console::WriteLine(class
System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void
.ctor() il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
Output
a1
a1
We created two locals that look
like class yyy and interface ddd. Then, we created an object that looks like
yyy and initialized the variable V_0 to it.
The statement d =a is translated
to: loading the value of V_0 on the stack, and using instruction stloc.1 to
initialize the variable V_1. Thereafter, calling the function a1 of the
interface ddd.
We loaded the variable value V_1
on the stack. Since it was called through the interface, we used callvirt
instead of call. If we had called it through the object of type yyy, then we
would have used call.
Thus, IL understands that a call
through an interface object is to be treated in a special manner. We can change
the last occurrence of ldloc.1 to ldloc.0, since both have the same values. A
call to an interface is evaluated at run time, as the assembler does not
convert it into a class access. In the locals directive, the word class, and
not interface, is placed in front of ddd.
Thus, calling a function through
an interface is equivalent to using callvirt since, there is no code in the
interface. A callvirt takes more time to execute than the plain call
instruction. However, callvirt introduces dynamism at runtime.
a.cs
class zzz
{
public static void Main()
{
ddd d = new yyy();
eee e = new yyy();
d.a1();
e.a1();
}
}
interface ddd
{
void a1();
}
interface eee
{
void a1();
}
class yyy : ddd , eee
{
void ddd.a1()
{
System.Console.WriteLine("ddd a1");
}
void eee.a1()
{
System.Console.WriteLine("eee a1");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals ( class yyy v , class eee v1)
newobj instance void
yyy::.ctor()
stloc.0
newobj instance void
yyy::.ctor()
stloc.1
ldloc.0
callvirt instance void
ddd::a1()
ldloc.1
callvirt instance void
eee::a1()
ret
}
}
.class interface private abstract auto ansi ddd
{
.method public hidebysig newslot virtual abstract instance void
a1() il managed
{
}
}
.class interface private abstract auto ansi eee
{
.method public hidebysig newslot virtual abstract instance void
a1() il managed
{
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
implements ddd,eee
{
.method private hidebysig newslot final virtual instance void
ddd.a1() il managed
{
.override ddd::a1
ldstr "ddd a1"
call void [mscorlib]System.Console::WriteLine(class
System.String)
ret
}
.method private hidebysig newslot final virtual instance void
eee.a1() il managed
{
.override eee::a1
ldstr "eee
a1"
call void [mscorlib]System.Console::WriteLine(class
System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void
.ctor() il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
Output
ddd a1
eee a1
We have created a class yyy that
is derived from two interfaces, ddd and eee, that have the same function a1.
Since we want a separate
implementation for each, we have to preface each occurrence of a1 with the name
of the interface, i.e. either ddd or
eee, in the class yyy.
We have created the two objects
that look like yyy and stored them in classes that look like ddd and eee. Since
the function is called from an interface pointer, we have to use callvirt
instead of call. In the IL code, the two interfaces are created as shown
earlier.
In class yyy, we implement from
ddd and eee, but since the two functions cannot have the same name, we have to
preface the name of the function with the name of the interface.
In the method, we have used a
directive called .override. This directive
clearly specifies as to which function from a specified interface the
function override. Calling of an interface is a run time issue. The CLR does
all the routine work.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object {
.method public hidebysig static void vijay() il managed {
.entrypoint
.locals (class xxx V_0,class ddd V_1)
newobj instance void
xxx::.ctor()
stloc.0
ldloc.0
stloc.1
ldloc.1
callvirt instance void
ddd::a2()
ldloc.0
call instance void xxx::a2()
ret
}
}
.class interface private abstract auto ansi ddd
{
.method public hidebysig newslot virtual abstract instance void
a1() il managed
{
}
.method public hidebysig newslot virtual abstract instance void
a2() il managed
{
}
}
.class private auto abstract ansi yyy extends
[mscorlib]System.Object implements ddd
{
.method public hidebysig newslot final virtual instance void
a1() il managed
{
ldstr "a1"
call void [mscorlib]System.Console::WriteLine(class
System.String)
ret
}
.method public hidebysig newslot virtual instance void a2() il
managed
{
ldstr "a2"
call void [mscorlib]System.Console::WriteLine(class
System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void
.ctor() il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
.class xxx extends yyy
{
.method public hidebysig newslot final virtual instance void
a2() il managed
{
ldstr "a22"
call void [mscorlib]System.Console::WriteLine(class
System.String)
ret
}
}
Output
a2
a22
The above program clears a large
number of cobwebs. Let us start analysing this program from the beginning.
We have a class interface ddd
that has two functions a1 and a2. We then create a class yyy, that implements
from ddd and contains code for only one
function a1.
This makes the class incomplete
and hence, we tag it with the modifier abstract. However, this modifier is
optional. One more class xxx is created, that derives from yyy and implements
the second function a2. All goes well so far.
Then, using call, the function a2 of class xxx is called.
However, when we call the same function off the interface ddd using callvirt,
the function is called off class yyy and not xxx. This is so as the function in
class xxx has nothing to do with the one in class yyy.
Compare this example with the
override modifier example shown earlier. If we eliminate the code of function
a2 from the class yyy, we get the following error:
Output
Exception occurred: System.TypeLoadException: Could not load
class 'xxx' because the method 'a2' is not defined.
at zzz.vijay()
The function a2 is present in
the class xxx, but it has been eliminated from the class yyy. However, the
function needs to be present in both the classes.
The same rules of nesting apply
to interfaces also. Nothing stops an interface from implementing another
interfaces, using the keyword implements. Here, the word implements may be
misleading.
The keyword implies that the class that implements this
interface must provide the code for it.
An interface has five
restrictions:
i. All methods must be either virtual or
static.
ii. The virtual methods must be abstract and
public.
iii. No
instance fields are allowed.
iv. An interface is abstract and cannot be
instantiated.
v. An interface cannot inherit from a class.
vi. Under no
circumstances can an interface contain code.
Structures
A structure handles memory more
efficiently than a class. IL does not support a struct type directly. As IL
does not recognise a structure, it does not enforce the following rules:
• Constructors must have parameters
• All members of a structure must be initialised
before leaving the constructor.
Also, structures are derived
from ValueType and not Object.
The type system of the .Net
world is simplicity personified. It divides all the known types into one of the
two categories: a value type or a reference type.
• A reference type is known by a
reference, that is, a memory location that stores the address where the object
resides in memory.
• A value type, however, is directly
stored in the memory location occupied by the variable that represents the type.
Value types are used to
represent small data items like local variables, integers, numbers with decimal
places etc. The memory allocated is on the stack and not on the heap.
To access a reference type, the
location of the variable in memory is to be first determined. This is not true
for a value type. Hence, there is no overhead of an indirection involved with a
value type and therefore, it is much more efficient.
The disadvantage of a value type
is that they cannot be derived from and, if the data they represent is fairly
large, then copying the type on the stack is not an efficient way of
representing that type. There is no
need to instantiate a variable of value type, as it is already instantiated.
Apart from these variations, value types are similar to reference types.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (value class xxx v)
ldloca v
initobj value class xxx
ldloca v
ldfld int32 xxx::i
call void System.Console::WriteLine(int32)
ret
}
}
.class value xxx
{
.field public int32 i
.field public int32 j
}
Output
0
In the above example, we have
created a value class or a value type called xxx, that has two public fields i
and j. We used the instruction initobj to create a new value type.
To display the value of i, we
first created a local variable that represents our value class. In our case,
the variable is called v. ldloca is used to load the address of the variable v
on the stack. Then we called initobj with the name of the value class xxx as a
parameter thus creating a a new value type.
We, then, again load the address
of the value type v on the stack and call ldfld. This instruction needs the
address of the value type on the stack to work with. The only reason that the
value of i is ZERO is that the instruction initobj guarantees that all members
of the value type will be initialized to zero.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object {
.method public hidebysig static void vijay() il managed {
.entrypoint
.locals (value class xxx v)
ldloca v
initobj value class xxx
ldloca v
ldc.i4.2
call instance void xxx::.ctor(int32)
ldloca v
ldfld int32 xxx::i
call void System.Console::WriteLine(int32)
ret
}
}
.class value xxx
{
.field public int32 i
.field public int32 j
.method public hidebysig specialname rtspecialname instance
void .ctor(int32 p) il managed
{
ldarg.0
ldarg.1
stfld int32 xxx::i
ret
}
}
Output
2
The correct way to initialize a
value class is to call the constructor. We first have to load the address of
the value type v on the stack. Then, since the constructor expects a single
parameter on the stack, we place the number 2 on the stack using ldc. The
constructor is then called in the same manner as we call any other function.
In the constructor, we first
place the this pointer on the stack. The this pointer, or the first invisible
parameter to a function, is a reference to the starting location of the object
in memory. Parameter 1 is placed on the stack and stfld is called.
The constructor initializes all
members of a value class. The static fields of a value class are initialized
when the value type is first loaded.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (value class xxx v)
ldloca v
ldfld int32 xxx::i
call void System.Console::WriteLine(int32)
ret
}
}
.class value xxx
{
.field public int32 i
.field public int32 j
}
Output
51380288
Not using initobj, like in the
above example will assign a random value to the value type. The use of initobj
is optional. This instruction requires a managed pointer to an instance of the
value type and it is one of the few instructions that does not return anything
on the stack. The constructor is never called by the initobj instruction. The
sole role initobj performs is to initialize all the value class members to
ZERO.
While verifying code, one should
ensure that all the fields of a value type are assigned a value before they are
read or passed as parameters to a method. The code in constructor assigns
values to every field.
You can see the contrast between
initobj and newobj. Value Types use initobj whereas reference types use newobj.
Also, value types are derived from System.ValueType.
Value types can have static,
instance and virtual methods. Here, the
static methods are called in a similar manner when in a class.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (value class xxx v)
ldloca v
ldc.i4.3
call instance void xxx::a1(int32)
ldloca v
ldfld int32 xxx::i
call void System.Console::WriteLine(int32)
ret
}
}
.class value xxx
{
.field public int32 i
.field public int32 j
.method public instance void
a1(int32 p) il managed
{
ldarg.0
ldarg.1
stfld int32 xxx::i
ret
}
}
Output
3
To call an instance function of
a value class, there is no need for
either initobj or the constructor call. But, it is a good practice to do
so. We have to place the address of the value type or the this pointer on the
stack and then place the parameters. The function a1 uses the this pointer to
access the fields.
We modified the function abc to
read as follows:
.method public virtual instance void a1(int32 p) il managed
Despite making the function virtual, the program executes
as before. The order of the virtual
modifier is very important.
You may recall that a virtual
function has to be called using the instruction callvirt and not the
instruction call. However, in the case of a value class, we cannot use
callvirt. Instead, the instruction call is used.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (value class xxx v)
ldloca v
box xxx
callvirt instance void ddd::a1()
ret
}
}
.class interface ddd
{
.method public hidebysig newslot virtual abstract instance void
a1() il managed
{
}
}
.class value xxx implements ddd
{
.field public int32 i
.field public int32 j
.method public virtual instance void a1() il managed
{
ldstr "hi"
call void System.Console::WriteLine(class System.String)
ret
}
}
Output
hi
In the above program, we
specified an interface ddd, that contains a single function called a1. We
created a value class xxx that implements from ddd.
Our intention is to call the
function a1 from the interface ddd. As mentioned earlier, to call a function
off an interface, the instruction callvirt has to be used and not the
instruction call, as, an interface does not contain any code.
The callvirt instruction
requires a reference type on the stack because it does not work with value
types. Thus, we use ldloca to load the address of the value type on the stack.
Then, we use the box instruction to convert it into a reference type.
Output
Exception occurred: System.NullReferenceException: Attempted
to dereference a null object reference.
at zzz.vijay()
If we comment out the box
instruction, the following exception is generated because callvirt looks for a
a boxed type on the stack:
Boxing
and Unboxing
a.cs
public class zzz
{
public static void Main()
{
yyy a = new yyy(10,20);
yyy b;
b = a;
System.Console.WriteLine( b.i );
}
}
class yyy
{
public int i,j;
public yyy(int x, int y)
{
System.Console.WriteLine("Const");
i=x;j=y;
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0,class yyy V_1)
ldc.i4.s 10
ldc.i4.s 20
newobj instance void
yyy::.ctor(int32,int32)
stloc.0
ldloc.0
stloc.1
ldloc.1
ldfld int32 yyy::i
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
.field public int32 i
.field public int32 j
.method public hidebysig specialname rtspecialname instance
void .ctor(int32 x,int32 y) il managed
{
ldarg.0
call instance void
[mscorlib]System.Object::.ctor()
ldstr
"Const"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ldarg.0
ldarg.1
stfld int32 yyy::i
ldarg.0
ldarg.2
stfld int32 yyy::j
ret
}
}
Output
Const
10
The constructor assigns values
to fields. It places the values on the stack and uses stfld to assign the
values to the fields. The question that arises is that what happens when we
equate reference objects with each other.
The explanation is very simple:
A reference object is simply a memory location stored in a local variable. The
variable V_0 contains a reference to the newly created object in memory. We
place this value on the stack and use ldloc.1 to initialize the variable V_1 to
this value.
Thus, a reference object is a
number representing the memory location of
an object. Here, the same number is stored in the objects a and b. Hence
b.i displays the number 10. Here, the constructor does not get called again, as
no new object is created.
a.cs
class zzz {
public static void Main()
{
xxx a = new xxx(1);
object b = a;
a.x = 2;
System.Console.WriteLine(((xxx)b).x);
}
}
struct xxx
{
public int x;
public xxx(int i)
{
x = i;
}
}
a.il
.assembly mukhi {}
.class public auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (value class xxx V_0,class System.Object V_1,value class
xxx V_2)
ldloca.s V_0
ldc.i4.1
call instance void
xxx::.ctor(int32)
ldloca.s V_0
box xxx
stloc.1
ldloca.s V_0
ldc.i4.2
stfld int32 xxx::x
ldloc.1
unbox xxx
ldobj xxx
stloc.2
ldloca.s V_2
ldfld int32 xxx::x
call void
[mscorlib]System.Console::WriteLine(int32)
ret
}
}
.class value private auto ansi sealed xxx extends
[mscorlib]System.ValueType
{
.field public int32 x
.method public hidebysig specialname rtspecialname instance
void .ctor(int32 i) il managed
{
ldarg.0
ldarg.1
stfld int32 xxx::x
ret
}
}
Output
1
This program and the next one
should be read in conjunction if you want to grasp the following:
• Concepts of boxing and unboxing
• The major difference between a class
and a structure.
The concept of a structure is
not supported by IL. On conversion to IL, a struct becomes a class with the
modifier value added to it. It is sealed and derived from ValueType hence
referred to as a value class.
In the C# program, an object is
created, which is an instance of the structure xxx. The constructor is passed
the constant value 1, which is used to initialize the int field x to 1. Then,
the object b is initialized to a. Next, we change the value of the member x
from 1 to 2 using the value object a.
We display the value of the
field x using b. The cast operator is used as the data type of b is Object and
not xxx. We notice that there are two x ints in memory:
• One with a value of 2 that is
associated with a,
• The other one with the value of 1 that
is associated with b.
So much for the C# program, let
us see as to what happens in our IL program. We create 3 objects in IL i.e. two
variables V_1 and V_2 of the class xxx and one that looks like an Object.
We place the address of V_0 on
the stack followed by the value 1. Then, we call the constructor using a call
and not newobj, since we have a value class or structure and not a pure class.
The constructor initializes the field x to 1.
We have to convert this value
class to a pure object that is an instance of the class Object. We load the address
of V_O and call the box instruction, which converts a value class into a class
and places the reference of the newly created object on the stack.
Then, we store this reference in
the local variable V_1 using stloc.1. This is the code generated when the
statement object b = a is converted to IL. We have created a fresh object using
the box instruction. Thus there are two xxx objects in memory, one as the value
object V_0 and one as a reference object V_1.
We now need to initialize the
field x to 2. To do so, the constant 2
is placed on the stack and stfld is called. The easier part of the code is
over.
The problem is in the expression
WriteLine((xxx)b).x. The object b or V_1 is a reference object. We have to cast
it to a value object. To do this, we need to unbox it. The act of converting a
reference object to a value object is called unboxing. The unbox instruction
requires a reference type on the stack and it will place a value type whose
data type is specified by the name following xxx.
The instruction ldobj loads an
instance of xxx on the stack whose pointer is already present on the stack. We
store this instance in V_2 and load this value type again on the stack. Then we
load the value of x and display it using WriteLine.
a.cs
class zzz
{
public static void Main()
{
yyy c = new yyy(1);
object d = c;
c.x = 2;
System.Console.WriteLine(((yyy)d).x);
}
}
class yyy
{
public int x;
public yyy(int i)
{
x = i;
}
}
a.il
.assembly mukhi {}
.class public auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0,class System.Object V_1)
ldc.i4.1
newobj instance void
yyy::.ctor(int32)
stloc.0
ldloc.0
stloc.1
ldloc.0
ldc.i4.2
stfld int32 yyy::x
ldloc.1
castclass yyy
ldfld int32 yyy::x
call void
[mscorlib]System.Console::WriteLine(int32)
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
.field public int32 x
.method public hidebysig specialname rtspecialname instance
void .ctor(int32 i) il managed
{
ldarg.0
call instance void
[mscorlib]System.Object::.ctor()
ldarg.0
ldarg.1
stfld int32 yyy::x
ret
}
}
Output
2
c is an object of type yyy and
holds a value of 1 in its member x. Object d is another class or call it a
structure, it does not create a new object in memory but instead, points to the
same object referenced by c. Thus, we have one yyy object in memory, and
any changes made to the value of x
using d will be reflected when using c and vice-versa.
Here, as we are dealing with a
class, the instruction newobj is used to create it. To initialize the object d
to c, we first use ldloc.0 to place its value on the stack and then use the
instruction stloc.1 to initialize local V_1.
Then, we initialize c.x to 2 in
the usual manner, by first placing the reference on the stack using ldloc.0 and
then, placing the value on the stack using stfld.
Object d is already a reference
object and yyy is a class. Hence we simply use castclass. It is easy to use
casting here because neither boxing nor unboxing is required to be carried out.
The important point to be
mentioned is that we are not creating another yyy in memory, and hence, there
is only one field x in memory. This was not the case earlier case, when a
structure was used.
a.cs
class zzz
{
public static void Main()
{
long f = 1;
object b = f;
int i = (int)b;
}
}
a.il
.assembly mukhi {}
.class public auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int64 V_0,class System.Object V_1,int32 V_2)
ldc.i4.1
conv.i8
stloc.0
ldloca.s V_0
box
[mscorlib]System.Int64
stloc.1
ldloc.1
unbox
[mscorlib]System.Int32
ldind.i4
stloc.2
ret
}
}
This example is deceptively
similar to the one above.
First we take a reference object
b and equate it to a value object f. Then we cast the reference type object b
to a value type int. The C# compiler gives us no errors but a runtime exception
is thrown.
Back to IL. We first create a
long or an int64 V_0 using locals. Then we create an Object V_1 and finally an
int V_2.
We thereafter, place 1 on the
stack, convert it into 8 bytes using conv.i8 and use stloc.0 to store the value
in V_0. The address is then placed on the stack, as we need to use the box
instruction to convert it into a reference type, which is finally to be stored
in b or V_1.
Unbox the object b, the one
created out of a value type, and store in an int. To do this, we need to place
a reference on the stack and call unbox. This will place a value address on the
stack and use ldind.i4 to fetch the value stored at this address. Then, we use
stloc.2 to initialize the variable V_2. The exception clearly states that we
cannot cast an object that is a reference type to a value type.
Output
Exception occurred: System.InvalidCastException: An exception
of type System.InvalidCastException was thrown.
at zzz.vijay()
a.cs
class zzz {
public static void Main() {
}
int x;
void abc( int x)
{
this.x = x;
}
}
a.il
.assembly mukhi {}
.class public auto ansi zzz extends [mscorlib]System.Object
{
.field private int32 x
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
.method private hidebysig instance void abc(int32 x) il managed
{
ldarg.0
ldarg.1
stfld int32 zzz::x
ret
}
}
Languages decide on how you
write code and name variables. In C# a field and a parameter to a function can
have the same names, but the parameter name has more visibility than the field
name.
this.x refers to the field name
in the function abc unlike the parameter named x. In IL, this dilemma does not
arise, as we have one set of instructions that deals with fields, a second set
that deals with parameters to functions and a third set that deals with locals.
Thus there is no way that a name clash can ever occur.
a.cs
class zzz
{
public static void Main()
{
xxx x = new xxx(10);
System.Console.WriteLine(x.i);
x.abc();
}
}
struct xxx
{
public int i;
public xxx( int j)
{
i = j;
}
public void abc()
{
System.Console.WriteLine("abc");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (value class xxx V_0)
ldloca.s V_0
ldc.i4.s 10
call instance void
xxx::.ctor(int32)
ldloca.s V_0
ldfld int32 xxx::i
call void
[mscorlib]System.Console::WriteLine(int32)
ldloca.s V_0
call instance void
xxx::abc()
ret
}
}
.class value private auto ansi sealed xxx extends
[mscorlib]System.ValueType
{
.field public int32 i
.method public hidebysig specialname rtspecialname instance
void .ctor(int32 j) il managed
{
ldarg.0
ldarg.1
stfld int32 xxx::i
ret
}
.method public hidebysig instance void abc() il managed
{
ldstr
"abc"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
10
abc
We take one more program on
structures before we close this chapter. . We have created a struct containing
a field i and a function abc. Structures are value objects and are stored on
the stack and not on the heap. Thus, the word value has been used in the locals
directive. It is a class, but of a value type.
In the definition of the
structure, we have added two modifiers, sealed and value. Therefore, we cannot
derive from this value class. Everything else is similar to a class.