-9-
Properties and Indexers
A field is simply a memory
location, whereas, a property is a collection of methods. A property is
represented by a value, in the same way as a field. Properties can be
considered as smart fields.
It is not compulsory to store
the value of a property in a field, but this is the accepted practice. The CLR
supports the syntax of properties, but these properties do not exist at
runtime.
a.cs
public class zzz
{
public static void Main()
{
aa a = new aa();
int gg = a.ff + 9;
System.Console.WriteLine(gg);
}
}
public class aa
{
public int ff
{
get
{
System.Console.WriteLine("in get");
return 12;
}
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig
static void vijay() il managed
{
.entrypoint
.locals (class aa V_0,int32 V_1)
newobj instance void aa::.ctor()
stloc.0
ldloc.0
call instance int32 aa::get_ff()
ldc.i4.s 9
add
stloc.1
ldloc.1
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
.class public auto ansi aa extends [mscorlib]System.Object
{
.method public hidebysig specialname instance int32 get_ff() il
managed
{
.locals (int32 V_0)
ldstr "in
get"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ldc.i4.s 12
stloc.0
br.s IL_000f
IL_000f: ldloc.0
ret
}
.property instance int32 ff()
{
.get instance int32 aa::get_ff()
}
}
Output
in get
21
We have created a property
called ff in the class aa. This property is written as a directive called
.property in the IL file, with the modifier instance, as it is a non-static
property, and with the return type int32.
There is an accessor called get, whose equivalent
directive in IL is also called as .get. This get is represented by the function
get_ff, that simply returns a value with the data type of the property.
In this case, the br instruction
is superfluous. The local variable V_0 is used to store the return value that
is to be placed on the stack.
The statement int gg = a.ff + 9;
gets executed in a unique way as follows:
• The this pointer is placed on the
stack.
• Then, the expression a.ff get replaced
by a call to a function get_ff from the class aa.
• Thereafter, the return value is placed
on the stack.
• The number 9 is placed on the stack,
followed by the add instruction.
• The property gets converted into a
function beginning with get.
On executing the il assembler
'ilasm' on a.il, you will see the following output
Output
Class 2 Methods: 1;
Props: 1;
a.cs
public class zzz
{
public static void Main()
{
aa a = new aa();
a.ff = 19;
}
}
public class aa
{
public int ff
{
set
{
System.Console.WriteLine(value);
}
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig
static void vijay() il managed
{
.entrypoint
.locals (class aa V_0)
newobj instance void
aa::.ctor()
stloc.0
ldloc.0
ldc.i4.s 19
call instance void aa::set_ff(int32)
ret
}
}
.class public auto ansi aa extends [mscorlib]System.Object
{
.method public hidebysig specialname instance void set_ff(int32
'value') il managed
{
ldarg.1
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
.property instance int32 ff()
{
.set instance void aa::set_ff(int32)
}
}
Output
19
A property with a set, obtains a
function called set_ff, having a parameter called value. We also have a
directive called .set.
Ldarg.1 is used to place the
first parameter of a function on the stack. The call to a property a.ff = 19
gets converted into the function call. Thus, a property actually consists of
two functions, get and set. They get called, depending on whether we want to
obtain the value of a property or change it, respectively.
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig
static void vijay() il managed
{
.entrypoint
ret
}
}
.class public auto ansi aa extends [mscorlib]System.Object
{
.property instance int32 ff()
{
}
}
Error checks in IL are sparse.
We have a property called ff, which does not have either a get or a set
directive. The C# compiler screams at this omission, but the IL assembler turns
a blind eye to this.
Hopefully, the next version of
IL should have more reasonable error checks. Having said this, henceforth, we
will not comment on the lack of error checks. It will be useful to remember in
the IL world, you are on your own. The excess freedom given by the IL assembler
also means that you have to assume greater responsibility as a programmer.
a.cs
public class zzz
{
public static void Main()
{
yyy a = new yyy();
a[1] = 17;
System.Console.WriteLine(a[1]);
}
}
public class yyy
{
public int this[int i]
{
set
{
System.Console.WriteLine("{0} {1} ",value ,i);
}
get
{
System.Console.WriteLine("{0}" , i);
return 23;
}
}
}
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)
newobj instance void
yyy::.ctor()
stloc.0
ldloc.0
ldc.i4.1
ldc.i4.s 17
call instance void
yyy::set_Item(int32,int32)
ldloc.0
ldc.i4.1
call instance int32 yyy::get_Item(int32)
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
.class public auto ansi yyy extends [mscorlib]System.Object
{
.method public hidebysig specialname instance void
set_Item(int32 i,int32 'value') il managed
{
ldstr "{0} {1}
"
ldarga.s 'value'
box
[mscorlib]System.Int32
ldarga.s i
box
[mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(class
System.String,class System.Object,class System.Object)
ret
}
.method public hidebysig specialname instance int32
get_Item(int32 i) il managed
{
.locals (int32 V_0)
ldstr
"{0}"
ldarga.s i
box
[mscorlib]System.Int32
call void [mscorlib]System.Console::WriteLine(class
System.String,class System.Object)
ldc.i4.s 23
stloc.0
br.s IL_0016
IL_0016:ldloc.0
ret
}
.property instance int32 Item(int32)
{
.get instance int32 yyy::get_Item(int32)
.set instance void yyy::set_Item(int32,int32)
}
}
Output
17 1
1
23
A indexer is a property. It has
no equivalent directive in IL. An indexer is simply a property with an extra
parameter, and no other complications. When we initialize a[1]using the
statement a[1] = 17, we are actually placing three parameters on the stack:
• The this pointer
• The array index 1
• The value 17.
Then, we call set_Item, as it is
an indexer and not a property. The two parameters to the function are i and
value. If you remember, the indexer variable has been named i.
The function get_Item gets
called with the single parameter i and returns a value. The first parameter to
the WriteLine function is a string and the rest of the parameters are objects.
We need to convert our int value types into objects. Thus we need to box them.
• Using the function set_Item, we are
displaying the index and the value.
• Using the function get_Item, we are
displaying only the value.
• Using the last WriteLine function, we
are displaying the value of a[1], which is 23.
Thus, indexers are an alias for
a property with an extra parameter.
The properties directive is used
only by compilers and other tools, to understand as to what methods are being
associated with the property. If you are not convinced, you can delete the
property directive from the above programs and run them. There will be no
change at all in the way they execute.
a.cs
public class zzz {
public static void Main() {
yyy a = new xxx();
a.ff = 19;
}
}
public class yyy
{
public virtual int ff
{
set
{
System.Console.WriteLine("yyy");
}
}
}
public class xxx : yyy
{
public override int ff
{
set
{
System.Console.WriteLine("xxx");
}
}
}
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)
newobj instance void
xxx::.ctor()
stloc.0
ldloc.0
ldc.i4.s 19
callvirt instance void
yyy::set_ff(int32)
ret
}
}
.class public auto ansi yyy extends [mscorlib]System.Object
{
.method public hidebysig newslot specialname virtual instance
void set_ff(int32 'value') il managed
{
ldstr
"yyy"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.property instance int32 ff()
{
.set instance void yyy::set_ff(int32)
}
}
.class public auto ansi xxx extends yyy
{
.method public hidebysig specialname virtual instance void set_ff(int32 'value') il managed
{
ldstr
"xxx"
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
yyy::.ctor()
ret
}
.property instance int32 ff()
{
.set instance void xxx::set_ff(int32)
}
}
Output
xxx
The above example demonstrates the
use of virtual properties. The concept of a property is simply an illusion. As
mentioned earlier, properties are converted into a series of functions. Thus
what applies to virtual functions also applies to virtual properties. We cannot
use the modifier virtual in the properties directive.
The rationale behind using a
property over a field is:
If the value of a field changes,
no code gets called. The class is thus, unaware of the change. In the case of a
property, a method gets called. This method can contain a large amount of code.
This code can do anything.
Also, we can be very sure that
the user does not change the value of the property beyond certain acceptable
limits. A method call can be optimised, and hence, a property does not carry
any significant overhead as compared to a direct access to a field. The only
disadvantage is that the properties cannot be made global.
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)
newobj instance void
yyy::.ctor()
stloc.0
ldloc.0
ldc.i4.s 19
call instance void
yyy::set_ff(int32)
ret
}
}
.class public auto ansi yyy extends [mscorlib]System.Object
{
.field int32 jj
.method public hidebysig specialname instance void set_ff(int32 'value') il managed
{
ldstr
"yyy"
call void
[mscorlib]System.Console::WriteLine(class System.String)
ret
}
.property instance int32 ff()
{
.set instance void yyy::set_ff(int32)
.backing int32 jj
}
}
Output
yyy
A property normally has a field
that stores the value associated with a property. Since the property directive
is used, for documentation purposes, it would not be a bad idea to have a
directive called backing, which can be used to state the name of this field. We
are not forced to do so. The assembler only checks to make sure that the filed
is present. It is not used in any way. It must have the same data type as the
property. Using the attribute specialname, we can inform the compiler to give
it special treatment.
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)
ret
}
}
.class public auto ansi yyy extends [mscorlib]System.Object
{
.method public hidebysig specialname instance void set_ff(int32 'value') il managed
{
ret
}
.property instance int32 ff()
{
.set instance void yyy::set_ff(int32)
.other void abc()
}
}
We finally have one last
directive called .other, that specifies the other functions that are associated
with the property. In this case, the assembler does not check for the existence
of the function, and thus, we have not included it.
To summarise, the properties
directive is implemented as a series of method calls. The same is true for
indexers also.