03 July 2006

[Article(".NET Attributes")]

There have been many articles on Attributes, so rather than go through the rigmarole  of re-hashing a subject I have will give a quick introduction. After that I have knocked up a quick demo which will hopefully get you moving in the right direction! At the end is a list of useful resources.

What are these attribute tags?
Fig 1.1 Example of Attribute defined on a method:

[Fig 1.1]

[Attribute("argument")]
private void
Foo() { }

[/Fig 1.1]

Attributes are a declarative way of defining some functionality associated with a method, types, fields or even properties. Put simply attributes allow a definition of specific meta data against code. The .NET model of compilation model is to take source code and compile it down to Intermediate Language (IL) which is then run by the Common Language Runtime (CLR). For those who have not come across IL before, IL is similar to machine code, but retains extra information about the code, making the IL a damn sight easier to read that x86 assembler!

Enough with the talk, show us some code!

Lab
For this quick introduction we are going to be looking at the conditional attribute. This attribute is defined in the System.Diagnostics namespace.

Looking at code 1.1. we see that there are two methods define in our Program class.  FillArray which populates our generic integer list with random integers. The second method called DebugPrint prints out all the items in the list with the items index number and the value that is stored. A very useful little function for debugging an integer list. In the Main method we call the DebugPrint statement twice. Before the list is sorted and afterwards. This is all fine and dandy, until we come till the time when we no longer want to release code with debug statements in, but rather a release build without.

[code 1.1]

class Program
{
    
internal static void Main(string
[] args)
   {
        
List<int> digits = new List<int
>();
            
        
//Fill array with random digits
        
FillArray(digits);

        //What have we got in the array?
        DebugPrint(digits);

        //Sort the array
        digits.Sort();

        //Is it sorted?      
        DebugPrint(digits);

        Console.ReadLine();
  }

  internal static void FillArray(List<int> digits)
    
{
       
Random rand = new Random();

       for (int i = 0; i < 10; i++)
       {
          digits.Add(rand.Next(0, 100));
       }
  }

  internal static void DebugPrint(List<int> digits)
 
{
        
Console.WriteLine("[BEGIN Debug Print]");

         for (int i = 0; i < digits.Count; i++)
         
{
              
Console.WriteLine("{0} \t=>\t {1}",i,digits[i]);
         
}

         
Console.WriteLine("[END Debug Print]\n\n");
  
}
}

[/code 1.1]

Personally coming from the school of C the obvious solution to this problem is to surround the DebugPrint method with a compiler directive (code 1.2). What a simple solution, unfortunately if the code is built the compiler will have a small problem as there are calls to this method, which in the opinion of the compiler, does not exist! To solve this problem every call to DebugPrint() needs to be surrounded by the same compiler directive.

[code 1.2]

#IF DEBUG

   internal static void DebugPrint(List<int> digits)
  
{
          
Console.WriteLine("[BEGIN Debug Print]");

         for (int i = 0; i < digits.Count; i++)
         
{
               
Console.WriteLine("{0} \t=>\t {1}",i,digits[i]);
         
}

         
Console.WriteLine("[END Debug Print]\n\n");
    
}

#ENDIF

[/code 1.2]

Far from ideal, in real terms this means remembering to surround all the calls and changing the token that is used to define debugging will require a change in all these compiler directives. From a semantic point of view it also makes our code that little bit harder tounderstand. So does a simpler solution exist? Well if there didn’t I probably wouldn’t be writing this! Cue drum rolls and enter stage right attributes. In the System.Diagnostics there is a class ConditionalAttribute. This class is explained as giving the following functionality, “Indicates to compilers that a method is callable if a specified pre-processing identifier is applied to the method.” Code 1.3 defines this class as a Attribute above the DebugPrint method. The class is passed an argument “DEBUG” which in turn is passed to the ConditionalAttribute constructor. This sets the class up such that if this compiler directive is defined in the correct scope this method is callable otherwise it is not. Note that I have not said that the method is removed, it is merely not callable. Compile this code in debug mode, and everything is fine and dandy with our lovely debug message printing. Drop it into release mode and it compiles first time with the method calls in place, and our DebugMethod is no longer called, compiler directives for free?

[code 1.3]

      [Conditional("DEBUG")]  
   internal static void DebugPrint(List<int> digits)
  
{
          
Console.WriteLine("[BEGIN Debug Print]");

         for (int i = 0; i < digits.Count; i++)
         
{
               
Console.WriteLine("{0} \t=>\t {1}",i,digits[i]);
         
}

         
Console.WriteLine("[END Debug Print]\n\n");
    
}

[/code 1.3]

Let’s try find out what the C# pre-processor is doing here. Time for ILDASM (not used ILDASM before? Click here)! Looking at Fig 1.2 (the debug build) all the methods are in the assembly as they should be. Browsing through the MSIL output of the main function (Code 1.4) the calls to DebugPrint are there. Great! Moving to the release build, open ILDASM something most peculiar has happened, the method DebugPrint is still in the assembly, uh oh. Something must have gone wrong. Before panicking too much let’s take a gander at the Main method (code 1.5) the calls to DebugPrint have been removed. Suddenly it all makes sense. The pre-processor is not removing the function from the class just the ability to call it (removes the calls to the function). “But Mr Ian Saunders”, you all cry, what happens if the function returns a number that is used in another statement, or pass the array using the out key word? For exactly these reasons the Conditional Attribute does not allow the method to return a value or take an out parameter.

[Fig 1.2]

[/Fig 1.2]

[Code 1.4]
.method assembly hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       42 (0x2a)
  .maxstack  1
  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> digits)
  IL_0000:  nop
  IL_0001:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
  IL_0006:  stloc.0
  IL_0007:  ldloc.0
  IL_0008:  call       void Attributes.Program::FillArray(class [mscorlib]                                               System.Collections.Generic.List`1<int32>)
  IL_000d:  nop
  IL_000e:  ldloc.0
  IL_000f:  call       void Attributes.Program::DebugPrint(class [mscorlib]                                               System.Collections.Generic.List`1<int32>)
  IL_0014:  nop
  IL_0015:  ldloc.0
  IL_0016:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Sort()
  IL_001b:  nop
  IL_001c:  ldloc.0
  IL_001d:  call       void Attributes.Program::DebugPrint(class [mscorlib]                                            System.Collections.Generic.List`1<int32>)
  IL_0022:  nop
  IL_0023:  call       string [mscorlib]System.Console::ReadLine()
  IL_0028:  pop
  IL_0029:  ret
} // end of method Program::Main

[/Code 1.4]

[Fig 1.3]

[/Fig 1.3]

[Code 1.5]
.method assembly hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       25 (0x19)
  .maxstack  1
  .locals init ([0] class [mscorlib]System.Collections.Generic.List`1<int32> digits)
  IL_0000:  newobj     instance void class [mscorlib]System.Collections.Generic.List`1<int32>::.ctor()
  IL_0005:  stloc.0
  IL_0006:  ldloc.0
  IL_0007:  call       void Attributes.Program::FillArray(class [mscorlib]
                                                System.Collections.Generic.List`1<int32>)
  IL_000c:  ldloc.0
  IL_000d:  callvirt   instance void class [mscorlib]System.Collections.Generic.List`1<int32>::Sort()
  IL_0012:  call       string [mscorlib]System.Console::ReadLine()
  IL_0017:  pop
  IL_0018:  ret
} // end of method Program::Main

[/Code 1.5]

Summary:

Attributes provide a powerful way of adding extra functionality to your code, in a simple seamless way, covering up the complexity and the constraints applied to the code. The example I have used is a tad contrived, though I daresay will be very useful in university projects when debug code is about 60% of what is written ;). Why stop there the .NET namespaces provides hundreds of built in attributes that can be used straight out of the box?  If the provided functionality doesn’t quite float your boat why not create your own attribute? It’s really not as complicated as you think and there is a wealth of knowledge on how to do this on the internet.

Regards,
Ian Saunders

About the resources: As there are so many attributes and all located in varying namespaces in can be difficult to know if there are any attributes that apply to the domain of your problem. In my experience it appears the best way is to crack open your favourite search engine or MSDN and search for either the namespace you are playing with or the general concept you are looking at. Hope that is helpful!

Resources:

The code I used is attached to this article.


MSDN article on Attributes: This article gives a very good and thourough explanation of attributes using VB .NET as a base language. Also provides an introduction to creating custom Attribute classes.

Thorough introduction to Attributes in VB .NET with an authortive O’Reilly Book

Attributes in VB .NET: An introduction to attributes as well as a step by step guide to creating a custom attribute class.

Definition of System.Attribute namespace from MSDN:

Using Attributes for creating .NET code that can work with COM

Attributes in Business Objects: A great article in VB .NET and C# on how to use attributes in your own custom business objects
Part 1: http://www.spaanjaars.com/QuickDocId.aspx?quickdoc=390
Part 2 :http://www.spaanjaars.com/QuickDocId.aspx?quickdoc=391

Using Attributes for debugging: Invaluable resource for customizing and refining the debugging experience in Visual Studio 2005

Another Slightly in-depth article about using Attributes to improve code debugging:

 

Comments

# Adam said:
Or, you could just use a "#else" to define a dummy DebugPrint() that does nothing when "DEBUG" is not defined.  As this is an internal function, all calls to it and the function itself can be completely optimized away by the compiler.

(Hmmm.....why is the function not optimised away in the release build above if it is never referenced and marked as internal?)
03 July 06 at 11:57 AM
# ukstudentzine said:
Good point about the dummy function. As for why the function is still included in the assembly, I am not certain will do a little investigating :)
04 July 06 at 5:20 AM
New Comments to this post are disabled
Page view tracker