程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> [轉載]C#委托和事情(Delegate、Event、EventHandler、EventArgs)

[轉載]C#委托和事情(Delegate、Event、EventHandler、EventArgs)

編輯:C#入門知識

[轉載]C#委托和事情(Delegate、Event、EventHandler、EventArgs)。本站提示廣大學習愛好者:([轉載]C#委托和事情(Delegate、Event、EventHandler、EventArgs))文章只能為提供參考,不一定能成為您想要的結果。以下是[轉載]C#委托和事情(Delegate、Event、EventHandler、EventArgs)正文


原文鏈接:http://blog.csdn.net/zwj7612356/article/details/8272520

14.1、委托

當要把辦法作為實參傳送給其他辦法的形參時,形參需求運用委托。委托是一個類型,是一個函數指針類型,這個類型將該委托的實例化對象所能指向的函數的細節封裝起來了,即規則了所能指向的函數的簽名,也就是限制了所能指向的函數的參數和前往值。當實例化委托的時分,委托對象會指向某一個婚配的函數,本質就是將函數的地址賦值給了該委托的對象,然後就可以經過該委托對象來調用所指向的函數了。應用委托,順序員可以在委托對象中封裝一個辦法的援用,然後委托對象作為形參將被傳給調用了被援用辦法的代碼,而不需求知道在編譯時辰詳細是哪個辦法被調用。

普通的調用函數,我們都不會去運用委托,由於假如只是單純的調用函數,運用委托更費事一些;但是假如想將函數作為實參,傳遞給某個函數的形參,那麼形參就一定要運用委托來接納實參,普通運用辦法是:在函數裡面定義委托對象,並指向某個函數,再將這個對象賦值給函數的形參,形參也是該委托類型的對象變量,函數外面再經過形參來調用所指向的函數。

14.1.1、定義委托

語法如下:

delegate  result-type   Identifier ([parameters]);

闡明:

result-type:前往值的類型,和辦法的前往值類型分歧

Identifier:委托的稱號

parameters:參數,要援用的辦法帶的參數

小結:

當定義了委托之後,該委托的對象一定可以而且也只能指向該委托所限制的函數。即參數的個數、類型、順序都要婚配,前往值的類型也要婚配。

由於定義委托相當於是定義一個新類,所以可以在定義類的任何中央定義委托,既可以在一個類的外部定義,那麼此時就要經過該類的類名來調用這個委托(委托必需是public、internal),也可以在任何類的內部定義,那麼此時在命名空間中與類的級別是一樣的。依據定義的可見性,可以在委托定義上添加普通的訪問修飾符:當委托定義在類的裡面,那麼可以加上public、internal修飾符;假如委托定義到類的外部,那麼可以加上public、 private、 protected、internal。普通委托都是定義在類的裡面的。

14.1.2、實例化委托

Identifier  objectName  =  new  Identifier( functionName);

實例化委托的本質就是將某個函數的地址賦值給委托對象。在這裡:

Identifier :這個是委托名字。

objectName :委托的實例化對象。

functionName:是該委托對象所指向的函數的名字。關於這個函數名要特別留意:定義這個委托對象一定是在類中定義的,那麼假如所指向的函數也在該類中,不論該函數是靜態還是非靜態的,那麼就直接寫函數名字就可以了;假如函數是在別的類外面定義的public、

internal,但是假如是靜態,那麼就直接用類名.函數名,假如是非靜態的,那麼就類的對象名.函數名,這個函數名與該對象是有關系的,比方假如函數中呈現了this,表示的就是對以後對象的調用。

14.1.3、委托推斷

C# 2.0用委托推斷擴展了委托的語法。當我們需求定義委托對象並實例化委托的時分,就可以只傳送函數的稱號,即函數的地址:

Identifier  objectName  =  functionName;

這外面的functionName與14.1.2節中實例化委托的functionName是一樣的,沒什麼區別,滿足下面的規則。

C#編譯器創立的代碼是一樣的。編譯器會用objectName檢測需求的委托類型,因而會創立Identifier委托類型的一個實例,用functionName即辦法的地址傳送給Identifier的結構函數。

留意:

不能在functionName前面加括號和實參,然後把它傳送給委托變量。調用辦法普通會前往一個不能賦予委托變量的普通對象,除非這個辦法前往的是一個婚配的委托對象。總之:只能把相婚配的辦法的地址賦予委托變量。

委托推斷可以在需求委托實例化的任何中央運用,就跟定義普通的委托對象是一樣的。委托推斷也可以用於事情,由於事情基於委托(參見本章前面的內容)。

14.1.4、匿名辦法

到目前為止,要想使委托任務,辦法必需曾經存在。但實例化委托還有另外一種方式:即經過匿名辦法。

用匿名辦法定義委托的語法與後面的定義並沒有區別。但在實例化委托時,就有區別了。上面是一個十分復雜的控制台使用順序,闡明了如何運用匿名辦法:

using System;

namespace Wrox.ProCSharp.Delegates

{

  class Program

  {

    delegate string DelegateTest(string val);

    static void Main()

    {

  string mid = ", middle part,";

  //在辦法中定義了辦法

  DelegateTest  anonDel = delegate(string param)

  {

    param += mid;

    param += " and this was added to the string.";

    return param;

  };

  Console.WriteLine(anonDel("Start of string"));

    }

  }

}

委托DelegateTest在類Program中定義,它帶一個字符串參數。有區別的是Main辦法。在定義anonDel時,不是傳送已知的辦法名,而是運用一個復雜的代碼塊:它後面是關鍵字delegate,前面是一個參數:

delegate(string param)

{

  param += mid;

  param += " and this was added to the string.";

  return param;

};

匿名辦法的優點是增加了要編寫的代碼。辦法僅在有委托運用時才定義。在為事情定義委托時,這是十分顯然的。(本章前面討論事情。)這有助於降低代碼的復雜性,尤其是定義了好幾個事情時,代碼會顯得比擬復雜。運用匿名辦法時,代碼執行得不太快。編譯器仍定義了一個辦法,該辦法只要一個自動指定的稱號,我們不需求知道這個稱號。

在運用匿名辦法時,必需遵照兩個規則:

1、在匿名辦法中不能運用跳轉語句跳到該匿名辦法的內部,反之亦然:匿名辦法內部的跳轉語句不能跳到該匿名辦法的外部。

2、在匿名辦法外部不能訪問不平安的代碼。另外,也不能訪問在匿名辦法內部運用的ref和out參數。但可以運用在匿名辦法內部定義的其他變量。辦法外部的變量、辦法的參數可以恣意的運用。

假如需求用匿名辦法屢次編寫同一個功用,就不要運用匿名辦法。而編寫一個指定的辦法比擬好,由於該辦法只需編寫一次,當前可經過稱號援用它。

14.1.5、多播委托

後面運用的每個委托都只包括一個辦法調用,調用委托的次數與調用辦法的次數相反,假如要調用多個辦法,就需求屢次給委托賦值,然後調用這個委托。

委托也可以包括多個辦法,這時分要向委托對象中添加多個辦法,這種委托稱為多播委托,多播委托有一個辦法列表,假如調用多播委托,就可以延續調用多個辦法,即先執行某一個辦法,等該辦法執行完成之後再執行另外一個辦法,這些辦法的參數都是一樣的,這些辦法的執行是在一個線程中執行的,而不是每個辦法都是一個線程,最終將執行完成一切的辦法。

假如運用多播委托,就應留意對同一個委托調用辦法鏈的順序並未正式定義,調用順序是不確定的,不一定是依照添加辦法的順序來調用辦法,因而應防止編寫依賴於以特定順序調用辦法的代碼。假如要想確定順序,那麼只能是單播委托,調用委托的次數與調用辦法的次數相反。

多播委托的各個辦法簽名最好是前往void;否則,就只能失掉委托最後調用的一個辦法的後果,而最後調用哪個辦法是無法確定的。

多播委托的每一個辦法都要與委托所限定的辦法的前往值、參數婚配,否則就會有錯誤。

我自己寫代碼測試,測試的後果目前都是調用順序和參加委托的順序相反的,但是不掃除有不同的時分。 

delegate result-type Identifier ([parameters]); 

14.1.5.1、委托運算符 =

Identifier  objectName  =  new  Identifier( functionName);

或許

Identifier  objectName  =  functionName;

這裡的“=”號表示清空 objectName 的辦法列表,然後將 functionName 參加到 objectName 的辦法列表中。

14.1.5.2、委托運算符 +=

objectName  +=  new  Identifier( functionName1);

或許

objectName  +=  functionName1;

這裡的“+=”號表示在原有的辦法列表不變的狀況下,將 functionName1  參加到 objectName 的辦法列表中。可以在辦法列表中加上多個相反的辦法,執行的時分也會執行完一切的函數,哪怕有相反的,就會屢次執行同一個辦法。

留意:objectName 必需是曾經賦值了的,否則在定義的時分直接運用該符號:

Identifier  objectName    +=  new  Identifier( functionName1);或許

Identifier  objectName  +=  functionName1;就會報錯。

14.1.5.3、委托運算符 -=:

objectName  -=  new  Identifier( functionName1);

或許

objectName  -=  functionName1;

這裡的“-=”號表示在 objectName 的辦法列表中減去一個functionName1。可以在辦法列表中屢次減去相反的辦法,減一次只會減一個辦法,假如列表中無此辦法,那麼減就沒有意義,對原有列表無影響,也不會報錯。

留意:objectName 必需是曾經賦值了的,否則在定義的時分直接運用該符號:

Identifier  objectName    -=  new  Identifier( functionName1);或許

Identifier  objectName  -=  functionName1;就會報錯。

14.1.5.4、委托運算符 +、-:

Identifier  objectName  =  objectName  + functionName1 - functionName1;或許

Identifier  objectName  =  new  Identifier( functionName1) + functionName1 - functionName1;

關於這種+、-表達式,在第一個符號+或許-的後面必需是委托而不能是辦法,前面的+、-左右都隨意。這個不是相對規律,還有待進一步的研討。

14.1.5.5、多播委托的異常處置

經過一個委托調用多個辦法還有一個大問題。多播委托包括一個逐一調用的委托集合。假如經過委托調用的一個辦法拋出了異常,整個迭代就會中止。上面是MulticastIteration示例。其中定義了一個復雜的委托DemoDelegate,它沒有參數,前往void。這個委托調用辦法One()和Two(),這兩個辦法滿足委托的參數和前往類型要求。留意辦法One()拋出了一個異常:

using System;

namespace Wrox.ProCSharp.Delegates

{

public delegate void DemoDelegate();

class Program

{

static void One()

{

Console.WriteLine("One");

throw new Exception("Error in one");

}

static void Two()

{

Console.WriteLine("Two");

}

在Main()辦法中,創立了委托d1,它援用辦法One(),接著把Two()辦法的地址添加到同一個委托中。調用d1委托,就可以調用這兩個辦法。異常在try/catch塊中捕捉:

static void Main()

{

DemoDelegate d1 = One;

d1 += Two;

try

{

d1();

}

catch (Exception)

{

Console.WriteLine("Exception caught");

}

}

}

}

委托只調用了第一個辦法。第一個辦法拋出了異常,所以委托的迭代會中止,不再調用Two()辦法。當調用辦法的順序沒有指定時,後果會有所不同。

One

Exception Caught

留意:

多播委托包括一個逐一調用的委托集合。假如經過委托調用的一個辦法拋出了異常,整個迭代就會中止。即假如任一辦法引發了異常,而在該辦法內未捕捉該異常,則該異常將傳遞給委托的調用方,並且不再對調用列表中前面的辦法停止調用。

在這種狀況下,為了防止這個問題,應手動迭代辦法列表。Delegate類定義了辦法GetInvocationList(),它前往一個Delegate對象數組。如今可以運用這個委托調用與委托直接相關的辦法,捕捉異常,並持續下一次迭代。

static void Main()

{

DemoDelegate d1 = One;

d1 += Two;

Delegate[] delegates = d1.GetInvocationList();

foreach (DemoDelegate d in delegates)

{

try

{

d();

}

catch (Exception)

{

Console.WriteLine("Exception caught");

}

}

}

修正了代碼後運轉使用順序,會看到在捕捉了異常後,將持續迭代下一個辦法。

One

Exception caught

Two

留意:其實假如在多播委托的每個詳細的辦法中捕捉異常,並在外部處置,而不拋出異常,一樣能完成多播委托的一切辦法執行終了。這種方式與下面方式的區別在於這種方式的宜昌市在函數外部處置的,下面那種方式的異常是在函數裡面捕捉並處置的。

14.1.6、經過委托對象來調用它所指向的函數

1、委托實例的稱號,前面的括號中應包括調用該委托中的辦法時運用的參數。

2、調用委托對象的Invoke()辦法,Invoke前面的括號中應包括調用該委托中的辦法時運用的參數。

留意:實踐上,給委托實例提供括號與調用委托類的Invoke()辦法完全相反。由於Invoke()辦法是委托的同步伐用辦法。

 

留意:不論是多播委托還是單播委托,在沒有特殊處置的狀況下,在一個線程的執行進程中去調用委托(委托對象所指向的函數),調用委托的執行是不會新起線程的,這個執行還是在原線程中的,這個關於事情也是一樣的。當然,假如是在委托所指向的函數外面去啟動一個新的線程那就是另外一回事了。

14.2、事情

14.2.1、自定義事情

14.2.1.1、聲明一個委托:

Delegate result-type delegateName ([parameters]);

這個委托可以在類A內定義也可以在類A外定義。

14.2.1.2、聲明一個基於某個委托的事情

Event delegateName  eventName;

eventName不是一個類型,而是一個詳細的對象,這個詳細的對象只能在類A內定義而不能在類A外定義。

14.2.1.3、在類A中定義一個觸發該事情的辦法

ReturnType  FunctionName([parameters])

{

 ……

If(eventName != null)

{

eventName([parameters]);

或許eventName.Invoke([parameters]);

}

……

}

觸發事情之後,事情所指向的函數將會被執行。這種執行是經過事情稱號來調用的,好像委托對象名一樣的。

觸發事情的辦法只能在A類中定義,事情的實例化,以及實例化之後的完成體都只能在A類外定義。

14.2.1.4、初始化A類的事情

在類B中定義一個類A的對象,並且讓類A對象的那個事情指向類B中定義的辦法,這個辦法要與事情關聯的委托所限定的辦法吻合。

14.2.1.5、觸發A類的事情

在B類中去調用A類中的觸發事情的辦法:用A類的對象去調用A類的觸發事情的辦法。

14.2.1.6、順序實例

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;

using System.Windows.Forms;

namespace DelegateStudy

{

public delegate void DelegateClick (int a);

public class butt

    {

public event DelegateClick Click;

public void OnClick(int a)

    {

if(Click != null)

    Click.Invoke(a);

  //Click(a);//這種方式也是可以的

MessageBox.Show("Click();");

    }

    }

class Frm

    {

public static void Btn_Click(int a)

    {

for (long i = 0; i < a; i++)

Console.WriteLine(i.ToString());

    }

static void Main(string[] args)

    {

butt b = new butt();

  //在委托中,委托對象假如是null的,直接運用+=符號,會報錯,但是在事情中,初始化的時分,只能用+=

    b.Click += new DelegateClick (Fm_Click); //事情是基於委托的,所以委托推斷一樣適用,上面的語句一樣無效:b.Click += Fm_Click;

//b.Click(10);錯誤:事情“DelegateStudy.butt.Click”只能呈現在 += 或 -= 的右邊(從類型“DelegateStudy.butt”中運用時除外)

    b.OnClick (10000);   

MessageBox.Show("sd234234234");

Console.ReadLine();

    }

   }

}

14.2.2、控件事情

基於Windows的使用順序也是基於音訊的。這闡明,使用順序是經過Windows來與用戶通訊的,Windows又是運用預定義的音訊與使用順序通訊的。這些音訊是包括各種信息的構造,使用順序和Windows運用這些信息決議下一步的操作。

比方:當用戶用鼠標去點擊一個windows使用順序的按鈕的時分,windows操作零碎就會捕捉到這個點擊按鈕的舉措,這個時分它會依據捕捉到的舉措發送一個與之對應的預定義的音訊給windows使用順序的這個按鈕,windows使用順序的按鈕音訊處置順序會處置接納到的音訊,這個順序處置進程就是依據收到的音訊去觸發相應的事情,事情被按鈕觸發後,會告訴一切的該事情的訂閱者來接納這個事情,從而執行相應的的函數。

在MFC等庫或VB等開發環境推出之前,開發人員必需處置Windows發送給使用順序的音訊。VB和明天的.NET把這些傳送來的音訊封裝在事情中。假如需求呼應某個音訊,就應處置對應的事情。

14.2.2.1、控件事情委托EventHandler

在控件事情中,有很多的委托,在這裡引見一個最常用的委托EventHandler,.NET Framework中控件的事情很多都基於該委托,EventHandler委托已在.NET Framework中定義了。它位於System命名空間:

Public delegate void EventHandler(object sender,EventArgs e);

14.2.2.2、委托EventHandler參數和前往值

事情最終會指向一個或許多個函數,函數要與事情所基於的委托婚配。事情所指向的函數(事情處置順序)的命名規則:依照商定,事情處置順序應遵照“object_event”的命名商定。object就是引發事情的對象,而event就是被引發的事情。從可讀性來看,應遵照這個命名商定。

首先,事情處置順序總是前往void,事情處置順序不能有前往值。其次是參數,只需是基於EventHandler委托的事情,事情處置順序的參數就應是object和EventArgs類型:

第一個參數接納引發事情的對象,比方當點擊某個按鈕的時分,這個按鈕要觸發單擊事情最終執行這個函數,那麼就會把以後按鈕傳給sender,當有多個按鈕的單擊事情都指向這個函數的時分,sender的值就取決於以後被單擊的那個按鈕,所以可以為幾個按鈕定義一個按鈕單擊處置順序,接著依據sender參數確定單擊了哪個按鈕:

if(((Button)sender).Name =="buttonOne")

第二個參數e是包括有關事情的其他有用信息的對象。

14.2.2.3、控件事情的其他委托

控件事情還有其他的委托,比方在窗體上有與鼠標事情關聯的委托:

Public delegate void MouseEventHandler(object sender,MouseEventArgs e);

public event MouseEventHandler MouseDown;

this.MouseDown += new System.Windows.Forms.MouseEventHandler(this.Form1_MouseDown);

private void Form1_MouseDown(object sender, MouseEventArgs e){};

MouseDown事情運用MouseDownEventArgs,它包括鼠標的指針在窗體上的的X和Y坐標,以及與事情相關的其他信息。

控件事情中,普通第一個參數都是object sender,第二個參數可以是恣意類型,不同的委托可以有不同的參數,只需它派生於EventArgs即可。

14.2.2.4、順序實例

using System;

using System.Collections.Generic;

using System.Text;

using System.Threading;

 

namespace SecondChangeEvent1

{

// 該類用來存儲關於事情的無效信息外,

// 還用來存儲額定的需求傳給訂閱者的Clock形態信息

public class TimeInfoEventArgs : EventArgs

    {

public TimeInfoEventArgs(int hour,int minute,int second)

    {

this.hour = hour;

this.minute = minute;

this.second = second;

    }

public readonly int hour;

public readonly int minute;

public readonly int second;

    }

 

// 定義名為SecondChangeHandler的委托,封裝不前往值的辦法,

// 該辦法帶參數,一個clock類型對象參數,一個TimeInfoEventArgs類型對象

public delegate void SecondChangeHandler(

   object clock,

   TimeInfoEventArgs timeInformation

    );

// 被其他類察看的鐘(Clock)類,該類發布一個事情:SecondChange。察看該類的類訂閱了該事情。

public class Clock

    {

// 代表小時,分鐘,秒的公有變量

int _hour;

 

public int Hour

    {

get { return _hour; }

set { _hour = value; }

    }

private int _minute;

 

public int Minute

    {

get { return _minute; }

set { _minute = value; }

    }

private int _second;

 

public int Second

    {

get { return _second; }

set { _second = value; }

    }

 

// 要發布的事情

public event SecondChangeHandler SecondChange;

 

// 觸發事情的辦法

protected void OnSecondChange(

   object clock,

   TimeInfoEventArgs timeInformation

    )

    {

// Check if there are any Subscribers

if (SecondChange != null)

    {

// Call the Event

    SecondChange(clock, timeInformation);

    }

    }

 

// 讓鐘(Clock)跑起來,每隔一秒鐘觸發一次事情

public void Run()

    {

for (; ; )

    {

// 讓線程Sleep一秒鐘

Thread.Sleep(1000);

 

// 獲取以後時間

    System.DateTime dt = System.DateTime.Now;

 

// 假如秒鐘變化了告訴訂閱者

if (dt.Second != _second)

    {

// 發明TimeInfoEventArgs類型對象,傳給訂閱者

TimeInfoEventArgs timeInformation =

   new TimeInfoEventArgs(

   dt.Hour, dt.Minute, dt.Second);

 

// 告訴訂閱者

    OnSecondChange(this, timeInformation);

    }

 

// 更新形態信息

    _second = dt.Second;

    _minute = dt.Minute;

    _hour = dt.Hour;

 

    }

    }

    }

 

 

/* ======================= Event Subscribers =============================== */

 

// 一個訂閱者。DisplayClock訂閱了clock類的事情。它的任務是顯示以後時間。

public class DisplayClock

    {

// 傳入一個clock對象,訂閱其SecondChangeHandler事情

public void Subscribe(Clock theClock)

    {

    theClock.SecondChange +=

   new SecondChangeHandler(TimeHasChanged);

    }

 

// 完成了委托婚配類型的辦法

public void TimeHasChanged(

   object theClock, TimeInfoEventArgs ti)

    {

 

Console.WriteLine("Current Time: {0}:{1}:{2}",

   ti.hour.ToString(),

   ti.minute.ToString(),

   ti.second.ToString());

    }

    }

 

// 第二個訂閱者,他的任務是把以後時間寫入一個文件

public class LogClock

    {

public void Subscribe(Clock theClock)

    {

    theClock.SecondChange +=

   new SecondChangeHandler(WriteLogEntry);

    }

 

// 這個辦法原本應該是把信息寫入一個文件中

// 這裡我們用把信息輸入控制台替代

public void WriteLogEntry(

   object theClock, TimeInfoEventArgs ti)

    {

Clock a = (Clock)theClock;

Console.WriteLine("Logging to file: {0}:{1}:{2}",

   a.Hour.ToString(),

   a.Minute.ToString(),

   a.Second.ToString());

    }

    }

 

/* ======================= Test Application =============================== */

 

// 測試擁有順序

public class Test

    {

public static void Main()

    {

// 創立clock實例

Clock theClock = new Clock();

 

// 創立一個DisplayClock實例,讓其訂閱下面創立的clock的事情

DisplayClock dc = new DisplayClock();

    dc.Subscribe(theClock);

 

// 創立一個LogClock實例,讓其訂閱下面創立的clock的事情

LogClock lc = new LogClock();

    lc.Subscribe(theClock);

 

// 讓鐘跑起來

    theClock.Run();

    }

    }

}

14. 3、小結

(1)、在定義事情的那個類A外面,可以恣意的運用事情名,可以觸發;在別的類外面,事情名只能呈現在 += 或 -= 的右邊來指向函數,即只能實例化,不能直接用事情名觸發。但是可以經過A類的對象來調用A類中的觸發事情的函數。這是獨一觸發事情的方式。

(2)、不論是多播委托還是單播委托,在沒有特殊處置的狀況下,在一個線程的執行進程中去調用委托(委托對象所指向的函數),調用委托的執行是不會新起線程的,這個執行還是在原線程中的,這個關於事情也是一樣的。當然,假如是在委托所指向的函數外面去啟動一個新的線程那就是另外一回事了。

(3)、事情是針對某一個詳細的對象的,普通在該對象的所屬類A中寫壞事件,並且寫好觸發事情的辦法,那麼這個類A就是事情的發布者,然後在別的類B外面定義A的對象,並去初始化該對象的事情,讓事情指向B類中的某一個詳細的辦法,B類就是A類事情的訂閱者。當經過A類的對象來觸發A類的事情的時分(只能A類的對象來觸發A類的事情,別的類的對象不能觸發A類的事情,只能訂閱A類的事情,即實例化A類的事情),作為訂閱者的B類會接納A類觸發的事情,從而使得訂閱函數被執行。一個發布者可以有多個訂閱者,當發布者發送事情的時分,一切的訂閱者都將接納到事情,從而執行訂閱函數,但是即便是有多個訂閱者也是單線程。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved