共用方式為


20 個委派

20.1 一般

委派宣告會定義衍生自 類別的類別 System.Delegate。 委派實例會封裝調用清單,這是一或多個方法的清單,每個方法稱為可呼叫的實體 對於實例方法,可呼叫的實體是由實例和該實例上的方法所組成。 對於靜態方法,可呼叫的實體只包含方法。 叫用具有一組適當自變數的委派實例,會導致每個委派的可呼叫實體使用指定的自變數集合來叫用。

注意:委派實例的有趣且實用的屬性是,它不知道或關心其封裝之方法的類別;重要的是,這些方法與委派的類型相容(~20.4)。 這讓委派完全適合「匿名」調用。 end note

20.2 委派宣告

delegate_declaration是宣告新委派類型的type_declaration~14.7)。

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定義於 ≦23.2

同一個修飾詞在委派宣告中出現多次是編譯時期錯誤。

提供variant_type_parameter_list委派宣告是泛型委派宣告。 此外,泛型類別宣告或泛型結構宣告內的任何委派都是泛型委派宣告,因為應該提供包含型別的型別自變數來建立建構型別 ({8.4)。

new只有在另一種類型內宣告的委派上才允許修飾詞,在此情況下,它會指定這類委派會以相同名稱隱藏繼承的成員,如 \15.3.5 中所述

publicprotectedinternalprivate 修飾詞可控制委派類型的存取範圍。 根據委派宣告發生的內容而定,可能不允許其中一些修飾詞 ({7.5.2)。

委派的類型名稱是 標識碼

如同方法 (~15.6.1),如果 ref 存在,則委派會傳回 by-ref;否則,如果 return_typevoid,則委派會傳回 no-no-value;否則,委派會傳回逐一值。

選擇性 parameter_list 會指定委派的參數。

回值或 returns-no-value 委派宣告的return_type 會指定委派傳回的結果類型,如果有的話。

returns-by-ref 委派宣告的ref_return_type會指定委派所傳回之variable_reference{9.5) 所參考的變數類型。

選擇性 的variant_type_parameter_list~18.2.3) 會指定委派本身的類型參數。

委派型別的傳回型別應該是 void,或輸出安全型別(~18.2.3.2)。

委派型別的所有參數類型都應該是輸入安全(~18.2.3.2)。 此外,任何輸出或參考參數類型也應該是輸出安全。

注意:由於常見的實作限制,輸出參數必須安全輸入。 end note

此外,委派的任何類型參數上,每個類別類型條件約束、介面類型條件約束和類型參數條件約束都應該是輸入安全。

C# 中的委派類型是名稱相等的,在結構上不相等。

範例:

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

委派型 D1 別和 D2 是兩個不同的類型,因此它們不可以互換,儘管其簽章相同。

end 範例

如同其他泛型型別宣告,應該提供型別自變數來建立建構的委派型別。 建構委派類型的參數類型和傳回型別是由替代委派宣告中每個類型參數所建立,這是建構委派型別的對應型別自變數。

宣告委派型別的唯一 方式是透過delegate_declaration。 每個委派類型都是衍生自 System.Delegate的參考型別。 每個委派類型所需的成員詳述於 \20.3。 委派類型是隱含的 sealed,因此不允許從委派類型衍生任何類型。 也不允許宣告衍生自 System.Delegate的非委派類別類型。 System.Delegate 本身不是委派類型;它是衍生所有委派型別的類別類型。

20.3 委派成員

每個委派類型都會從 類別繼承成員,Delegate如 \15.3.4 中所述。 此外,每個委派類型都應該提供非泛型Invoke方法,其參數清單符合委派宣告中的parameter_list,其傳回型別符合委派宣告中的return_typeref_return_type,以及傳回ref_kind符合委派宣告中之ref_kind的傳回委派。 方法 Invoke 至少可以和包含委派型別一樣可存取。 Invoke在委派型別上呼叫 方法的語意相當於使用委派調用語法 (~20.6) 。

實作可能會在委派類型中定義其他成員。

除了具現化之外,可以套用至類別或類別實例的任何作業也可以分別套用至委派類別或實例。 特別是,可以透過一般成員存取語法來存取類型的成員 System.Delegate

20.4 委派相容性

如果下列所有專案都成立,方法或委派類型M與委派類型D相容:

  • DM 具有相同數目的參數,且 中的每個 D 參數與 中的 M對應參數具有相同的參考參數修飾詞。
  • 針對每個值參數,識別轉換 (~10.2.2) 或隱含參考轉換 (~10.2.8) 會從 中的 D 參數類型到 中 M對應的參數類型。
  • 針對每個參考參數,中的 D 參數類型與 中的 M參數類型相同。
  • 下列其中一項成立:
    • DM 都是 returns-no-value
    • DM 是傳回值 (~15.6.1\20.2),而且從 的 M 傳回型別到 的傳回型 D別存在識別或隱含參考轉換。
    • DM 都是 returns-by-ref,且在 的傳回型 M 別與 的傳回型 D別之間存在識別轉換,而且兩者都有相同的 ref_kind

此相容性定義允許傳回型別中的共變數和參數類型的反變數。

範例:

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) {...}
}

和方法A.M1B.M1都與委派類型和 D1 D2相容,因為它們具有相同的傳回類型和參數清單。 方法 B.M2B.M3B.M4 與 委派類型不相容,因為它們有不同的傳回型D1D2別或參數清單。 和方法B.M5B.M6都與委派類型D3相容。

end 範例

範例:

delegate bool Predicate<T>(T value);

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

方法 X.F 與委派類型 Predicate<int> 相容,而且 方法 X.G 與委派類型 Predicate<string>相容。

end 範例

注意:委派相容性的直覺意義在於,如果委派的每個調用都可以取代為方法的調用,而不違反類型安全性,將選擇性參數和參數數位視為明確參數,則方法與委派類型相容。 例如,在下列程式代碼中:

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");
    }
}

方法 PrintAction<string> 委派類型相容,因為委派的任何 Action<string> 調用也會是方法的有效調用 Print

例如,如果上述方法的 Print 簽章已變更為 Print(object value, bool prependTimestamp = false)Print 則此方法將不再與 Action<string> 這個子句的規則相容。

end note

20.5 委派具現化

委派的實例是由delegate_creation_expression建立的,也就是轉換至委派類型、委派組合或委派移除。 新建立的委派實例接著會參考一或多個:

  • delegate_creation_expression 中所參考的靜態方法,或
  • 目標物件(不能為 null)和delegate_creation_expression中參考的實例方法,或
  • 另一個委派(~12.8.16.6)。

範例:

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
    }
}

end 範例

委派實例所封裝的方法集合稱為 調用清單。 從單一方法建立委派實例時,它會封裝該方法,而且其調用清單只包含一個專案。 不過,當兩個非null 委派實例結合時,其調用清單會串連在左操作數,然後依右操作數的順序組成新的調用清單,其中包含兩個或多個專案。

從單一委派建立新的委派時,產生的調用清單只有一個專案,也就是來源委派 (~12.8.16.6)。

委派會使用二進位 #(~12.10.5)和+=運算符來合併(~12.21.4)。+ 您可以使用二進位 - #12.10.6) 和-=運算符,從委派的組合中移除委派(~12.21.4)。 委派可以比較是否相等(~12.12.9)。

範例:下列範例顯示一些委派的具現化,以及其對應的調用清單:

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.
   }
}

當 和 cd2 具現化時cd1,它們都會封裝一個方法。 具現化時 cd3 ,它會有兩個方法的調用清單, M1 並以 M2該順序排列。 cd4的呼叫清單會依該順序包含 M1M2M1。 針對 cd5,呼叫清單會依該順序包含 M1M2M1M1M2

從具有delegate_creation_expression的另一個委派建立委派 時, 結果具有與原始結構不同的調用清單,但會導致使用相同的順序叫用相同的方法。 從cd3其調用清單中建立時td3,只有一個成員,但該成員是方法M1M2的清單,而且這些方法td3的叫用順序與 叫cd3用的順序相同。 同樣地,當td4具現化其調用清單只有兩個專案,但是它會依該順序cd4叫用三個方法 M1M2M1

調用清單的結構會影響委派減法。 委派 cd6,其建立方式是從 cd2 減去 (叫M2M1M2用、 cd4M1) 叫用 M1 和 。M1 不過,藉由從 cd2 減去 (M2M1M2用、 和 M1td4 所建立的委派 td6,仍會依該順序叫M1用 、 M2M1,如同M2清單中不是單一專案,而是巢狀清單的成員。 如需結合 (以及移除) 委派的更多範例,請參閱 <20.6

end 範例

具現化后,委派實例一律會參考相同的調用清單。

注意:請記住,當兩個委派合併或一個從另一個委派移除時,具有其本身調用清單的新委派結果;合併或移除的委派調用清單會保持不變。 end note

20.6 委派調用

C# 提供叫用委派的特殊語法。 當叫用其調用清單包含一個專案的非null 委派實例時,它會叫用具有所指定相同自變數的一個方法,並傳回與所參考方法相同的值。 (如需委派調用的詳細資訊,請參閱 <12.8.9.4 >。如果在叫用這類委派期間發生例外狀況,而且叫用的方法內不會攔截該例外狀況,則搜尋例外狀況 catch 子句會在呼叫委派的方法中繼續,就好像該方法已直接呼叫該委派所參考的方法一樣。

叫用清單包含多個專案的委派實例調用,會依序叫用調用清單中的每一個方法繼續進行。 所呼叫的每個方法都會傳遞與委派實例相同的自變數集。 如果這類委派調用包含參考參數 (~15.6.2.3.3.3),則每個方法調用都會與相同變數的參考一起發生;叫用清單中的一個方法對該變數所做的變更將會在調用清單的更下一步的方法中看見。 如果委派調用包含輸出參數或傳回值,其最終值會來自清單中最後一個委派的調用。 如果在處理這類委派的叫用期間發生例外狀況,而且叫用的方法內不會攔截該例外狀況,則搜尋例外狀況 catch 子句會繼續於呼叫委派的方法中,而且不會叫用清單之後的任何方法。

嘗試叫用值會導致 null 類型 System.NullReferenceException例外狀況的委派實例。

範例:下列範例示範如何具現化、合併、移除和叫用委派:

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
    }
}

如 語句 cd3 += cd1;所示,委派可以多次出現在調用清單中。 在此情況下,只會在每個發生次數叫用一次。 在這樣的調用清單中,當移除該委派時,調用清單中的最後一個出現專案就是實際移除的叫用清單。

在執行最後一個語句之前, cd3 -= cd1委派 cd3 會緊接在參考空的調用清單。 嘗試從空白清單中移除委派(或從非空白清單中移除不存在的委派),不是錯誤。

產生的輸出為:

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

end 範例