Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


C# von Eric Gunnerson
Die neue Sprache für Microsofts .NET-Plattform
C# - Zum Katalog
gp Kapitel 32 C# im Detail
  gp 32.1 C#-Stil
  gp 32.2 Richtlinien für Bibliotheksautoren
  gp 32.3 Benennung von Klassen
  gp 32.4 Unsicherer Code
  gp 32.5 XML-Dokumentation
  gp 32.6 Speicherbereinigung in der .NET-Laufzeitumgebung
  gp 32.7 Weitergehende Reflektion
  gp 32.8 Optimierung

Kapitel 32 C# im Detail

In diesem Kapitel sollen einige Themenbereiche von C# ausführlicher besprochen werden. Es werden Bereiche abgedeckt, die für Autoren von Bibliotheken/Frameworks von Interesse sind, z. B. Stilrichtlinien und XML-Dokumentation, und es wird das Schreiben von unsicherem Code sowie die Funktionsweise der Speicherbereinigung der .NET-Laufzeitumgebung behandelt.


Galileo Computing

32.1 C#-Stil  downtop

Die meisten Sprachen verfügen über erwartete Ausdrücke. C#-Textzeichenfolgen verwenden beispielsweise eher eine Zeigerarithmetik als Arrayverweise. C# ist noch nicht lange genug erhältlich, als dass die Programmierer bereits umfassende Erfahrung in diesem Bereich besitzen könnten, aber es gibt einige Richtlinien die .NET Common Language Runtime betreffend, die berücksichtigt werden sollten.

Diese Leitsätze werden in der .NET-Dokumentation unter den Entwurfsrichtlinien für Klassenbibliotheken ausgeführt und sind besonders für Autoren von Frameworks oder Bibliotheken von Bedeutung.

Die Beispiele in diesem Buch sind mit diesen Richtlinien konform, daher sollten sie Ihnen bereits ein wenig vertraut vorkommen. Die .NET-SDK enthält hierzu viele weitere Beispiele.


Galileo Computing

32.1.1 Benennung  downtop

Es gibt zwei Benennungskonventionen.

gp  Über PascalCasing wird der erste Buchstabe des ersten Wortes als Großbuchstabe formatiert.
gp  camelCasing ist dasselbe wie PascalCasing, abgesehen davon, dass der erste Buchstabe des ersten Wortes nicht in einen Großbuchstaben umgewandelt wird.

Im Allgemeinen wird PascalCasing für Elemente verwendet, die außerhalb einer Klasse, Aufzählung, Methode usw. offen gelegt werden. Eine Ausnahme hierbei bildet der Methodenparameter, der unter Verwendung von camelCasing definiert wird.

Private Mitglieder von Klassen, z. B. Felder, werden anhand von camelCasing definiert.

Für die Benennung gelten einige weitere Konventionen:

gp  Vermeiden Sie bei der Benennung übliche Schlüsselwörter, um das Risiko einer Kollision in anderen Sprachen zu minimieren.
gp  Ereignisklassen sollten auf EventArg enden.
gp  Ausnahmeklassen sollten auf Exception enden.
gp  Schnittstellen sollten mit I beginnen.

Attributklassen sollten auf Attribute enden.


Galileo Computing

32.1.2 Kapselung  downtop

Üblicherweise sollten Klassen so weit wie möglich gekapselt werden. Mit anderen Worten, eine Klasse sollte möglichst wenige Details zum internen Aufbau offen legen.

In der Praxis bedeutet dies, eher Eigenschaften als Felder zu verwenden, um spätere Änderungen zu ermöglichen.


Galileo Computing

32.2 Richtlinien für Bibliotheksautoren  downtop

Die folgenden Richtlinien richten sich vor allem an Programmierer, die Bibliotheken für eine Verwendung durch Dritte schreiben.


Galileo Computing

32.2.1 CLS-Kompatibilität  downtop

Beim Schreiben von Softwarekomponenten, die von anderen Entwicklern eingesetzt werden, ist es sinnvoll, eine CLS-Kompatibilität (Common Language Specification) zu gewährleisten. Diese Spezifikationen beschreiben, welche Funktionen eine Sprache unterstützen muss, um als .NET-fähige Sprache eingestuft zu werden. Sie finden diese Spezifikationen im Abschnitt »What ist the Common Language Specification« der .NET-SDK-Dokumentation.

Der C#-Compiler überprüft den Code auf eine Übereinstimmung mit den CLS, wenn in einer der Quelldateien das ClsCompliant-Assemblierungsattribut gesetzt wurde.

Für die Übereinstimmung mit den CLS gelten folgende Einschränkungen:

gp  Typen ohne Vorzeichen können nicht als Teil der öffentlichen Schnittstelle einer Klasse offen gelegt werden. Sie können im privaten Bestandteil einer Klasse uneingeschränkt verwendet werden.
gp  Unsichere (z. B. Zeiger) Typen können in der öffentlichen Schnittstelle einer Klasse nicht eingesetzt werden. Wie bei den Typen ohne Vorzeichen ist auch hier eine uneingeschränkte Verwendung in den privaten Abschnitten der Klasse möglich.
gp  Bezeichner (beispielsweise Klassen- oder Mitgliedsnamen) dürfen sich nicht nur bezüglich der Groß-/Kleinschreibung unterscheiden.

Die Kompilierung des folgenden Codeabschnitts führt beispielsweise zu einem Fehler:

// Fehler
using System;

[CLSCompliant(true)]

class Test
{
    public uint Process() {return(0);}
}

Galileo Computing

32.3 Benennung von Klassen  downtop

Zur Vermeidung von Kollisionen zwischen Namespaces und Klassen, die von anderen Unternehmen bereitgestellt werden, sollten Namespaces anhand der CompanyName.TechnologyName-Konvention benannt werden. Der vollständige Name einer Klasse oder eines Steuerelements eines Röntgenlasers würde beispielsweise folgendermaßen lauten:

AppliedEnergy.XRayLaser.Controller

Galileo Computing

32.4 Unsicherer Code  downtop

Die Codeprüfung in der .NET-Laufzeitumgebung hat viele Vorteile. Die Möglichkeit zur Prüfung der Typensicherheit eines Codes ermöglicht nicht nur Downloadszenarien, sondern verhindert auch häufig auftretende Programmierfehler.

Bei der Handhabung binärer Strukturen, der Kommunikation mit COM-Objekten, die Strukturen mit Zeigern enthalten, oder in Situationen, in denen die Leistung eine wichtige Rolle spielt, ist eine bessere Steuerung gefordert. In diesen Fällen kann unsicherer Code verwendet werden.

Unsicher bedeutet hierbei, dass die Laufzeitumgebung nicht überprüfen kann, ob die Codeausführung sicher ist. Eine Ausführung des Codes kann nur erfolgen, wenn die Assemblierung volles Vertrauen genießt. Dies bedeutet, dass ein Einsatz in Downloadszenarien nicht möglich ist und der Missbrauch von unsicherem Code verhindert wird.

Nachfolgend ein Beispiel zur Verwendung von unsicherem Code zum schnellen Kopieren von Strukturarrays. Bei der kopierten Struktur handelt es sich um eine Punktstruktur aus x- und y-Werten.

Es sind drei Versionen der Funktion vorhanden, mit denen Punktarrays geklont werden. ClonePointArray() ist ohne unsichere Funktionen geschrieben und kopiert lediglich die Arrayeinträge. Die zweite Version, ClonePointArrayUnsafe(), verwendet Zeiger, um den Speicher zu durchlaufen und den Kopiervorgang durchzuführen. Die letzte Version, ClonePointArrayMemcpy(), ruft die Systemfunktion CopyMemory() zur Ausführung des Kopiervorgangs auf.

Um einen gewissen Zeitvergleich zu erhalten, wird folgender Code eingesetzt:

using System;
using System.Diagnostics;

public struct Point
{
    public Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

        // sichere Version
    public static Point[] ClonePointArray(Point[] a)
    {
        Point[] ret = new Point[a.Length];

        for (int index = 0; index < a.Length; index++)
            ret[index] = a[index];

        return(ret);
    }

        // unsichere Version mit Zeigerarithmetik
    unsafe public static Point[] ClonePointArrayUnsafe(Point[] a)
    {
        Point[] ret = new Point[a.Length];

            // a und ret sind fest und können durch die
            // Speicherbereinigung im festen Block nicht verschoben 
werden
        fixed (Point* src = a, dest = ret)
        {
            Point*    pSrc = src;
            Point*    pDest = dest;
            for (int index = 0; index < a.Length; index++)
            {
                *pDest = *pSrc;
                pSrc++;
                pDest++;
            }
        }

        return(ret);
    }
        // CopyMemory aus kernel32 importieren
    [sysimport(dll = "kernel32.dll")]
    unsafe public static extern void
    CopyMemory(void* dest, void* src, int length);

    // unsichere Version ruft CopyMemory() auf
    unsafe public static Point[] ClonePointArrayMemcpy(Point[] a)
    {
        Point[] ret = new Point[a.Length];

        fixed (Point* src = a, dest = ret)
        {
            CopyMemory(dest, src, a.Length * sizeof(Point));
        }

        return(ret);
    }

    public override string ToString()
    {
        return(String.Format("({0}, {1})", x, y));
    }

    int x;
    int y;
}

class Test
{
    const int iterations = 20000;    // # Durchläufe zum Kopieren
    const int points = 1000;        // # Punkte im Array
    const int retryCount = 5;        // # Neuversuche

    public delegate Point[] CloneFunction(Point[] a);

    public static void TimeFunction(Point[] arr,
        CloneFunction func, string label)
    {
        Point[]    arrCopy = null;
        long start;
        long delta;
        double min = 5000.0d;    // große Zahl;

           // Kopiervorgang retryCount Mal wiederholen, schnellste Zeit 
ermitteln
        for (int retry = 0; retry < retryCount; retry++)
        {
            start = Counter.Value;
            for (int iterate = 0; iterate < iterations; iterate++)
                arrCopy = func(arr);
            delta = Counter.Value – start;
            double result = (double) delta / Counter.Frequency;
            if (result < min)
                min = result;
        }
        Console.WriteLine("{0}: {1:F3} seconds", label, min);
    }

    public static void Main()
    {
        Console.WriteLine("Points, Iterations: {0} {1}", points, iterations);
        Point[] arr = new Point[points];
        for (int index = 0; index < points; index++)
            arr[index] = new Point(3, 5);

        TimeFunction(arr,
            new CloneFunction(Point.ClonePointArrayMemcpy), "Memcpy");
        TimeFunction(arr,
            new CloneFunction(Point.ClonePointArrayUnsafe), "Unsafe");
        TimeFunction(arr,
            new CloneFunction(Point.ClonePointArray), "Baseline");
    }
}

Die Zeitgeberfunktion verwendet eine Zuweisung (delegate) zur Beschreibung der Klonfunktion, damit diese eine beliebige der Klonfunktionen einsetzen kann. Sie verwendet die Counter-Klasse, die Zugriff auf die Systemzeitgeber bietet. Die Häufigkeitsrate – und Genauigkeit – dieser Klasse richtet sich nach der verwendeten Windows-Version.

Wie bei jedem Leistungsvergleich ist der anfängliche Speicherstatus sehr wichtig. Daher durchläuft TimeFunction() jede Methode fünf Mal und gibt nur den Durchlauf mit der kürzeste Zeitdauer aus. Typischerweise ist der erste Durchlauf langsamer, da der CPU-Cache noch nicht bereit ist. Für Interessierte: Diese Zeiten wurden auf einem 500 MHz-Pentium-Rechner mit Windows 2000 Professional erzielt, wurden jedoch mit der Vorbetasoftware ausgeführt, daher sollte die Leistung nicht unbedingt die Leistungsergebnisse des Endprodukts widerspiegeln.

Das ProgrammS wurde mit verschiedenen Werten für points und iterations ausgeführt. Die Ergebnisse werden nachstehend zusammengefasst:

Methode p=10, i=2.000.000 p=1.000, i=20.000 p=100.000, i=200
Baseline 1.562 0.963 3.459
Unsafe 1.486 1.111 3.441
Memcpy 2.028 1.121 2.703

Bei kleinen Arrays ist der unsichere Code am schnellsten, für sehr umfangreiche Arrays erzielte der Systemaufruf die besten Ergebnisse. Der Systemaufruf ist weniger gut für kleine Arrays geeignet, da beim Aufruf der systemeigenen Funktion Overhead entsteht. Interessant ist, dass der unsichere Code nicht als klarer Sieger gegenüber dem Baseline-Code aus dem Vergleich hervorgeht.

Was lernen wir daraus? Der Einsatz von unsicherem Code kann nicht mit schnellerem Code gleichgesetzt werden, und es ist wichtig, im Rahmen der Leistungsoptimierung einen Vergleichstest durchzuführen.


Galileo Computing

32.4.1 Strukturlayout  downtop

Die Laufzeit ermöglicht einer Struktur über das Attribut StructLayout, das Layout der zugehörigen Datenmitglieder festzulegen. Standardmäßig wird das Layout einer Struktur automatisch bestimmt, d. h., die Felder können von der Laufzeit beliebig angeordnet werden. Bei der Verwendung von interop zum Aufruf von systemeigenem oder COM-Code ist möglicherweise eine weitergehende Steuerung erforderlich.

Beim Setzen des StructLayout-Attributs können über den Aufzählungsbezeichner LayoutKind drei Layouttypen angegeben werden:

gp  Sequential – alle Felder werden in der Deklarationsreihenfolge verwendet. Beim sequenziellen Layout kann mit der Eigenschaft Pack die Packart bestimmt werden.
gp  Explicit – jedes Feld verfügt über ein festgelegtes Offset. Beim expliziten Layout muss das StructOffset-Attribut für jedes Mitglied verwendet werden, um das Elementoffset in Bytes anzugeben.
gp  Union – alle Mitglieder werden Offset 0 zugeordnet.

Zusätzlich kann die Eigenschaft CharSet gesetzt werden, um für die Zeichenfolgendatenmitglieder das Standardmarshalling festzulegen.


Galileo Computing

32.5 XML-Dokumentation  downtop

Die Dokumentation stets auf dem neuesten Stand der aktuellen Implementierung zu halten, stellt immer eine Herausforderung dar. Eine Möglichkeit hierbei ist die, die Dokumentation als Bestandteil des Quellcodes zu verwalten und anschließend in eine separate Datei zu extrahieren.

C# unterstützt ein XML-basiertes Dokumentationsformat. Mit diesem kann der XML-Aufbau geprüft und eine kontextbasierte Validierung ausgeführt werden, es können compilerspezifische Informationen eingefügt werden (z. B. vollqualifizierte Namen für Mitglieder und Parameter); anschließend werden die Daten in eine separate Datei geschrieben.

Die XML-Unterstützung von C# kann in zwei Bereiche gegliedert werden, die Compilerunterstützung und die Dokumentationskonventionen. Zur Compilerunterstützung gehören Tags, die durch den Compiler auf besondere Weise verarbeitet werden, entweder für die Überprüfung von Inhalten oder für die Symbolsuche. Die übrigen Tags definieren die .NET-Dokumentationskonventionen und werden unverändert durch den Compiler übergeben.


Galileo Computing

32.5.1 Compilerunterstützungstags  downtop

Die Compilerunterstützungstags sind ein gutes Beispiel für die Zaubertricks eines Compilers; sie werden mit Hilfe von Informationen verarbeitet, die nur dem Compiler bekannt sind. Das folgende Beispiel veranschaulicht die Verwendung der Unterstützungstags:

// Datei: employee.cs
using System;
namespace Payroll
{

/// <summary>
/// Die Employee-Klasse enthält Daten zu einem Mitarbeiter.
/// Diese Klasse enthält eine <see cref="String">string</see>
/// </summary>
public class Employee
{
    /// <summary>
    /// Erstellungsroutine für eine Employee-Instanz. Beachten 
Sie, dass
    /// <paramref name="name">name2</paramref> eine Zeichenfolge 
ist.
    /// </summary>
    /// <param name="id">Mitarbeiter-ID</param>
    /// <param name="name">Mitarbeitername</param>
    public Employee(int id, string name)
    {
        this.id = id;
        this.name = name;
    }

    /// <summary>
    /// Parameterlose Erstellungsroutine für eine Employee-Instanz.
    /// </summary>
    /// <remarks>
    /// <seealso cref="Employee(int, string)">Employee(int, string)</seealso>
    /// </remarks>
    public Employee()
    {
        id = -1;
        name = null;
    }
    int id;
    string name;
}
}

Der Compiler führt eine spezielle Verarbeitung der vier Dokumentationstags durch. Bei den Tags param und paramref wird geprüft, ob es sich bei dem Namen innerhalb des Tags um einen Parameternamen für die Funktion handelt.

Bei den Tags see und seealso wird unter Verwendung der Regeln für die Bezeichnersuche nach dem im cref-Attribut übergebenen Namen gesucht, damit der Name in einen vollqualifizierten Namen aufgelöst werden kann. Anschließend wird vor dem Namen ein Code eingefügt, um zu kennzeichnen, auf was sich der Name bezieht. Beispiel:

<see cref="String">

wird zu

<see cref="T:System.String">

String wird in die Klasse System.String aufgelöst, T: bedeutet, dass es sich um einen Typ handelt.

Das seealso-Tag wird ähnlich gehandhabt:

<seealso cref="Employee(int, string)">

wird zu

<seealso cref="M:Payroll.Employee.#ctor(System.Int32,System.String)">

Der Verweis bezieht sich auf eine Erstellungsmethode, die als ersten Parameter einen int-, als zweiten Parameter einen string-Wert aufweist.

Zusätzlich zur vorstehenden Übersetzung legt der Compiler die XML-Informationen zu jedem Codeelement in einem member-Tag ab, das den Mitgliedsnamen unter Verwendung der gleichen Codierung angibt. Dies ermöglicht einem Nachverarbeitungstool das einfache Abgleichen der Mitglieder und der Verweise auf Mitglieder.

Die aus dem vorstehenden Beispiel generierte XML-Datei lautet folgendermaßen (mit einigen Wortumbrüchen):

<?xml version="1.0"?>
<doc>
    <assembly>
        <name>employee</name>
    </assembly>
    <members>
        <member name="T:Payroll.Employee">
            <summary>
            /// Die Employee-Klasse enthält 
Daten zu einem Mitarbeiter.
            Diese Klasse enthält eine <see cref="T:System.String">string</see>

            </summary>
        </member>
        <member name="M:Payroll.Employee.#ctor(System.Int32,System.String)">
            <summary>
            /// Erstellungsroutine für eine Employee-Instanz. Beachten 
Sie, dass
            /// <paramref name="name2">name</paramref> eine 
Zeichenfolge ist.
            </summary>
            <param name="id">Mitarbeiter-ID</param>
            <param name="name">Mitarbeitername</param>
        </member>
        <member name="M:Payroll.Employee.#ctor">
            <summary>
            /// Parameterlose Erstellungsroutine für eine Employee-Instanz.
            </summary>
            <remarks>
            <seealso cref="M:Payroll.Employee.#ctor(System.Int32,System.String)"
>Employee(int, string)</seealso>
            </remarks>
        </member>
    </members>
</doc>

Die Nachverarbeitung einer Datei kann einfach sein; es kann eine XSL-Datei (Extensible Style Language) mit Festlegungen zum XLM-Rendering hinzugefügt werden. Dies führt in einem Browser mit XSL-Unterstützung zu einer Anzeige wie in Abbildung 32.1.

Abbildung 32.1  XML-Datei im Internet Explorer, Formatierung festgelegt durch eine XSL-Datei
Abbildung


Galileo Computing

32.5.2 XML-Dokumentationstags  downtop

Die übrigen XML-Dokumentationstags beschreiben die .NET-Dokumentationskonventionen. Sie können für ein bestimmtes Projekt erweitert, bearbeitet oder ignoriert werden.

Tag Beschreibung
<Summary> Eine kurze Beschreibung des Elements
<Remarks> Eine ausführliche Beschreibung eines Elements
<c> Formatzeichen als Code innerhalb eines anderen Textes
<code> Mehrzeiliger Codeabschnitt – üblicherweise in einem <example>-Abschnitt
<example> Ein Beispiel zur Verwendung einer Klasse oder Methode
<exception> Die von einer Klasse ausgegebenen Ausnahmen
<list> Eine Liste von Elementen
<param> Beschreibt einen Parameter für eine Mitgliedsfunktion
<paramref> Ein Verweis auf einen Parameter in einem anderen Text
<permission> Die auf ein Mitglied angewendete Berechtigung
<returns> Der Rückgabewert einer Funktion
<see cref="member"> Eine Verknüpfung zu einem Mitglied oder Feld in der aktuellen Kompilierungsumgebung
<seealso cref="member"> Eine Verknüpfung im »Siehe auch«-Abschnitt der Dokumentation.
<value> Beschreibt den Wert einer Eigenschaft.


Galileo Computing

32.6 Speicherbereinigung in der .NET-Laufzeitumgebung  downtop

Die Speicherbereinigung hat in einigen Gebieten der Softwarewelt einen schlechten Ruf. Einige Programmierer sind der Meinung, dass eine selbst durchgeführte Speicherzuordnung in jedem Fall besser ist als eine automatische Speicherbereinigung.

Es stimmt, eine selbst durchgeführte Speicherbereinigung ist häufig besser, jedoch nur mit einer benutzerdefinierten Zuordnungsroutine für jedes Programm und wahrscheinlich auch für jede Klasse. Darüber hinaus erfordern benutzerdefinierte Zuordnungsroutinen eine Menge Schreibaufwand und müssen verwaltet werden.

In sehr vielen Fällen erzielt eine gut austarierte Speicherbereinigung gegenüber einer nicht verwalteten Heapzuordnungsroutine ähnliche oder bessere Ergebnisse.

In diesem Abschnitt sollen die Funktionsweise der Speicherbereinigung und deren Steuerung erläutert werden, und Sie erfahren, welche Steuerungsmöglichkeiten Ihnen in einer Welt mit automatischer Speicherbereinigung zur Verfügung stehen. Die hier bereitgestellten Informationen beziehen sich auf die PC-Plattform. Systeme mit eingeschränkteren Ressourcen weisen häufig einfachere Speicherbereinigungssysteme auf.

Beachten Sie des Weiteren, dass für Mehrprozessorsysteme und Servercomputer Optimierungen durchgeführt werden.


Galileo Computing

32.6.1 Zuordnung  downtop

Die Heapzuordnung in der .NET-Laufzeitwelt ist sehr schnell; das System muss lediglich sicherstellen, dass für das angeforderte Objekt im verwalteten Heap genügend Platz vorhanden ist, einen Zeiger auf diesen Speicherort zurückgeben und den Zeiger an das Objektende verschieben.

Die Speicherbereinigung tauscht Einfachheit zur Zuordnungszeit gegen Komplexität zur Bereinigungszeit. Zuordnungen sind in den meisten Fällen sehr schnell – auch, wenn nicht genügend Speicherplatz für die Zuordnung vorhanden ist und eine Speicherbereinigung zur Freigabe von Speicherplatz für die Objektzuordnung erforderlich ist.

Um sicherzustellen, dass genügend Speicherplatz vorhanden ist, muss das System natürlich unter Umständen eine Speicherbereinigung durchführen.

Zur Leistungsoptimierung werden große Objekte (>20K) von einem Heap für große Objekte aus zugeordnet.


Galileo Computing

32.6.2 Kennzeichnen und Komprimieren  downtop

Die .NET-Speicherbereinigung verwendet einen so genannten »Kennzeichnen und Komprimieren«-Algorithmus. Bei Durchführung einer Bereinigung startet die Speicherbereinigung an den Stammobjekten (einschließlich der globalen, statischen, lokalen und CPU-Register) und ermittelt alle Objekte, die von diesen Stammobjekten (Rootobjekten) referenziert werden. Über diesen Vorgang werden die derzeit in Verwendung befindlichen Objekte ermittelt, d. h., alle weiteren Objekte im System werden nicht länger benötigt.

Zum Abschließen der Bereinigung werden alle referenzierten Objekte in den verwalteten Heap kopiert, und die Zeiger auf die Objekte werden aktualisiert. Anschließend wird der Zeiger für den nächsten verfügbaren Speicherort an das Ende des referenzierten Objekts verschoben.

Da durch Speicherbereinigung Objekte und Objektverweise verschoben werden, dürfen im System keine weiteren Operationen durchgeführt werden. Mit anderen Worten, während der Speicherbereinigung müssen alle weiteren Prozesse gestoppt werden.


Galileo Computing

32.6.3 Generationen  downtop

Es ist aufwendig, alle Objekte zu durchlaufen, die derzeit referenziert werden. Ein Großteil dieser Arbeit ist verlorene Liebesmüh, denn je älter ein Objekt ist, desto wahrscheinlicher ist es, dass der Verweis erhalten bleibt. Umgekehrt liegt die Wahrscheinlichkeit, dass der Verweis auf ein neueres Objekt aufgehoben wird, sehr hoch.

Das Laufzeitsystem macht sich dieses Verhalten durch eine Generationenimplementierung in der Speicherbereinigung zunutze. Die Objekte im Heap werden drei Generationen zugeordnet:

Bei Objekten der Generation 0 handelt es sich um neu zugeordnete Objekte, die nicht für die Bereinigung in Frage kommen. Objekte der Generation 1 haben eine Speicherbereinigung überstanden, Objekte der Generation 2 haben bereits mehrere Speicherbereinigungen überstanden. Im Hinblick auf den Entwurf ausgedrückt enthält Generation 2 tendenziell langlebige Objekte (z. B. Anwendungen), Generation 1 enthält Objekte mittlerer Lebensdauer (etwa Formulare oder Listen), Generation 0 enthält eher kurzlebige Objekte, beispielsweise lokale Variablen.

Wenn das Laufzeitsystem eine Bereinigung durchführen muss, wird zunächst eine Bereinigung von Generation 0 durchgeführt. Diese Generation enthält den größten Prozentsatz nicht referenzierter Objekte und liefert daher den meisten Speicherplatz bei kleinstem Aufwand. Wenn durch die Bereinigung dieser Generation nicht genügend Speicher freigegeben wird, wird Generation 1 bereinigt und im Anschluss daran ggf. eine Bereinigung von Generation 2 durchgeführt.

Abbildung 32.2 zeigt einige dem Heap zugeordnete Objekte, bevor eine Speicherbereinigung ausgeführt wird. Das numerische Suffix gibt die Objektgeneration an; anfänglich gehören alle Objekte der Generation 0 an. Aktive Objekte sind nur diejenigen, die sich im Heap befinden, auch wenn weitere Objekte zugeordnet werden könnten.

Abbildung 32.2  Anfänglicher Speicherstatus vor einer Speicherbereinigung
Abbildung

Zum Zeitpunkt der ersten Speicherbereinigung befinden sich nur noch die Objekte B und D in Verwendung. Abbildung 32.3 zeigt den Heapspeicher nach der Bereinigung.

Abbildung 32.3  Speicherstatus nach der ersten Speicherbereinigung
Abbildung

Da B und D eine Bereinigung überstanden haben, wird ihre Generationenkennung auf 1 erhöht. Anschließend werden neue Objekte zugeordnet, wie in Abbildung 32.4 dargestellt.

Abbildung 32.4  Zuordnung neuer Objekte
Abbildung

Es vergeht einige Zeit. Bei der nächsten Speicherbereinigung handelt es sich bei D, G und H um aktive Objekte. Es wird eine Speicherbereinigung für Generation 0 durchgeführt, die zum in Abbildung 32.5 gezeigten Speicherstatus führt.

Abbildung 32.5  Speicherstatus nach einer Speicherbereinigung für Generation 0
Abbildung

Obwohl Objekt B nicht länger aktiv ist, wird es nicht bereinigt, da die Bereinigung nur für Objekte der Generation 0 erfolgte. Nach der Zuordnung einiger weiterer Objekte sieht der Heapspeicher wie in Abbildung 32.6 dargestellt aus.

Abbildung 32.6  Weitere Zuordnung neuer Objekte
Abbildung

Wieder vergeht einige Zeit, und die aktiven Objekte lauten nun D, G und L. Die nächste Speicherbereinigung erfolgt für die Generationen 0 und 1 und führt zum in Abbildung 32.7 dargestellten Speicherstatus.

Abbildung 32.7  Speicherstatus nach einer Speicherbereinigung für die Generationen 0 und 1
Abbildung


Galileo Computing

32.6.4 Finalisierung  downtop

Die Speicherbereinigung unterstützt ein Konzept, das als Finalisierung bezeichnet wird und in gewisser Form den Zerstörungsroutinen in C++ ähnelt. In C# werden diese ebenfalls als Zerstörungsroutinen bezeichnet und mit der gleichen Syntax wie C++-Destruktoren deklariert, von der Laufzeitperspektive aus werden sie als Finalisierungsroutinen bezeichnet.

Finalisierungsroutinen ermöglichen die Ausführung einiger »Aufräumarbeiten«, bevor ein Objekt bereinigt wird. Für diese Routinen gelten jedoch erhebliche Einschränkungen, sie sollten daher nicht häufig eingesetzt werden.

Bevor wir zu diesen Einschränkungen kommen, soll die Funktionsweise der Finalisierungsroutinen erläutert werden. Wenn ein Objekt mit einer Finalisierungsroutine zugeordnet wird, fügt das Laufzeitsystem dem Objektverweis eine Liste mit Objekten hinzu, die eine Finalisierung benötigen. Wenn die Speicherbereinigung erfolgt und ein Objekt keine Verweise enthält, jedoch in der Finalisierungsliste aufgeführt ist, wird sie für die Finalisierung gekennzeichnet.

Nach Abschluss der Speicherbereinigung wird der Thread für die Finalisierung aktiviert; er ruft die Finalisierungsroutinen für alle Objekte auf, die für die Finalisierung gekennzeichnet wurden. Nachdem die Finalisierungsroutine für ein Objekt aufgerufen wurde, wird das Objekt aus der Finalisierungsliste entfernt und wird damit für die nächste Speicherbereinigung freigegeben.

Dieses Schema führt im Hinblick auf die Finalisierungsroutinen zu folgenden Einschränkungen:

gp  Objekte mit Finalisierungsroutinen erzeugen einen größeren Systemoverhead und verbleiben länger im System.
gp  Die Finalisierung erfolgt über einen separaten Thread.
gp  Für die Finalisierung besteht keine garantierte Ausführungsreihenfolge. Wenn ein Objekt a über einen Verweis auf Objekt b verfügt und beide Objekte Finalisierungsroutinen besitzen, wird die Finalisierungsroutine von Objekt b möglicherweise vor der Finalisierungsroutine von Objekt a ausgeführt. Dies kann dazu führen, dass Objekt a während der Finalisierung über kein gültiges Objekt b verfügt.
gp  Finalisierungsroutinen werden bei einer normalen Programmbeendigung nicht aufgerufen, um die Beendigung zu beschleunigen. Dieses Verhalten kann gesteuert werden, hiervon wird jedoch abgeraten.

Alle diese Einschränkungen zusammen genommen sind der Grund dafür, dass von einer Verwendung der Finalisierungsroutinen abgesehen werden sollte.


Galileo Computing

32.6.5 Steuerung des Verhaltens der Speicherbereinigung  downtop

Gelegentlich kann es nützlich sein, das Verhalten der Speicherbereinigung zu steuern. Dies sollte in Maßen geschehen; der Zweck einer verwalteten Umgebung besteht zwar darin, die Vorgänge zu steuern, eine zu straffe Steuerung kann jedoch zu Problemen in anderen Bereichen führen.

Erzwingen einer Bereinigung

Mit der Funktion System.GC.Collect() kann eine Bereinigung erzwungen werden. Dies ist nützlich, wenn das Programmverhalten dem Laufzeitsystem nicht bekannt ist. Wenn das Programm beispielsweise vor kurzem eine große Anzahl Operationen abgeschlossen hat und eine Menge Objekte freigesetzt werden, ist das Erzwingen einer Bereinigung sinnvoll.

Erzwingen einer Finalisierung bei Beendigung

Wenn es äußerst wichtig ist, bei Beendigung alle Finalisierungsroutinen aufzurufen, kann die Methode System.GC.RequestFinalizeOnShutdown() eingesetzt werden. Dieses Vorgehen kann zu einer Verlangsamung der Anwendungsbeendigung führen.

Unterdrücken der Finalisierung

Wie bereits erwähnt, wird bei der Objekterstellung eine Instanz des Objekts auf die Finalisierungsliste gesetzt. Falls sich herausstellt, dass ein Objekt nicht finalisiert werden muss (beispielsweise, weil die Aufräumfunktion aufgerufen wurde), kann das Objekt über die Funktion System.GC.SupressFinalize() aus der Finalisierungsliste entfernt werden.


Galileo Computing

32.7 Weitergehende Reflektion  downtop

Die Beispiele im Abschnitt zu den Attributen haben gezeigt, wie die Reflektion zum Ermitteln der einer Klasse angehängten Attribute eingesetzt werden kann. Die Reflektion kann auch dazu verwendet werden, alle Typen in einer Assemblierung zu ermitteln oder die Funktionen in einer Assemblierung dynamisch zu ermitteln und aufzurufen. Mit der Reflektion kann sogar nach Bedarf die .NET-Zwischensprache ausgegeben werden, um direkt ausführbaren Code zu erzeugen.

Die Dokumentation der .NET Common Language Runtime enthält eine detailliertere Ausführung zur Verwendung der Reflektion.


Galileo Computing

32.7.1 Auflisten aller Typen in einer Assemblierung  downtop

In diesem Beispiel wird eine Assemblierung durchlaufen, um sämtliche der verwendeten Typen zu ermitteln.

using System;
using System.Reflection;
enum MyEnum
{
    Val1,
    Val2,
    Val3
}
class MyClass
{
}
struct MyStruct
{
}
class Test
{
    public static void Main(String[] args)
    {
            // Alle Typen der Assemblierung auflisten, die als
            // Parameter übergeben werden
        Assembly a = Assembly.LoadFrom (args[0]);
        Type[] types = a.GetTypes();

            // Jeden Typ durchsehen, und einige Informationen zum
            // jeweiligen Typ ausgeben.
        foreach (Type t in types)
        {
            Console.WriteLine ("Name: {0}", t.FullName);
            Console.WriteLine ("Namespace: {0}", t.Namespace);
            Console.WriteLine ("Base Class: {0}", t.BaseType.FullName);
        }
    }
}

Bei Ausführung dieses Codebeispiels durch Übergeben des Namens der .exe-Datei wird folgende Ausgabe erzeugt:

Name: MyEnum
Namespace:
Base Class: System.Enum
Name: MyClass
Namespace:
Base Class: System.Object
Name: MyStruct
Namespace:
Base Class: System.ValueType
Name: Test
Namespace:
Base Class: System.Object

Galileo Computing

32.7.2 Ermitteln von Mitgliedern  downtop

In diesem Beispiel werden die Mitglieder eines Typs aufgelistet:

using System;
using System.Reflection;
enum MyEnum
{
    Val1,
    Val2,
    Val3
}
class MyClass
{
    MyClass() {}
    static void Process()
    {
    }
    public int DoThatThing(int i, Decimal d, string[] args)
    {
        return(55);
    }
    public int        value = 0;
    public float        log = 1.0f;
    public static int    value2 = 44;
}
class Test
{
    public static void Main(String[] args)
    {
            // Namen und Werte im enum-Abschnitt abrufen
        Console.WriteLine("Fields of MyEnum");
        Type t = typeof (MyEnum);

            // Instanz der Auflistung erstellen
        object en = Activator.CreateInstance(t);
        foreach (FieldInfo f in t.GetFields(BindingFlags.LookupAll))
        {
            object o = f.GetValue(en);
            Console.WriteLine("{0}={1}", f, o);
        }

            // Jetzt Felder der Klasse durchlaufen
        Console.WriteLine("Fields of MyClass");
        t = typeof (MyClass);
        foreach (MemberInfo m in t.GetFields(BindingFlags.LookupAll))
        {
            Console.WriteLine("{0}", m);
        }

            // Jetzt Methoden der Klasse durchlaufen
        Console.WriteLine("Methods of MyClass");
        foreach (MethodInfo m in t.GetMethods(BindingFlags.LookupAll))
        {
            Console.WriteLine("{0}", m);
            foreach (ParameterInfo p in m.GetParameters())
            {
                Console.WriteLine("  Param: {0} {1}",
                    p.ParameterType, p.Name);
            }
        }
    }
}

Dieser Code erzeugt die folgende Ausgabe:

Fields of MyEnum
Int32 value__=0
MyEnum Val1=0
MyEnum Val2=1
MyEnum Val3=2
Fields of MyClass
Int32 value
Single log
Int32 value2
Methods of MyClass
Void Finalize ()
Int32 GetHashCode ()
Boolean Equals (System.Object)
  Param: System.Object obj
System.String ToString ()
Void Process ()
Int32 DoThatThing (Int32, System.Decimal, System.String[])
  Param: Int32 i
  Param: System.Decimal d
  Param: System.String[] args
System.Type GetType ()
System.Object MemberwiseClone ()

Um die Werte der Felder in einer Aufzählung zu erhalten, muss eine Instanz der Aufzählung vorhanden sein. Obwohl die Aufzählung mit Hilfe einer einfachen new-Anweisung erstellt werden kann, wurde hier die Activator-Klasse eingesetzt, um das Erstellen einer Instanz nach Bedarf zu verdeutlichen.

Beim Durchlaufen der Methoden von MyClass werden die Standardmethoden von object ebenfalls angezeigt.


Galileo Computing

32.7.3 Aufrufen von Funktionen  downtop

In diesem Beispiel wird die Reflektion dazu eingesetzt, alle Assemblierungen von der Befehlszeile aus zu öffnen, nach den Klassen zu suchen, mit denen eine bestimmte Assemblierung implementiert wird und anschließend eine Instanz dieser Klassen zu erstellen und eine Funktion für die Assemblierung aufzurufen.

Dieses Vorgehen ist nützlich, um eine Architektur mit sehr später Bindung bereitzustellen, bei der eine Komponente in andere Komponentenlaufzeiten integriert werden kann.

Dieses Beispiel umfasst vier Dateien. Die erste Datei definiert die IProcess-Schnittstelle, die durchsucht wird. Die zweite und die dritte Datei enthalten Klassen, mit denen diese Schnittstelle implemetiert wird. Jede dieser Dateien wird in einer separaten Assemblierung kompiliert. Die letzte Datei ist die Treiberdatei. Mit ihr werden die an der Befehlszeile übergebenen Assemblierungen geöffnet und nach Klassen durchsucht, mit denen IProcess implementiert wird. Wird eine solche Klasse ermittelt, wird eine Instanz der Klasse erzeugt und die Funktion Process() aufgerufen.

IProcess.cs

IProcess definiert die gesuchte Schnittstelle.

// Datei=IProcess.cs
namespace MamaSoft
{
    interface IProcess
    {
        string Process(int param);
    }
}

Process1.cs

// Datei=process1.cs
// Kompilieren mit: csc /target:library process1.cs iprocess.cs
using System;
namespace MamaSoft
{
    class Processor1: IProcess
    {
        Processor1() {}

        public string Process(int param)
        {
            Console.WriteLine("In Processor1.Process(): {0}", param);
            return("Raise the mainsail! ");
        }
    }
}

Dieser Code sollte kompiliert werden mit

csc /target:library process1.cs iprocess.cs

Process2.cs

// Datei=process2.cs
// Kompilieren mit: csc /target:library process2.cs iprocess.cs
using System;
namespace MamaSoft
{
    class Processor2: IProcess
    {
        Processor2() {}

        public string Process(int param)
        {
            Console.WriteLine("In Processor2.Process(): {0}", param);
            return("Shiver me timbers! ");
        }
    }
class Unrelated
{
}
}

Dieser Code sollte kompiliert werden mit

csc /target:library process2.cs iprocess.cs

Driver.cs

// Datei=driver.cs
// Kompilieren mit: csc driver.cs iprocess.cs
using System;
using System.Reflection;
using MamaSoft;
class Test
{
    public static void ProcessAssembly(string aname)
    {
        Console.WriteLine("Loading: {0}", aname);
        Assembly a = Assembly.LoadFrom (aname);

            // Jeden Typ in der Assemblierung durchlaufen
        foreach (Type t in a.GetTypes())
        {
                // Handelt es sich um eine Klasse, ist dies vielleicht 
die
                gesuchte Klasse
            if (t.IsClass)
            {
                Console.WriteLine("  Found Class: {0}", t.FullName);

                    // Prüfen, ob IProcess implementiert wird
                if (t.GetInterface("IProcess") == null)
                    continue;

                    // IProcess wird implementiert. Instanz
                       // des Objekts erstellen.
                object o = Activator.CreateInstance(t);
                    // Parameterliste erstellen, aufrufen
                    // und Rückgabewert ausgeben.
                Console.WriteLine("    Calling Process() on {0}",
                            t.FullName);
                object[] args = new object[] {55};
                object result;
                result = t.InvokeMember("Process",
                    BindingFlags.Default |
                    BindingFlags.InvokeMethod,
                    null, o, args);
                Console.WriteLine("    Result: {0}", result);
            }
        }
    }
    public static void Main(String[] args)
    {
        foreach (string arg in args)
            ProcessAssembly(arg);
    }
}

Nach der Kompilierung kann dieses Beispiel mit folgender DLL ausgeführt werden:

process process1.dll process2.dll

Auf diese Weise wird folgende Ausgabe erzeugt:

Loading: process1.dll
  Found Class: MamaSoft.Processor1
    Calling Process() on MamaSoft.Processor1
In Processor1.Process(): 55
    Result: Raise the mainsail!
Loading: process2.dll
  Found Class: MamaSoft.Processor2
    Calling Process() on MamaSoft.Processor2
In Processor2.Process(): 55
    Result: Shiver me timbers!
  Found Class: MamaSoft.Unrelated

Galileo Computing

32.8 Optimierung  toptop

Bei Verwendung des /optimize+-Flags führt der C#-Compiler folgende Optimierungen durch:

gp  Nie gelesene lokale Variablen werden gelöscht, auch wenn sie zugeordnet sind.
gp  Nicht erreichbarer Code (Code nach einem return beispielsweise) wird entfernt.
gp  Ein try-catch mit leerem try-Block wird entfernt.
gp  Ein try-finally mit leerem try-Block wird in normalen Code konvertiert.
gp  Ein try-finally mit leerem finally-Block wird in normalen Code konvertiert.
gp  Es wird eine Zweigoptimierung durchgeführt (Entfernen redundanter Sprünge in der IL).
   

Select * from SQL Server 2000




Copyright © Galileo Press GmbH 2001 - 2002
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das <openbook> denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


[Galileo Computing]

Galileo Press GmbH, Gartenstraße 24, 53229 Bonn, fon: 0228.42150.0, fax 0228.42150.77, info@galileo-press.de