Verwendung und Steuerung des Präprozessors
Kommentare

In dieser Ausgabe der C#-Kolumne geht es ausnahmsweise nicht um ein bestimmtes C#-Sprachmerkmal. Dieses Mal widmen wir uns dem C#-Compiler und dabei insbesondere dem Präprozessorvorgang. Mithilfe der definierten C#-Präprozessoranweisungen (Preprocessor Directives) ist es möglich, den Compilervorgang zu beeinflussen.

In einer der letzten C#-Kolumnen wurde die Funktionsweise von Attributen erläutert [1]. Dabei wurde u. a. auch das Attribut ConditionalAttribute vorgestellt. Mit dessen Hilfe kann festgelegt werden, dass eine Methode nur im Debug-Modus ausgeführt werden soll. Es beeinflusst somit die Programmausführung zur Laufzeit. Präprozessoranweisungen dagegen werden schon zur Kompilierzeit (Compile Time Check) ausgewertet. Es existieren unterschiedliche Präprozessoranweisungen, die bekannteste ist wahrscheinlich die #region-Direktive, um den Quellcode in zusammenhängende und/oder logische Bereiche zu gruppieren. Obwohl sie sich nicht direkt funktionell auf den Quellcode auswirkt, gehört sie zu den Präprozessoranweisungen. Verarbeitet werden diese Präprozessoranweisungen von dem so genannten namensgleichen Preprocessor, wobei der C#-Preprocessor nicht mit dem Funktionsumfang eines C/C++ – Preprocessor verglichen werden kann. Der C#-Preprocessor unterstützt zum Beispiel keine Makros. Dennoch existieren einige nützliche Präprozessoranweisungen, die in eigenen Projekten sinnvoll verwendet werden können. Präprozessoranweisungen sind im Quellcode leicht erkennbar, da sie immer mit einem HASH-Zeichen (#) beginnen. Die folgenden Abschnitte stellen einige von ihnen vor. Tabelle 1 zeigt eine vollständige Übersicht über die verfügbaren Präprozessoranweisungen [2].

#warning vs. TODO

Während der Entwicklung eines Projektes ergeben sich gelegentlich Situationen, in denen noch nicht alle Anforderungen klar sind oder benötigte Abhängigkeiten fehlen. Tritt eine solche Situation ein, kann der Code meist noch nicht vollständig implementiert werden. Um die betroffenen Codestellen später leichter wiederzufinden, sollten sie mithilfe von Kommentaren (z. B. //TODO …) markiert werden. So ist zumindest sichergestellt, dass die betroffenen Stellen zukünftig leicht aufgefunden werden können. Damit sie dann nicht in Vergessenheit geraten, wäre es allerdings wünschenswert, wenn eine Meldung auf die noch zu erledigenden Stellen hinweist. Hierfür eignet sich der Einsatz der #warning-Direktive sehr gut. Sie generiert zur Übersetzungszeit eine Warnung und gibt diese als Compilermeldung aus. So gehen offene Punkte nicht im Quellcode unter. Listing 1 zeigt, wie die Direktive verwendet werden kann.

Listing 1

#define DEBUG
using System;
namespace Preprocessor
{
  class Program
  {
    public static void UsingSymbols()
    {
      #if (DEBUG)
        #warning DEBUG ist aktiviert
      #endif
    }
  }
}
  

In Abbildung 1 ist zu sehen, dass die Warnung während des Kompilierens innerhalb des Ausgabefensters ausgegeben wird. Reichen Warnungen nicht aus, kann auch mit der #error-Direktive eine Fehlermeldung generiert werden.

Abb. 1: Ausgabe einer eigenen Warnung
Abb. 1: Ausgabe einer eigenen Warnung
#define + #if = Debug-Modus

Die Direktive #define ermöglicht die Definition eines so genannten Symbols. Die definierten Symbole können an anderer Stelle ausgewertet werden. In Kombination mit der #if-Direktive wird es so möglich, logische Verzweigungen zu erstellen. Listing 2 zeigt dazu ein einfaches Beispiel. Zunächst werden die beiden Symbole SYM_A und SYM_B definiert. Im Hauptprogramm werden diese dann innerhalb von #if-Direktiven ausgewertet. Je nachdem welche Symbole definiert wurden, erfolgt eine Verzweigung mit entsprechender Ausgabe auf der Konsole. Innerhalb einer Anwendung ist dieser Mechanismus sehr hilfreich, um bestimmte Programmabläufe nur im Testmodus (Debug-Modus) auszuführen. So kann zum Beispiel ein Symbol DEBUG definiert werden, das nur während der Entwicklungszeit gesetzt ist. Innerhalb des Quellcodes kann dann das Symbol verwendet werden, um zusätzliche Ausgaben oder Testwerte zu setzen. Per #define definierte Symbole sind allerdings immer nur in der Codedatei gültig, in der sie festgelegt wurden. Auch wenn sich Symbole teilweise wie Variablen verhalten und denselben Namen tragen können wie bereits definierte Variablen im Quellcode, können ihnen keine Werte zugewiesen werden. Symbole und Variablen sind völlig unabhängig voneinander. Wird an späterer Stelle ein definiertes Symbol nicht mehr benötigt, kann es per #undef-Direktive wieder entfernet werden. Nachfolgende Prüfungen, ob das Symbol vorhanden ist, ergeben dann ein negatives Ergebnis.

Listing 2

#define SYM_A
#define SYM_B
using System;
namespace Preprocessor
{
  class Program
  {
    public static void UsingSymbols()
    {
      #if (SYM_A && !SYM_B)
        Console.WriteLine("Symbol SYM_A ist definiert");
      #elif (!SYM_A && SYM_B)
        Console.WriteLine("Symbol SYM_B ist definiert");
      #elif (SYM_A && SYM_B)
        Console.WriteLine("Symbol SYM_A und SYM_B ist definiert");
      #else
        Console.WriteLine("Kein Symbol wurde definiert.");
      #endif
    }
    static void Main(string[] args)
    {
      UsingSymbols();
    }
  }
}
  
Warnungen ignorieren

Während der Entwicklungsphase können zahlreiche Warnungen u. U. stören und dazu verleiten, selbst wichtige bzw. neue Warnungen zu ignorieren. Aus diesem Grund ist es teilweise sinnvoll, bekannte Warnungen – die erst zu einem späteren Zeitpunkt gelöst werden – zunächst auszublenden. Dafür kann die #pragma warning-Direktive verwendet werden. Listing 3 zeigt die Verwendung dieser Direktive. Die Beispielmethode definiert die Variable i, verwendet sie aber nie. In diesem Fall meldet der Compiler die Standardmeldung „The variable ‚i‘ is declared but never used“. Um diese Meldung auszuschalten, wird die in Listing 3 gezeigte Direktive notiert. Über die Direktive #pragma warning können per Komma separiert, mehrere zu ignorierende Warnungsnummern angegeben werden. Zum Abschalten der Warnungen wird der Schalter disable verwendet. Mittels restore können die deaktivierten Warnungen wieder eingeschaltet werden.

Listing 3

public static void UsingSymbols()
    {
      #pragma warning disable 0168
      int i;
      // später wieder per restore aktivieren
      #pragma warning restore 0168
    }
  
Zeilennummern und Quellcodedatei

Die Direktive #line ermöglicht die Beeinflussung der Zeilennummer und des Dateinamens der Quellcodedatei, die für eine Meldung zur Kompilierzeit verwendet werden. Abbildung 2 zeigt ein Beispiel und die korrespondierende Meldung des Compilers. Wie zu erkennen ist, werden für die Meldungen die eigenen Zeilennummern und Dateinamen im Ausgabefenster verwendet. Diese Einstellungen werden solange verwendet, bis entweder per #line neue Werte vorgegeben werden oder per default auf die Ursprungswerte zurückgewechselt wird. Im Beispiel geschieht dies in der letzten #line-Direktive. Danach werden wieder die Standardwerte für Zeilennummer und Dateiname verwendet. Neben den hier gezeigten Optionen existiert noch eine hidden-Eigenschaft. Unter deren Verwendung ist es möglich, komplette Zeilennummern und Codebereiche auszublenden.

Abb. 2: Beeinflussung von Zeilennummern und Dateinamen
Abb. 2: Beeinflussung von Zeilennummern und Dateinamen
Zusammenfassung

Direktiven bieten die Möglichkeit, vor bzw. bei der Kompilierzeit auf das Kompilierverhalten einzuwirken. Eigene Warnungen und Fehler auszugeben, kann davor schützen, unfertige Codestellen zu vergessen. Die am häufigsten verwendete – hier aber nicht explizit erklärte Direktive – ist die #region-Direktive. Durch ihre Verwendung kann der Quellcode strukturiert und in logische Gruppen zusammengefasst werden. Auch wenn der C#-Präprozessor nicht so mächtig ist wie der C/C++-Präprozessor, bietet er einige sinnvolle Funktionen.

Tabelle 1: Übersicht über die Präprozessoranweisungen

Präprozessoranweisungen Beschreibung
#if Leitet einen Block ein, der nur unter der definierten Bedingung ausgeführt wird
#else Alternativer Block, wenn #if-Bedingung nicht zutrifft
#elif Alternative zum #if/#else-Konstrukt
#endif Beendet einen #if-Block
#define Definiert ein Symbol
#undef Entfernt das definierte Symbol
#warning Ermöglicht die Erstellung einer Warnmeldung, die zur Übersetzungszeit ausgegeben wird
#error Erstellt eine Fehlermeldung und gibt diese zur Übersetzungszeit aus
#line Ändert die Zeilennummer für die Compilerausgabe
#region Ermöglicht das logische Zusammenfassen von Codebereichen zu einem Block
#endregion Endpunkt der #region
#pragma Einleitung spezieller Anweisungen für den Compiler; derzeit werden nur die Anweisungen „warning“ und „checksum“ unterstützt, eigene Anweisungen können nicht umgesetzt werden
#pragma warning Ermöglicht das Ein- und Abschalten von Warnungen
#pragma checksum Generiert Kontrollsummen für Quellcodedateien
Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -