2009-06-01

System.Diagnostic.ConditionalAttribute

Atrybut System.Diagnostics.ConditionalAttribute można stosować do metod i klas atrybutów. Jeśli podany w System.Diagnostic.ConditionalAttribute symbol kompilacji warunkowej istnieje metoda będzie przetwarzana, zaś atrybut wyemitowany. Za pomocą tego atrybutu realizujemy kompilację warunkową. W poniższym przykładzie rozpatrzymy tylko stosowanie atrybutów kompilacji warunkowej dla metod. Jeśli idzie o atrybuty to bardzo wiele metod z przestrzeni nazw System.Diagnosic ma na metody nałożony atrybut: [Conditional("DEBUG")], który gwarantuje wywoływanie metod tylko w konfiguracji debugowania.
#define TEST1
//#define TEST2
#define TEST3
//#define TEST4

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Linq;

namespace TestApp
{
   public partial class Form1 : Form
   {
       [System.Diagnostics.Conditional("TEST1")]
       public void Test1()
       {
       }

       public int GetXX()
       {
           return System.DateTime.Now.Millisecond;
       }

       [System.Diagnostics.Conditional("TEST2")]
       public void Test2(int xx)
       {
       }

       #if TEST3
       public void Test3()
       {
       }
       #endif

       #if TEST4 && TEST3
       public void Test4()
       {
       }
       #endif

       public Form1()
       {
           InitializeComponent();

           Test1();

           if (GetXX() == 56)
               Test2(GetXX());

           Test3();

           System.Diagnostics.Debug.Close();

           #if TEST4
           Test4();
           #endif
       }
   }
}
Wyemitowany kod pośredni:
public class Form1 : Form
{
   // Fields
   private IContainer components;

   // Methods
   public Form1()
   {
       this.InitializeComponent();
       this.Test1();
       this.GetXX();
       this.Test3();
   }

   protected override void Dispose(bool disposing)
   {
       if (disposing && (this.components != null))
       {
           this.components.Dispose();
       }
       base.Dispose(disposing);
   }

   public int GetXX()
   {
       return DateTime.Now.Millisecond;
   }

   private void InitializeComponent()
   {
       base.SuspendLayout();
       base.AutoScaleDimensions = new SizeF(6f, 13f);
       base.AutoScaleMode = AutoScaleMode.Font;
       base.ClientSize = new Size(0x1d9, 0x127);
       base.Name = "Form1";
       this.Text = "Form1";
       base.ResumeLayout(false);
   }

   [Conditional("TEST1")]
   public void Test1()
   {
   }

   [Conditional("TEST2")]
   public void Test2(int xx)
   {
   }

   public void Test3()
   {
   }
}
W przykładzie tym możemy zaobserwować zachowanie kompilacji warunkowej zrealizowanej przy pomocy atrybutów i przy pomocy komend preprocesora. Zgodnie z oczekiwaniem do kodu pośredniego emitowane są tylko wywołania metod Test1() i Test3(). Choć metoda Test2() nie jest wywoływana to jej ciało jest emitowane. W przypadku komend preprocesora cały kod w #if ... #endif jest przez preprocesor wycinany przed przystąpieniem do kompilacji. Na podstawie atrybutu warunkowego nie możemy zdecydować o tym czy danej metody nie emitować. W innym pliku bowiem może znaleźć się definicja #define TEST2, co spowoduje, że dla tamtejszych metod Test2() zostanie wyemitowane ich wywołanie. Atrybut warunkowy można nałożyć tylko na metody, które zwracają nie zwracają wyniku. Gdyby nie ograniczenie kompilator miałby np. problem z kompilacja wyrażenia matematycznego, którego jeden element jest metodą warunkową. No i jeszcze jedna sprawa. Porównajmy sobie dwie takie definicje:
[Serializable]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
[ComVisible(true)]
public sealed class ConditionalAttribute : Attribute

[Serializable]
[ComVisible(true)]
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method, Inherited = false)]
public sealed class DebuggerStepThroughAttribute : Attribute
Trochę się różnią, ale pytanie brzmi gdzie tu jest ograniczenie nie pozwalające nałożyć atrybut warunkowy na metodę, która coś zwraca. I po drugie gdzie tu jest powiedziane, że atrybut warunkowy nie możemy nałożyć na klasę, tylko na inne atrybuty warunkowe ? Które podejście jest lepsze. Wadą atrybutu warunkowego jest to, że kod metody pomimo tego, że nie jest używana, zostanie wyemitowany. Jeśli naszą intencją jest dostarczenie okrojonej wersji programu i dodatkowo chcemy ukryć działanie pewnych metod to to podejście nam nie zapewni. Jeśli wywołanie metody ma zależeć od kombinacji logicznej kilku atrybutów to tylko komendy preprocesora dają nam taką możliwości (za pomocą atrybutów warunkowych możemy co najwyżej zrealizować warunek logicznego i). Jeśli chcemy dostarczyć innego zachowania metody dla zdefiniowanego symbolu, a innego dla jego braku (typowe DEBUG i RELEASE) to atrybut warunkowy nam tego nieumożliwi. Za pomocą atrybutu warunkowego nie możemy nałożyć warunku tylko na część metody. Wadą komend preprocesora jest to, że muszą być ona nakładane zarówno na definicję metody jak i jej wywołania. Jeśli metoda jest wywoływana setki razy, każde jej wywołanie musi być ujęte w stosowny blok preprocesora, a każda modyfikacja warunku #if wymaga bardzo wielu zmian. Kompilacja warunkowa zrealizowana za pomocą komend preprocesora nie nadaje się najlepiej do wywołania warunkowej metody z jednego modułu w innym module, kiedy moduł ten nie mamy w postaci źródeł, ale w postaci skompilowanej. Po skompilowaniu taka metoda albo jest albo jej nie ma. W przypadku kompilacji warunkowej jest zawsze, a dodatkowo zachowany jest atrybut kompilacji warunkowej. Dzięki czemu definiując symbol w tym atrybucie sprawiamy, że metoda jest wywoływana. Innymi słowy atrybut warunkowy pozostawia po sobie pewną infrastrukturę (informację) o możliwości warunkowego wykorzystania kodu w danym module. Powiedzmy sobie od razu, że C# to nie jest C++, gdzie skomplikowane #if zdarzają się bardzo często zwłaszcza jeśli chodzi o projekty wieloplatformowe. Java wogóle nie ma żadnych metod kompilacji warunkowej, tam takie sprawy załatwia się zdefiniowaniem zmiennej finalnej, a metody warunkowe wywołuje się poprzez if na tej zmiennej. Kompilator, jeśli warunek jest fałszywy nie emituje wtedy całego bloku if. W przypadku Javy jeśli mamy intensywne logowanie połączone z skomplikowanym formatowaniem stringów, sam if we wnętrzu metody logującej nie wystarczy. Co prawdą logowanie nie nastąpi ale możemy mnóstwo czasu stracić na formatowaniu stringu. Tej wady pozbawiony jest atrybut warunkowy w C#. Metoda nie tylko zostanie nie wywołana, ale także jej parametry nie będą przetwarzane. Widzimy to na przykładzie metody Test2(). Dodatkowo warto zwrócić uwagę, że warunek logiczny zostanie wyemitowany. Wspomniana metoda kompilacji warunkowej zastosowana w Javie także zadziała w C#. Zauważmy, że kompilacja warunkowa jest domeną kompilatora. W kodzie pośredniego nie ma po niej śladu. Jedyne co pozostaje to atrybuty warunkowe, dzięki czemu inne metody mogą skorzystać z warunkowego wywołania tamtych metod. Przy czym znowu atrybuty te są używane przez kompilator emitujący kod pośredni. Osobiście uważam, że dopóki naprawdę nie jest nam potrzebne #if ... #endif powinniśmy korzystać z atrybutów warunkowych. Warto też zauważyć chyba oczywistą rzecz, że w bloku #if ... #endif jeśli warunek jest nie spełniony może się znajdować dowolny tekst. Metoda oznaczona atrybutem warunkowym, nawet jeśli nie zostanie wywołana, ciągle musi mieć poprawnie zdefiniowane parametry. Innymi słowy wywołanie tej metody musi się kompilować.

Brak komentarzy:

Prześlij komentarz