Freigeben über


21 Stellvertretungen

21.1 Allgemein

Eine Delegatdeklaration definiert eine Klasse, die von der Klasse System.Delegate abgeleitet ist. Eine Delegatinstanz kapselt eine Aufrufliste, also eine Liste mit einzelnen oder mehreren Methoden, die jeweils als aufrufbare Entität bezeichnet werden. Bei Instanzmethoden besteht eine aufrufbare Entität aus einer Instanz und einer Methode für diese Instanz. Bei statischen Methoden besteht eine aufrufbare Entität nur aus einer Methode. Das Aufrufen einer Delegatinstanz mit einem geeigneten Satz von Argumenten veranlasst das Aufrufen der aufrufbaren Entität des Delegaten mit dem gegebenen Satz von Argumenten.

Hinweis: Eine interessante und nützliche Eigenschaft einer Delegateninstanz besteht darin, dass sie die Klassen der Methoden, die sie kapselt, nicht kennen oder interessieren; all das ist wichtig, dass diese Methoden mit dem Typ der Stellvertretung kompatibel sind (§21.4). Dadurch eignen sich Delegate ideal für den „anonymen“ Aufruf. Hinweisende

21.2 Stellvertretungsdeklarationen

Eine delegate_declaration ist eine type_declaration (§14.7), die einen neuen Delegattyp deklariert.

delegate_declaration
    : attributes? delegate_modifier* 'delegate' return_type delegate_header
    | attributes? delegate_modifier* 'delegate' ref_kind ref_return_type
      delegate_header
    ;

delegate_header
    : identifier '(' parameter_list? ')' ';'
    | identifier variant_type_parameter_list '(' parameter_list? ')'
      type_parameter_constraints_clause* ';'
    ;
    
delegate_modifier
    : 'new'
    | 'public'
    | 'protected'
    | 'internal'
    | 'private'
    | unsafe_modifier   // unsafe code support
    ;

unsafe_modifier ist in §24.2 definiert.

Es handelt sich um einen Kompilierungsfehler für diesen Modifizierer, der mehrmals in einer Delegatdeklaration auftritt.

Eine Delegatdeklaration, die eine variant_type_parameter_list bereitstellt, ist eine generische Delegatdeklaration. Darüber hinaus ist jedes in einer generischen Klassendeklaration oder einer generischen Strukturdeklaration geschachtelte Delegat seinerseits eine generische Delegatdeklaration, da Typargumente für den enthaltenden Typ übergeben werden müssen, um einen konstruierten Typ (§ 8.4) zu erstellen.

Der Modifizierer new ist nur für Delegate zulässig, die in einem anderen Typ deklariert wurden. In diesem Fall gibt er an, dass ein solcher Delegat einen geerbten Member unter demselben Namen verbirgt, siehe § 15.3.5.

Die Modifizierer public, protected, internal und private kontrollieren die Zugriff auf den Delegattyp. Abhängig vom Kontext, in dem die Delegatdeklaration erfolgt, sind einige dieser Modifizierer möglicherweise nicht zulässig (§ 7.5.2).

Der Typname des Delegaten lautet identifier.

Wie bei Methoden (§ 15.6.1) nutzt das Delegat bei Vorliegen von ref returns-by-ref. Hat dagegen return_type den Wert void, nutzt der Delegat returns-no-value. Andernfalls nutzt der Delegat returns-by-value.

Die optionale parameter_list gibt die Parameter des Delegaten an.

Der return_type einer Delegatdeklaration (returns-by-value oder returns-no-value) gibt den Typ des vom Delegaten zurückgegebenen Ergebnisses an (sofern relevant).

Der ref_return_type einer returns-by-ref-Delegatdeklaration gibt den Typ der Variablen an, auf die die vom Delegaten zurückgegebene variable_reference (§ 9.5) verweist.

Die optionale variant_type_parameter_list (§19.2.3) gibt die Typparameter für den Delegaten selbst an.

Der Rückgabetyp eines Stellvertretungstyps muss entweder voidoder ausgabesicher (§19.2.3.2) sein.

Alle Parametertypen eines Stellvertretungstyps müssen eingabesicher sein (§19.2.3.2). Darüber hinaus müssen alle Ausgabe- oder Referenzparametertypen ebenfalls output-safe sein.

Hinweis: Ausgabeparameter müssen aufgrund allgemeiner Implementierungsbeschränkungen input-safe sein. Hinweisende

Darüber hinaus müssen alle Klassentypeinschränkungen, Schnittstellentypeinschränkungen und Typparametereinschränkungen für Typparameter des Delegaten input-safe sein.

Delegattypen sind in C# namensäquivalent, nicht strukturäquivalent.

Beispiel:

delegate int D1(int i, double d);
delegate int D2(int c, double d);

Die Delegattypen D1 und D2 sind unterschiedliche Typen, also trotz identischer Signaturen nicht austauschbar.

Ende des Beispiels

Wie andere generische Typdeklarationen müssen Typargumente angegeben werden, um einen konstruierten Delegattyp zu erstellen. Die Parametertypen und der Rückgabetyp eines konstruierten Delegattyps werden mittels Substitution jedes Typparameters in der Delegatdeklaration durch das entsprechende Typargument des konstruierten Delegattyps erstellt.

Die einzige Möglichkeit, einen Delegattyp zu deklarieren, ist delegate_declaration. Jeder Delegattyp ist ein Verweistyp, der von System.Delegate abgeleitet wird. Die für jeden Stellvertretungstyp erforderlichen Mitglieder sind in §21.3 detailliert aufgeführt. Delegattypen sind implizit sealed. Deshalb darf kein Typ von einem Delegattyp abgeleitet werden. Ebenso ist es unzulässig, einen von System.Delegate abgeleiteten Klassentyp zu deklarieren, bei dem es sich nicht um einen Delegaten handelt. System.Delegate ist selbst kein Delegattyp. Es handelt sich vielmehr um einen Klassentyp, von dem alle Delegattypen abgeleitet werden.

21.3 Delegierte Mitglieder

Jeder Delegattyp erbt Member von der Klasse Delegate, siehe § 15.3.4. Darüber hinaus muss jeder Delegattyp eine nicht generische Invoke-Methode bereitstellen, deren Parameterliste der parameter_list in der Delegatdeklaration entspricht, deren Rückgabetyp dem return_type oder ref_return_type in der Delegatdeklaration entspricht, und für returns-by-ref-Delegate, deren ref_kind dem in der Delegatdeklaration entspricht. Die Methode Invoke muss mindestens die Zugriffsmöglichkeiten des enthaltenden Delegattyps bieten. Das Aufrufen der Invoke Methode für einen Delegatentyp entspricht semantisch der Verwendung der Stellvertretungsaufrufssyntax (§21.6).

Implementierungen können zusätzliche Member im Delegattyp definieren.

Abgesehen von der Instanziierung können alle Vorgänge, die auf eine Klasse oder Klasseninstanz angewendet werden können, auch auf eine Delegatklasse oder -instanz angewendet werden. Insbesondere kann über die übliche Memberzugriffssyntax auf Member des Typs System.Delegate zugegriffen werden.

21.4 Stellvertretungskompatibilität

Eine Methode oder ein Delegattyp (M) ist kompatibel mit einem Delegattyp (D), wenn alle folgenden Aussagen gelten:

  • D und M weisen dieselbe Anzahl an Parametern auf und jeder Parameter in D weist denselben als Referenz zu übergebenden Parametermodifizierer wie der entsprechende Parameter in M auf.
  • Für jeden Wertparameter ist eine Identitätskonvertierung (§ 10.2.2) oder eine implizite Verweiskonvertierung (§ 10.2.8) aus dem Parametertyp in D in den entsprechenden Parametertyp in M vorhanden.
  • Für jeden als Referenz zu übergebenden Parameter ist der Parametertyp D identisch mit dem Parametertyp in M.
  • Eine der folgenden Bedingungen trifft zu:
    • D und M sind beide Rückgaben mit keinem Wert.
    • D und M sind Rückgabe-nach-Wert (§15.6.1, §21.2), und eine Identitäts- oder implizite Verweiskonvertierung ist vom Rückgabetyp in den Rückgabetyp von MD.
    • D und M sind beide returns-by-ref, eine Identitätskonvertierung zwischen dem Rückgabetyp M und dem Rückgabetyp von D existiert, und beide haben dasselbe ref_kind.

Diese Definition der Kompatibilität ermöglicht die Kovarianz im Rückgabetyp und die Kontravarianz in Parametertypen.

Beispiel:

delegate int D1(int i, double d);
delegate int D2(int c, double d);
delegate object D3(string s);

class A
{
    public static int M1(int a, double b) {...}
}

class B
{
    public static int M1(int f, double g) {...}
    public static void M2(int k, double l) {...}
    public static int M3(int g) {...}
    public static void M4(int g) {...}
    public static object M5(string s) {...}
    public static int[] M6(object o) {...}
}

Die Methoden A.M1 und B.M1 sind mit den Delegattypen D1 und D2 kompatibel, da sie denselben Rückgabetyp und dieselbe Parameterliste verwenden. Die Methoden B.M2, B.M3 und B.M4 sind mit den Delegattypen D1 und D2 nicht kompatibel, da sie unterschiedliche Rückgabetypen oder Parameterlisten verwenden. Die Methoden B.M5 und B.M6 sind beide mit dem Delegattyp D3 kompatibel.

Ende des Beispiels

Beispiel:

delegate bool Predicate<T>(T value);

class X
{
    static bool F(int i) {...}
    static bool G(string s) {...}
}

Die Methode X.F ist mit dem Delegattyp Predicate<int> kompatibel, und die Methode X.G ist mit dem Delegattyp Predicate<string> kompatibel.

Ende des Beispiels

Hinweis: Die intuitive Bedeutung der Delegatkompatibilität besteht darin, dass eine Methode mit einem Delegattyp kompatibel ist, also jeder Aufruf des Delegaten durch einen Aufruf der Methode ersetzt werden kann, ohne die Typsicherheit zu verletzen. Optionale Parameter und Parameterarrays werden als explizite Parameter behandelt. Beachten Sie den folgenden Code als Beispiel:

delegate void Action<T>(T arg);

class Test
{
    static void Print(object value) => Console.WriteLine(value);

    static void Main()
    {
        Action<string> log = Print;
        log("text");
    }
}

Die Methode Print ist mit dem Delegattyp Action<string> kompatibel, da jeder Aufruf eines Action<string>-Delegaten auch ein gültiger Aufruf der Methode Print wäre.

Wenn die Signatur der oben genannten Print Methode beispielsweise in Print(object value, bool prependTimestamp = false) geändert würde, wäre die Print Methode nach den Regeln dieser Unterklausel nicht mehr mit Action<string> kompatibel.

Hinweisende

21.5 Instanziierung der Stellvertretung

Eine Instanz einer Stellvertretung wird durch eine delegate_creation_expression (§12.8.17.5), eine Konvertierung in einen Delegattyp, eine Stellvertretungskombination oder das Entfernen von Stellvertretungen erstellt. Die neu erstellte Delegatinstanz verweist dann auf eine oder mehrere der folgenden Komponenten:

  • Die statische Methode, auf die in delegate_creation_expression verwiesen wird. Oder:
  • Das Zielobjekt (darf nicht null sein) und die Instanzmethode, auf die in delegate_creation_expression verwiesen wird. Oder:
  • Ein anderer Stellvertreter (§12.8.17.5).

Beispiel:

delegate void D(int x);

class C
{
    public static void M1(int i) {...}
    public void M2(int i) {...}
}

class Test
{
    static void Main()
    {
        D cd1 = new D(C.M1); // Static method
        C t = new C();
        D cd2 = new D(t.M2); // Instance method
        D cd3 = new D(cd2);  // Another delegate
    }
}

Ende des Beispiels

Der Satz der in einer Delegatinstanz gekapselten Methoden wird als Aufrufliste bezeichnet. Wenn eine Delegatinstanz aus einer einzelnen Methode erstellt wird, kapselt sie diese Methode, und ihre Aufrufliste enthält nur einen Eintrag. Wenn jedoch zwei Delegatinstanzen, bei denen es sich nicht um null handelt, kombiniert werden, werden ihre Aufruflisten in der Reihenfolge vom linken zum rechten Operanden verkettet. Dabei entsteht eine neue Aufrufliste, die zwei oder mehr Einträge enthält.

Wenn eine neue Stellvertretung aus einer einzigen Stellvertretung erstellt wird, hat die resultierende Aufrufliste nur einen Eintrag, bei dem es sich um den Quelldelegat (§12.8.17.5) handelt.

Stellvertretungen werden mit den binären + Operatoren (§12.12.5) und += Operatoren (§12.23.4) kombiniert. Eine Stellvertretung kann aus einer Kombination von Stellvertretungen mithilfe der binären - (§12.12.6) und -= operatoren (§12.23.4) entfernt werden. Stellvertretungen können für die Gleichheit verglichen werden (§12.14.9).

Beispiel: Das folgende Beispiel zeigt die Instanziierung einer Reihe von Delegaten und die entsprechenden Aufruflisten:

delegate void D(int x);

class C
{
    public static void M1(int i) {...}
    public static void M2(int i) {...}
}

class Test
{
    static void Main() 
    {
        D cd1 = new D(C.M1); // M1 - one entry in invocation list
        D cd2 = new D(C.M2); // M2 - one entry
        D cd3 = cd1 + cd2;   // M1 + M2 - two entries
        D cd4 = cd3 + cd1;   // M1 + M2 + M1 - three entries
        D cd5 = cd4 + cd3;   // M1 + M2 + M1 + M1 + M2 - five entries
        D td3 = new D(cd3);  // [M1 + M2] - ONE entry in invocation
                             // list, which is itself a list of two methods.
        D td4 = td3 + cd1;   // [M1 + M2] + M1 - two entries
        D cd6 = cd4 - cd2;   // M1 + M1 - two entries in invocation list
        D td6 = td4 - cd2;   // [M1 + M2] + M1 - two entries in invocation list,
                             // but still three methods called, M2 not removed.
   }
}

Wenn cd1 und cd2 instanziiert werden, kapseln sie jeweils eine Methode. Wenn cd3 instanziiert wird, enthält es eine Aufrufliste mit zwei Methoden: M1 und M2 (in dieser Reihenfolge). Die Aufrufliste von cd4 enthält M1, M2 und M1 (in dieser Reihenfolge). Für cd5 enthält die Aufrufliste M1, M2, M1, M1 und M2 (in dieser Reihenfolge).

Beim Erstellen eines Delegaten mit delegate_creation_expression aus einem anderen Delegaten enthält das Ergebnis eine Aufrufliste mit einer anderen Struktur als beim Original, die jedoch dieselben Methoden in derselben Reihenfolge aufruft. Wenn td3 aus cd3 erstellt wird, enthält die Aufrufliste nur ein Member. Dieses Member ist dann aber eine Liste der Methoden M1 und M2, und diese Methoden werden von td3 in derselben Reihenfolge wie von cd3 aufgerufen. Entsprechend gilt, dass die Aufrufliste beim Instanziieren von td4 nur zwei Einträge enthält, aber die drei Methoden M1, M2 und M1 (in dieser Reihenfolge) genau wie cd4 aufruft.

Die Struktur der Aufrufliste wirkt sich auf die Delegatentfernung aus. Delegat cd6, erstellt, indem cd2 (ruft M2 auf) von cd4 (ruft M1, M2 und M1 auf) subtrahiert wird, ruft M1 und M1 auf. Delegat td6, erstellt, indem cd2 (ruft M2 auf) von td4 (ruft M1, M2 und M1 auf) subtrahiert wird, ruft weiterhin M1, M2 und M1 (in dieser Reihenfolge) auf, weil M2 kein einzelner Eintrag in der Liste ist, sondern Member einer geschachtelten Liste. Weitere Beispiele für das Kombinieren (und Entfernen) von Delegaten finden Sie unter §21.6.

Ende des Beispiels

Nach der Instanziierung verweist eine Delegatinstanz immer auf dieselbe Aufrufliste.

Hinweis: Bedenken Sie, dass beim Kombinieren zweier Delegate (oder beim Entfernen voneinander) ein neuer Delegat mit eigener Aufrufliste entsteht. Die Aufruflisten der beiden ursprünglichen Delegate bleiben unverändert. Hinweisende

21.6 Stellvertretungsaufruf

C# stellt eine spezielle Syntax für das Aufrufen von Delegaten bereit. Wenn eine Delegatinstanz, die nichtnull ist und deren Aufrufliste einen Eintrag enthält, aufgerufen wird, ruft sie die eine Methode mit denselben Argumenten auf, die ihr gegeben wurden, und gibt denselben Wert wie die referenzierte Methode zurück. (Ausführliche Informationen zum Aufruf von Delegaten finden Sie in §12.8.10.4.) Tritt während des Aufrufs eines solchen Delegaten eine Ausnahme auf und wird diese Ausnahme nicht innerhalb der aufgerufenen Methode abgefangen, wird die Suche nach einer Ausnahme-Catch-Klausel in der Methode fortgesetzt, die den Delegaten aufgerufen hat, als ob diese Methode direkt die Methode aufgerufen hätte, auf die sich dieser Delegat bezog.

Der Aufruf einer Delegat-Instanz, deren Aufrufliste mehrere Einträge enthält, wird fortgesetzt, indem jede der Methoden in der Aufrufliste synchron und in der angegebenen Reihenfolge aufgerufen wird. Jeder so aufgerufenen Methode wird dieselbe Gruppe von Argumenten übergeben, die auch an die Delegatinstanz übergeben wurde. Wenn ein solcher Delegataufruf Referenzparameter (§ 15.6.2.3.3) enthält, erfolgt jeder Methodenaufruf mit einem Verweis auf diese Variable. Änderungen an dieser Variablen durch eine Methode in der Aufrufliste sind für nachfolgende Methoden in der Aufrufliste sichtbar. Wenn der Delegataufruf Ausgabeparameter oder einen Rückgabewert enthält, stammt der endgültige Wert aus dem Aufruf des letzten Delegaten in der Liste. Tritt während der Verarbeitung des Aufrufs eines solchen Delegaten eine Ausnahme auf und wird diese Ausnahme nicht in der aufgerufenen Methode abgefangen, wird die Suche nach einer Ausnahme-Catch-Klausel in der Methode fortgesetzt, die den Delegaten aufgerufen hat. Die nachfolgenden Methoden in der Aufrufliste werden nicht aufgerufen.

Beim Versuch, eine Delegatinstanz aufzurufen, deren Wert null ist, tritt eine Ausnahme des Typs System.NullReferenceException auf.

Beispiel: Das folgende Beispiel zeigt, wie Delegate instanziiert, kombiniert, entfernt und aufgerufen werden:

delegate void D(int x);

class C
{
    public static void M1(int i) => Console.WriteLine("C.M1: " + i);

    public static void M2(int i) => Console.WriteLine("C.M2: " + i);

    public void M3(int i) => Console.WriteLine("C.M3: " + i);
}

class Test
{
    static void Main()
    {
        D cd1 = new D(C.M1);
        cd1(-1);             // call M1
        D cd2 = new D(C.M2);
        cd2(-2);             // call M2
        D cd3 = cd1 + cd2;
        cd3(10);             // call M1 then M2
        cd3 += cd1;
        cd3(20);             // call M1, M2, then M1
        C c = new C();
        D cd4 = new D(c.M3);
        cd3 += cd4;
        cd3(30);             // call M1, M2, M1, then M3
        cd3 -= cd1;          // remove last M1
        cd3(40);             // call M1, M2, then M3
        cd3 -= cd4;
        cd3(50);             // call M1 then M2
        cd3 -= cd2;
        cd3(60);             // call M1
        cd3 -= cd2;          // impossible removal is benign
        cd3(60);             // call M1
        cd3 -= cd1;          // invocation list is empty so cd3 is null
        // cd3(70);          // System.NullReferenceException thrown
        cd3 -= cd1;          // impossible removal is benign
    }
}

Wie in der Anweisung cd3 += cd1; gezeigt, kann ein Delegat mehrmals in einer Aufrufliste vorhanden sein. In diesem Fall wird er einfach einmal pro Vorkommen aufgerufen. Wenn der Delegat in einer Aufrufliste wie dieser entfernt wird, wird tatsächlich das letzte Vorkommen in der Aufrufliste entfernt.

Unmittelbar vor der Ausführung der abschließenden Anweisung cd3 -= cd1 verweist der Delegat cd3 auf eine leere Aufrufliste. Der Versuch, einen Delegaten aus einer leeren Liste zu entfernen (oder einen nicht vorhandenen Delegaten aus einer Liste zu entfernen, die nicht leer ist), verursacht keinen Fehler.

Folgendes wird ausgegeben:

C.M1: -1
C.M2: -2
C.M1: 10
C.M2: 10
C.M1: 20
C.M2: 20
C.M1: 20
C.M1: 30
C.M2: 30
C.M1: 30
C.M3: 30
C.M1: 40
C.M2: 40
C.M3: 40
C.M1: 50
C.M2: 50
C.M1: 60
C.M1: 60

Ende des Beispiels