|
Attributes
are used to specify additional information about an entity.
This information gets written in metadata at compile time.
An entity to which an attribute can be applied is known as
the attribute target. The target can be an assembly,
interface, structure, class and all possible class members.
An attribute is applied to the target by specifying the attribute
name in brackets as shown below.
[Serializable]
This attribute is applied to a class stating that objects
of this class can be serialised. When the compiler encounters
this attribute, it adds suitable instructions in the metadata.
There are thousands of pre-defined attributes in FCL. In addition
to them .NET allows us to define our own attributes, called
custom attributes. In this article we will examine
the process of defining and using custom attributes and reading
them using reflection.
We will create a custom attribute that can be applied to create
a documentation of a class and its members. Documentation
of a class would include the purpose of the class and how
many methods it has, whereas documentation of method would
include their purpose. We can write comments to achieve this.
However, comments remain in the source code and are not accessible
through the assembly. If we add documentation through attributes
they go to the assembly and can be accessed using reflection.
Declaring an Attribute
As everything in C# is declared in a class, an attribute is
also embodied in a class. To create a custom attribute we
have to derive a class from the System.Attribute class. We
have named our derived class as DocumentAttribute. The class
DocumentAttribute has three data members as shown below.
class DocumentAttribute:System.Attribute
{
string info;
int cnt;
}
The string variable info keeps a description of the entity
and the integer variable cnt keeps count of the methods. We
can declare attributes either with parameters or without parameters.
The parameters can be positional parameters or named parameters.
The positional parameters are initialised in constructor,
whereas, named parameters are initialised by defining a property.
Another difference between the two is that positional parameters
must be passed in the same sequence as given in the constructor.
The named parameters are optional and can be passed in any
sequence. We have declared the Document attribute to take
two parameters. We want that one of them viz. info should
be the positional parameter and cnt should be the named parameter.
This is because cnt would be used only if attribute is applied
to class. For class members we can drop it. So, we must define
a constructor and a property as shown below.
public DocumentAttribute (string i)
{
info = i ;
}
public int Count
{
get
{
return cnt ;
}
set
{
cnt = value ;
}
}
As we know, custom attributes are written in metadata and
can be read through reflection. Generally speaking, reading
an attribute means reading values of data members of the attribute
class. If we try to read the Document attribute it wont
be able to access the info data member as it is declared private.
So, in order to expose it through metadata, we would write
a read-only property. Here it is:
public string Info
{
get
{
return info ;
}
}
We must also mention the attribute targets. This can be done
by using an attribute called AttributeUsage. The AttributeUsage
attribute targets classes. So, it has to be declared just
above the DocumentAttribute class as shown below.
[AttributeUsage (AttributeTargets.Class | AttributeTargets.Method |
AttributeTargets.Constructor | AttributeTargets.Field,
AllowMultiple = false )]
class DocumentAttribute : System.Attribute
{
}
The AttributeUsage attribute takes two parameters. Using the
first parameter we can specify attribute targets. By setting
the second parameter to true we can specify that one member
can have multiple Document attributes. When attribute targets
are assembly or module, the attributes should be placed immediately
after all using statements and before any code.
Now that our attribute has been readied, let us see how to
use it.
Using an Attribute
We would use the Document attribute to create documentation
of the class test1. Here is the declaration of test1 class.
[Document (Class test1: created for testing custom attributes, Count = 2 )]
class test1
{
[Document ( i: Counter variable )]
int i = 0;
[Document ( Ctor test1: zero-argument ctor )]
public test1()
{
}
[Document ( increment( ): Increments counter )]
public void increment()
{
i++ ;
}
[Document ( decrement( ): Decrements counter )]
public void decrement()
{
i--;
}
}
We have applied the Document attribute to the class through
the statement
[Document (Class test1: created for testing custom attributes, Count=2 )]
Note that although name of our class is DocumentAttribute
we have omitted the word Attribute herethe compiler
would add it in automatically. When the compiler encounters
this statement, it searches for a class derived from the System.Attribute
class in all the namespaces mentioned in using statements.
The positional parameters are passed as the normal parameters,
but named parameters are passed by mentioning the name of
the parameter like
Count = 2
Needless to say this would invoke the set block of Count property.
On similar grounds, we have applied our attribute to other
class members.
Reading Attributes
The following code snippet shows how to read the Document
attribute applied to a class.
Type t=typeof(test1);
object[] a= t.GetCustomAttributes(typeof (DocumentAttribute), false);
foreach (object att in a)
{
DocumentAttribute d=(DocumentAttribute) att;
Console.WriteLine ({0} {1}, d.Info, d.Count);
}
To begin with we have created a reference t to the Type object.
We have used the typeof operator on test1 class, which returns
reference to an object of Type. The GetCustomAttributes( )
method takes the type of the attribute we want to search and
returns an array of references to objects, each of type DocumentAttribute.
Next, we have run a foreach loop to read and display each
attribute in the array. In our case only one attribute would
get displayed.
To read the attributes for methods, we would first obtain
all the methods and then read attributes of each of them.
This is done through the following statements.
MethodInfo[] mi=t.GetMethods();
foreach (MethodInfo m in mi)
{
object[ ] o = m.GetCustomAttributes ( false ) ;
foreach (object ma in o)
{
DocumentAttribute d = ( DocumentAttribute ) ma ;
Console.WriteLine ( {0}, d.Info ) ;
}
}
We have used the reference t to obtain the methods and collected
them in an array of type MethodInfo. We have called the GetCustomAttributes(
) method of the MethodInfo class to read the attributes of
each method.
Similarly we can use the GetCustomAttributes( ) method of
the ConstructorInfo and FieldInfo classes to read the attributes
given to constructors and fields.
 |
Yashavant
Kanetkar, one of the first Express Computer columnists,
is an established software expert, speaker and author
with several best-sellers to his credit, including titles
like “Let Us C” and the “Fundas” series. Contact him at
kanet@nagpur.dot.net.in |
|