委托與事件 在.net的爭霸戰 ,你選擇了誰?(異步委托產生的原因)


如果你對委托和事件尚有模糊的地方請參閱上2篇博文。

如果你對下面8個問題,可以輕而易舉的回答,那博文對你沒什么作用。

1.為什么在發布者與訂閱者的模式中,我們使用了事件而不使用委托變量?

2.為什么我們通常的多播委托的返回類型都是void?

3.如何讓事件只允許一個方法注冊?

4.非void多播委托如何返回多個返回值?

5.當委托鏈表的注冊方法異常時,如何解決?

6.如何解決事件中的委托方法的延時效果?

7.實現異步委托...?

8.保密

<磨刀>

理清思路:
委托 好比中介所,你在我這里注冊了方法,我就代替你完成任務。
事件 好比微博,凡是收聽我微博的人,只要我更新了微博(自己觸發什么條件),收聽我的人就會知道我更新了,以便做出自己的動作(評論/轉發)。
即:事件必須自己觸發。

 

<正文>
下面來解決疑問:
1.為什么在發布者與訂閱者的模式中,我們使用了事件而不使用委托變量?

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub sub = new Sub();
pub.Display += sub.OnDisplay;
pub.ChangeNumber();
pub.Display(100);

}
}
public delegate void DisplayEventHandle(int number);
class Pub
{
public DisplayEventHandle Display;

public void ChangeNumber()
{
for (int i = 0; i < 10; i++)
{
if (i == 2)
{
if (Display != null)//如果注冊方法
{
Display(i);
}
}
}
}
}

class Sub
{
public void OnDisplay(int i)
{
Console.WriteLine("The number is {0}",i);
}
}

分析:("="是賦值操作,"+="是注冊方法)
.對於使用委托變量,那么在類的內部 委托變量(字段)必須是Public,這樣不安全。
. pub.Display(100);事件本來是需要調用ChangeNumber()方法當i=2的時候觸發的,然后訂閱者作出一系列的動作,但是現在 pub.Display(100);委托自己就可以調用訂閱者的動作,影響到了所有訂閱者。

修改下代碼:
 

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub sub = new Sub();
pub.Display += sub.OnDisplay;
pub.ChangeNumber();
//pub.Display(100); 報錯

}
}
public delegate void DisplayEventHandle(int number);
class Pub
{
public event DisplayEventHandle Display;

public void ChangeNumber()
{
for (int i = 0; i < 10; i++)
{
if (i == 2)
{
if (Display != null)//如果注冊方法
{
Display(i);
}
}
}
}
}

class Sub
{
public void OnDisplay(int i)
{
Console.WriteLine("The number is {0}",i);
}
}

分析:
.對象再也無法直接調用委托變量了,因為加了event事件,本質是生成了 私有private的委托。所以無法進行賦值操作,也不能直接調用。
.這樣給客戶端 少了一些使用類內部變量的權力。

2.為什么我們通常的多播委托的返回類型都是void?
對於單播委托,咱們不討論了,太簡單了。
對於多播委托:
委托變量 +=方法1;
委托變量 +=方法2;

試想下,咱們都知道委托變量的聲明,參數和返回類型都是和方法一樣的,那么委托調用方法的時候,如果有返回值,到底這個返回值是 方法1,還是方法2的。

比如:拿上面的微博案例來說,我是發布者,我可能今天心情不好,然后發布了一條微博,我根本不關心,誰收聽我,也不關心收聽者對我評論。即:事件發布者,他運行了某個動作之后,如果這個動作內的條件滿足,那么就觸發 訂閱者的動作。事件發布者根本不需要關心,你訂閱者的返回值。

但是,你要說,我就是想知道返回值是多少?好,我們做個測試:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += sub1.OnDisplay;
pub.Display += sub2.OnDisplay;
pub.ChangeNumber();//I am Sub2

}
}
public delegate string DisplayEventHandle();
class Pub
{
public event DisplayEventHandle Display;

public void ChangeNumber()
{
for (int i = 0; i < 10; i++)
{
if (i == 2)
{
if (Display != null)//如果注冊方法
{
string str=Display();
Console.WriteLine(str);
}
}
}
}
}

class Sub1
{
public string OnDisplay()
{
return "I am Sub1";
}
}

class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}



答案是:最后一個訂閱者的返回值。

3.如何讓事件只允許一個方法注冊?
可能有時候,我的微博只想讓一個人收聽,不想讓 前任女友收聽,怎么辦?
從技術層面分析:
1.我們訂閱的時候,如何追加訂閱者 通過符號"+="是吧?但是我們說過,“=”符號也可以委托定義,從這個角度着手,可不可以讓事件變成不可訪問的(private),然后在內部利用“=”進行訂閱,而不是讓客戶自己“+=”符號訂閱。

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.AddDL(sub1.OnDisplay);
pub.AddDL(sub2.OnDisplay);
pub.Speaking();


}
}
public delegate string DisplayEventHandle();
class Pub
{
private event DisplayEventHandle Display;

public void AddDL(DisplayEventHandle method)
{
Display = method;//這里是"="不是"+="
}
public void RemoveDL(DisplayEventHandle method)
{
Display -= method;
}
public void Speaking()
{
if (Display != null)
{
string str = Display();
Console.WriteLine(str);
}
}
}

class Sub1
{
public string OnDisplay()
{
return "I am Sub1";
}
}

class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}


分析:
.有朋友可能會問,這樣不是和剛才一樣嗎?帶返回值的委托變量,其實不是的,當你使用+=的時候,2個方法都會被加入委托鏈表,而使用"="只是覆蓋。
.有點類似於屬性,對,就是 事件訪問器。
如下代碼:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
//pub.Display = sub1.OnDisplay;//錯誤,因為Display本質還是私有的委托,只是這里定義了2個委托
pub.Display += sub1.OnDisplay;
pub.Display += sub2.OnDisplay;//覆蓋sub1方法
pub.Speaking();

}
}
public delegate string DisplayEventHandle();
class Pub
{
private DisplayEventHandle display;
public event DisplayEventHandle Display
{
add { display = value; }
remove { display -= value; }
}
public void Speaking()
{
if (display != null)
{
string str = display();
Console.WriteLine(str);
}
}
}

class Sub1
{
public string OnDisplay()
{
return "I am Sub1";
}
}

class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}



分析:
1.通過分析,發現事件的本質是 生成一個private的委托變量,這就是為什么 無法通過 事件變量=方法 操作的原因,因為這個變量是委托的變量無法訪問。但是可以通過事件訪問器訪問,修改事件訪問器可以限制方法進入 委托鏈表。


4.非void委托如何返回多個返回值?
思路:
1.我們知道 多播委托注冊方法之后,會生產一個委托鏈表,那么我們可以把這個委托的注冊方法遍歷出來嗎?

View Code
public delegate string DL();
class Program
{
static void Main(string[] args)
{
DL one = DoSomething;
one += Attacking;
//delegate[] dls; 這樣是錯誤的,下面解釋了
Delegate[] dlArray =one.GetInvocationList();
foreach(var n in dlArray)
{
Console.WriteLine(n.Method.Name);//遍歷方法名字
}


}

static string DoSomething()
{
return "do it";
}

static string Attacking()
{
return "Attack";
}
}


首先,我們要先理清出一個概念:
delegate 與Delegate有什么區別?
Delegate:是一個抽象基類,它引用靜態方法或引用類實例及該類的實例方法。然而,只有系統和編譯器可以顯式地從 Delegate 類派生出委托類型。
MulticastDelegate:是一個繼承於Delegate的類,其擁有一個帶有鏈表格式的委托列表,該列表稱為調用列表,在調用多路廣播委托時,將按照調用列表中的委托出現的順序來同步調用這些委托。平常我們聲明一個delegate的類型,都是繼承於MulticastDelegate類的(注意:不能顯式地從此類進行派生。這點與Delegate類是一樣的,只有系統和編譯器也可以顯示地進行派生)。
delegate 是一個C#關鍵字,用來定義一個新的委托類型(繼承自MulticastDelegate類)。

2.方法名遍歷出來了,咱們利用List<>把每個方法的結果遍歷出來

View Code
public delegate string DL();
class Program
{
static void Main(string[] args)
{
DL one = DoSomething;
one += Attacking;
//delegate[] dls; 這樣是錯誤的,下面解釋了
Delegate[] dlArray =one.GetInvocationList();
List<string> lists = new List<string>();
foreach(var n in dlArray)
{
Console.WriteLine(n.Method.Name);
DL newone = (DL)n;//把Delegate顯示轉換成DL類型,因為DL類的基類是Delegate類
string str = newone();
lists.Add(str);
}
foreach (var n in lists)
{
Console.WriteLine(n);
}
}
static string DoSomething()
{
return "do it";
}

static string Attacking()
{
return "Attack";
}
}



應用於 事件中:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display +=new DisplayEventHandle(sub1.OnDisplay);//注冊方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注冊方法

List<string> lists=pub.Doing();//事件由發布者的某個條件觸發
foreach (var n in lists)
{
Console.WriteLine(n);
}


}
}
public delegate string DisplayEventHandle();

class Pub //發布者
{
public event DisplayEventHandle Display;
List<string> lists = new List<string>();
Delegate[] dls;
public List<string> Doing()
{
if (Display != null)
{
dls = Display.GetInvocationList();
foreach (var n in dls)
{
Console.WriteLine(n.Method.Name);
DisplayEventHandle one=(DisplayEventHandle)n;
string str = one();
lists.Add(str);
}
}

return lists;
}

}

class Sub1//訂閱者
{
public string OnDisplay()
{
return "I am Sub1";
}
}
class Sub2
{
public string OnDisplay()
{
return "I am Sub2";
}
}
}


事實上,發布者根不關心這些訂閱者返回什么,它關心的是訂閱者注冊的方法是否正確,是否會報錯,影響發布者的方法執行和后面訂閱者方法的執行,所以 這種技術主要用於 返回 訂閱者方法的異常處理信息。

5.當委托鏈表的注冊方法異常時,如何解決?

源代碼:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += new DisplayEventHandle(sub1.OnDisplay);//注冊方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注冊方法

pub.Doing();//事件由發布者的某個條件觸發

}
}
public delegate string DisplayEventHandle(object sender, EventArgs e);

class Pub //發布者
{
public event DisplayEventHandle Display;

public void Doing()
{
if (Display != null)
{
Display(this,EventArgs.Empty);
}


}

}

class Sub1//訂閱者
{
public string OnDisplay(object sender, EventArgs e)
{
return "I am Sub1";
}
}
class Sub2
{
public string OnDisplay(object sender, EventArgs e)
{
return "I am Sub2";
}
}



思考:如果訂閱者方法異常了怎么辦?對,我們利用try catch調試。
修改代碼:

View Code
public void Doing()
{
if (Display != null)
{
try
{
string str=Display(this, EventArgs.Empty);
Console.WriteLine(str);
}
catch (Exception e)
{
Console.WriteLine(e.Message.ToString());
}
}
}

class Sub1//訂閱者
{
public string OnDisplay(object sender, EventArgs e)
{
//return "I am Sub1";
throw new Exception("sub1方法異常了");
}
}



如果Sub1方法出了異常的話,那么就會終止 對 Sub2方法的調研,雖然 Doing()可以執行下去了。但是 影響了其他訂閱者。
從這個層面思考,我們把 注冊的方面 按照上面提到過的遍歷一下,就能解決,因為在Foreach循環內當一個方法出了問題,只影響到問題方法本身。

修改代碼如下:
 

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display +=new DisplayEventHandle(sub1.OnDisplay1);//注冊方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay2);//注冊方法

List<string> lists=pub.Doing();//事件由發布者的某個條件觸發
foreach (var n in lists)
{
Console.WriteLine(n);
}


}
}
public delegate string DisplayEventHandle(object sender,EventArgs e);

class Pub //發布者
{
public event DisplayEventHandle Display;
List<string> lists = new List<string>();
Delegate[] dls;
public List<string> Doing()
{
if (Display != null)
{
dls = Display.GetInvocationList();
foreach (var n in dls)
{
try
{
Console.WriteLine(n.Method.Name);
DisplayEventHandle one = (DisplayEventHandle)n;
string str = one(this, EventArgs.Empty);
lists.Add(str);
}
catch (Exception e)
{
Console.WriteLine(e.Message.ToString());
}
}
}

return lists;
}

}

class Sub1//訂閱者
{
public string OnDisplay1(object sender,EventArgs e)
{
throw new Exception("Sub1方法異常了");
}
}
class Sub2
{
public string OnDisplay2(object sender, EventArgs e)
{
return "I am Sub2";
}
}



輸出:
OnDisplay1
Sub1方法異常了
OnDisplay2
I am Sub2

總結:這樣即知道了哪個方法異常了,又不影響其他訂閱者調用自己的方法。


6.如何處理事件中的委托方法的超時?
上面可知,訂閱者的注冊方法如果有問題,會導致異常,然后影響到發布者的Doing()方法,還有一種讓到發布者的Doing()方法經過很長時間執行的,就是超時。
但是超時不會影響發布者把 訂閱者感興趣的信息發布給訂閱者,也不影響發布者的正常執行,只是執行Doing()會很長時間而已。

分析下:
1.發布者  執行某個動作的時候(事件由發布者自己觸發),根據訂閱者感興趣的信息會調用 訂閱者的注冊方法(比如 當一個數字大於10的時候)。
2.我們按F11調試的時候都會發現,當觸發事件的時候,就會轉到 訂閱者的內部方法上去,也就是說,當前線程在 執行 訂閱者的方法,所以 Main函數內部的客戶端就在等待方法執行完畢之后,才能繼續下面的代碼操作。

這里有點深度的:我舉個例子
當Main函數的在執行一個發布者的方法的時候
比如 計算1-100的和,如果一個感興趣的參數是和,當和大於10的時候,這個時候,線程就會轉到 訂閱者的方法上去,這個時候,客戶端(Main函數的方法還能執行嗎?)顯然不可以繼續執行了,必須等待訂閱者執行完,才能繼續下面的計算操作。

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += new DisplayEventHandle(sub1.OnDisplay1);//注冊方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay2);//注冊方法

List<string> lists=pub.Doing();//事件由發布者的某個條件觸發
foreach (var n in lists)
{
Console.WriteLine(n);
}

Console.WriteLine("線程已經回到Main函數");


}
}
public delegate string DisplayEventHandle(object sender,EventArgs e);

class Pub //發布者
{
public event DisplayEventHandle Display;
List<string> lists = new List<string>();
Delegate[] dls;
public List<string> Doing()
{
if (Display != null)
{
dls = Display.GetInvocationList();
foreach (var n in dls)
{

Console.WriteLine("現在是方法:"+n.Method.Name);
DisplayEventHandle one = (DisplayEventHandle)n;
string str = one(this, EventArgs.Empty);
lists.Add(str);


}
}

return lists;
}

}

class Sub1//訂閱者
{
public string OnDisplay1(object sender,EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(5));
return "線程已轉到Sub1,等待5秒,Sub1方法執行";
}
}
class Sub2
{
public string OnDisplay2(object sender, EventArgs e)
{
return "線程已轉到Sub2,Sub2方法執行。。。";
}
}



輸出:
現在是方法:OnDisplay1
現在是方法:OnDisplay2
線程已轉到Sub1,等待5秒,Sub1方法執行
線程已轉到Sub2,Sub2方法執行。。。
線程已經回到Main函數


我們是發布者,我們需要是立刻輸出:  線程已經回到Main函數,而訂閱者的超時影響了我發布者的延遲輸出。
還是拿微博來說:我是博主,我發微博就是抒發感情,和誰收聽我,以及收聽到我的信息沒有以及如何對我做出反應都不關心。
但是現在,我必須 等待訂閱者 方法結束了,我才可以操作,太讓人生氣了,為了解決這個問題。
怎么辦?怎么辦?IL看結構:
 


分析:
1.事件的本質我們知道,是生成 一個 private的委托變量
2.委托的本質我們知道,是生成 一個完整的繼承與MulticastDelegate的類,委托本質是個類
這個類里包括:
BeginInvoke()、EndInvoke()、Invoke()3個方法。
我們記得要調用委托方法的時候是這樣操作的:
委托變量();其實 實質就是 委托變量.Invoke();
對,就是這個方法是凶手,他妨礙了我們的發布者,讓我們等待。

KO它,開始 異步委托

7.實現異步委托...?
異步就是 一個主線程執行了(Main函數),你要是委托調用方法,那是你的事情,你自己重新開辟新線程去搞,別影響我的主線程。
異步一般是 Begin 和End 出現。

1.BeginInvoke()執行時,從線程池抓取一個 "沒事干的"的線程來替我去告訴 訂閱者調用委托方法。
注:對於調用BeginInvoke()方法的時候,讓線程去調用委托方法,這個委托變量必須只能有一個1個方法被綁定,如果是多播委托,必須像上面那么GetInvocationList()獲得所有委托對象,先遍歷出所有委托對象,再使用BeginInvoke()方法。
2.Main()繼續自己的線程執行下面的工作
3.EndInvoke();當訂閱者方法異常的時候,我們知道可以try catch捕獲,但是只有在EndInvoke()才會拋出。(其實發布者並不關心這些拋出異常的信息),並且拋出異常也是在另一個進程上。

好了,開始寫代碼:

View Code
class Program
{
static void Main(string[] args)
{
Pub pub = new Pub();
Sub1 sub1 = new Sub1();
Sub2 sub2 = new Sub2();
pub.Display += new DisplayEventHandle(sub1.OnDisplay);//注冊方法
pub.Display += new DisplayEventHandle(sub2.OnDisplay);//注冊方法

pub.Doing();//事件由發布者的某個條件觸發
Console.WriteLine("線程還在Main()");

Console.ReadKey();//為什么這樣寫,因為主線程是Main函數,當他執行完之后,程序就結束了,可能子線程還沒結束呢

}
}
public delegate void DisplayEventHandle(object sender, EventArgs e);

class Pub //發布者
{
public event DisplayEventHandle Display;

public void Doing()
{
if (Display != null)
{
Delegate[] dels = Display.GetInvocationList();
foreach (var n in dels)
{
DisplayEventHandle newone = (DisplayEventHandle)n;
IAsyncResult result = newone.BeginInvoke(this, EventArgs.Empty, null, null);//新開辟一個線程

//newone.EndInvoke(result);//加上這個效果如何?效果還是需要等待訂閱者方法的結果,所以這個是沒要添加的

}
}


}

}

class Sub1//訂閱者
{
public void OnDisplay(object sender, EventArgs e)
{
Thread.Sleep(TimeSpan.FromSeconds(3));
Console.WriteLine("Sub1線程");

}
}
class Sub2
{
public void OnDisplay(object sender, EventArgs e)
{
Console.WriteLine("Sub2線程");

}
}
}



輸出結果:
線程還在Main()
Sub2線程
Sub1線程

總結:和我們預期的一樣效果,注意上面的 Console.ReadKey(),不寫這個,程序運行完成將導致子線程的輸出無法完成。可能難以理解
分析下:
1.對於主線程就是 Main()函數,對於子線程分為 前台線程和后台線程,我們這里的就是后台線程,對於后台線程,只要主線程運行結束程序就結束了,不會管這些后台線程,但是對於前台線程注意了,必須前台線程結束了,主線程才會結束。

線程這塊知識暫時不討論了,會有專門的博文發布。
2.這里是 並行執行的,不要以為因為Foreach遍歷就會先執行Sub1,其實是2個一起執行的。也就是說 后台線程最長利用了3秒鍾。


哈哈,是不是覺得異步調用委托方法學完了?錯,這才剛剛開始:
注意到沒,上面有一個Console.ReadKey();
必須輸入一個 鍵,程序才會退出,所以異步調用就需要更多的控制,比如當后台線程執行完畢了,自動告訴客戶端,我結束了,可以關閉程序了這類問題,比如 客戶端需要 后台線程 執行的結果。

總結:帶着這些問題,將在明天發布《線程與異步調用委托方法的淵源》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM