日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
C#4.0中的協(xié)變和逆變

在上一篇文章中,我們實(shí)現(xiàn)了一個簡單的爬蟲,并指出了這種方式的缺陷?,F(xiàn)在,我們就來看一下,如何使用C# 4.0中所引入的“協(xié)變和逆變”特性來改進(jìn)這種消息執(zhí)行方式,這也是我認(rèn)為在“普適Actor模型”中最合適的做法。這次,我們動真格的了,我們會一條一條地改進(jìn)前文提出的缺陷。

協(xié)變和逆變
在以前的幾篇文章中,我們一直掛在嘴邊的說法便是消息于Actor類型的“耦合”太高。例如在簡單的爬蟲實(shí)現(xiàn)中,Crawler接受的消息為Crawl(Monitor, string),它的第一個參數(shù)為Monitor類型。但是在實(shí)際應(yīng)用中,這個參數(shù)很可能需要是各種類型,唯一的“約束”只是它必須能夠接受一個ICrawlResponseHandler類型的消息,這樣我們就能把抓取的結(jié)果傳遞給它。至于操作Crawler對象的是Monitor還是Robot,還是我們單元測試時動態(tài)創(chuàng)建的Mock對象(這很重要),Crawler一概不會關(guān)心。

但就是這個約束,在以前的實(shí)現(xiàn)中,我們必須讓這個目標(biāo)繼承Actor< ICrawlResponseHandler>,這樣它也就無法接受其他類型的消息了。例如Monitor還要負(fù)責(zé)一些查詢操作我們該怎么辦呢?幸運(yùn)的是,在.NET 4.0(C# 4.0)中,我們只需要讓這個目標(biāo)實(shí)現(xiàn)這樣一個接口即可:

 
 
 
 
  1. public interface IPort< out T>
  2. {
  3.     void Post(Action< T> message);
  4. }

瞅到out關(guān)鍵字了沒?事實(shí)上,還有一個東西您在這里還沒有看到,這便是Action委托在.NET 4.0中的簽名:

 
 
 
 
  1. public delegate void Action< in T>(T obj);

就在這樣一個簡單的示例中,協(xié)變和逆變所需要的in和out都出現(xiàn)了。這意味著如果有兩個類型Parent和Child,其中Child是Parent的子類(或Parent接口的實(shí)現(xiàn)),那么實(shí)現(xiàn)了IPort< Child>的對象便可以自動賦值給IPort< Parent>類型的參數(shù)或引用1。使用代碼來說明問題可能會更清楚一些:

 
 
 
 
  1. public class Parent
  2. {
  3.     public void ParentMethod() { };
  4. }
  5. public class Child : Parent { }
  6. static void Main(string[] args)
  7. {
  8.     IPort< Child> childPort = new ChildPortType();
  9.     IPort< Parent> parentPort = childPort; // 自動轉(zhuǎn)化
  10.     parentPort.Post(p => p.ParentMethod()); // 可以接受Action< Parent>類型作為消息
  11. }

這意味著,我們可以把ICrawlRequestHandler和ICrawlResponseHandler類型寫成下面的形式:

 
 
 
 
  1. internal interface ICrawlRequestHandler
  2. {
  3.     void Crawl(IPort< ICrawlResponseHandler> collector, string url);
  4. }
  5. internal interface ICrawlResponseHandler
  6. {
  7.     void Succeeded(IPort< ICrawlRequestHandler> crawler, string url, string content, List< string> links);
  8.     void Failed(IPort< ICrawlRequestHandler> crawler, string url, Exception ex);
  9. }

如今,Monitor和Crawler便可以寫成如下模樣:

 
 
 
 
  1. internal class Crawler : Actor< Action< Crawler>>, IPort< Crawler>, ICrawlRequestHandler
  2. {
  3.     protected override void Receive(Action< Crawler> message) { message(this); }
  4.     #region ICrawlRequestHandler Members
  5.     void ICrawlRequestHandler.Crawl(IPort< ICrawlResponseHandler> collector, string url)
  6.     {
  7.         try
  8.         {
  9.             string content = new WebClient().DownloadString(url);
  10.             var matches = Regex.Matches(content, @"href=""(http://[^""]+)""").Cast< Match>();
  11.             var links = matches.Select(m => m.Groups[1].Value).Distinct().ToList();
  12.             collector.Post(m => m.Succeeded(this, url, content, links));
  13.         }
  14.         catch (Exception ex)
  15.         {
  16.             collector.Post(m => m.Failed(this, url, ex));
  17.         }
  18.     }
  19.     #endregion
  20. }
  21. public class Monitor : Actor< Action< Monitor>>, IPort< Monitor>, ICrawlResponseHandler
  22. {
  23.     protected override void Receive(Action< Monitor> message) { message(this); }
  24.     #region ICrawlResponseHandler Members
  25.     void ICrawlResponseHandler.Succeeded(IPort< ICrawlRequestHandler> crawler,
  26.         string url, string content, List< string> links) { ... }
  27.     void ICrawlResponseHandler.Failed(IPort< ICrawlRequestHandler> crawler,
  28.         string url, Exception ex) { ... }
  29.     #endregion
  30.     private void DispatchCrawlingTasks(IPort< ICrawlRequestHandler> reusableCrawler)
  31.     {
  32.         if (this.m_readyToCrawl.Count < = 0)
  33.         {
  34.             this.WorkingCrawlerCount--;
  35.         }
  36.         var url = this.m_readyToCrawl.Dequeue();
  37.         reusableCrawler.Post(c => c.Crawl(this, url));
  38.         while (this.m_readyToCrawl.Count > 0 &&
  39.             this.WorkingCrawlerCount <  this.MaxCrawlerCount)
  40.         {
  41.             var newUrl = this.m_readyToCrawl.Dequeue();
  42.             IPort< ICrawlRequestHandler> crawler = new Crawler();
  43.             crawler.Post(c => c.Crawl(this, newUrl));
  44.             this.WorkingCrawlerCount++;
  45.         }
  46.     }
  47. }

Monitor的具體實(shí)現(xiàn)和上篇文章區(qū)別不大,您可以參考文章末尾給出的完整代碼,并配合前文的分析來理解,這里我們只關(guān)注被標(biāo)紅的兩行代碼。

在第一行中我們創(chuàng)建了一個Crawler類型的對象,并把它賦值給IPort< ICrawlerRequestHandler>類型的變量中。請注意,Crawler對象并沒有實(shí)現(xiàn)這個接口,它只是實(shí)現(xiàn)了IPort< Crawler>及ICrawlerRequestHandler。不過由于IPort< T>支持協(xié)變,于是IPort< Crawler>被安全地轉(zhuǎn)換成了IPort< ICrawlerRequestHandler>對象。

第二行中再次發(fā)生了協(xié)變:ICrawlRequestHandler.Crawel的第一個參數(shù)需要IPort< ICrawlResponseHandler>類型的對象,但是this是Monitor類型的,它并沒有實(shí)現(xiàn)這個接口。不過,和上面描述的一樣,由于IPort< T>支持協(xié)變,因此這樣的類型轉(zhuǎn)化是安全的,允許的。于是在Crawler類便可以操作一個“抽象”,而不是具體的Monitor類型來辦事了。

神奇不?但就是這么簡單。

“內(nèi)部”消息控制
在上一篇文章中,我們還提出了Crawler實(shí)現(xiàn)的另一個缺點(diǎn):沒有使用異步IO。WebClient本身的DownloadStringAsync方法可以進(jìn)行異步下載,但是如果在異步完成的后續(xù)操作(如分析鏈接)會在IO線程池中運(yùn)行,這樣我們就很難對任務(wù)所分配的運(yùn)算能力進(jìn)行控制。我們當(dāng)時提出,可以把后續(xù)操作作為消息發(fā)送給Crawler本身,也就是進(jìn)行“內(nèi)部”消息控制——可惜的是,我們當(dāng)時無法做到。不過現(xiàn)在,由于Crawler實(shí)現(xiàn)的是IPort< Crawler>接口,因此,我們可以把Crawler內(nèi)部的任何方法作為消息傳遞給自身,如下:

 
 
 
 
  1. internal class Crawler : Actor< Action< Crawler>>, IPort< Crawler>, ICrawlRequestHandler
  2. {
  3.     protected override void Receive(Action< Crawler> message) { message(this); }
  4.     #region ICrawlRequestHandler Members
  5.     public void Crawl(IPort< ICrawlResponseHandler> collector, string url)
  6.     {
  7.         WebClient client = new WebClient();
  8.         client.DownloadStringCompleted += (sender, e) =>
  9.         {
  10.             if (e.Error == null)
  11.             {
  12.                 this.Post(c => c.Crawled(collector, url, e.Result));
  13.             }
  14.             else
  15.             {
  16.                 collector.Post(c => c.Failed(this, url, e.Error));
  17.             }
  18.         };
  19.         client.DownloadStringAsync(new Uri(url));
  20.     }
  21.     private void Crawled(IPort< ICrawlResponseHandler> collector, string url, string content)
  22.     {
  23.         var matches = Regex.Matches(content, @"href=""(http://[^""]+)""").Cast< Match>();
  24.         var links = matches.Select(m => m.Groups[1].Value).Distinct().ToList();
  25.         collector.Post(c => c.Succeeded(this, url, content, links));
  26.     }
  27.     #endregion
  28. }

我們準(zhǔn)備了一個private的Crawled方法,如果抓取成功了,我們會把這個方法的調(diào)用封裝在一條消息中重新發(fā)給自身。請注意,這是個私有方法,因此這里完全是在做“內(nèi)部”消息控制。

開啟抓取任務(wù)
在上一篇文章中,我們?yōu)镸onitor添加了一個Start方法,它的作用是啟動URL。我們知道,對單個Actor來說消息的處理是線程安全的,但是這個前提是使用“消息”傳遞的方式進(jìn)行通信,如果直接調(diào)用Start公有方法,便會破壞這種線程安全特性。不過現(xiàn)在的Monitor已經(jīng)不受接口的限制,可以自由接受任何它可以執(zhí)行的消息,因此我們只要對外暴露一個Crawl方法即可:

 
 
 
 
  1. public class Monitor : Actor< Action< Monitor>>, IPort< Monitor>,
  2.     ICrawlResponseHandler,
  3.     IStatisticRequestHandelr
  4. {
  5.     ...
  6.     public void Crawl(string url)
  7.     {
  8.         if (this.m_allUrls.Contains(url)) return;
  9.         this.m_allUrls.Add(url);
  10.         if (this.WorkingCrawlerCount <  this.MaxCrawlerCount)
  11.         {
  12.             this.WorkingCrawlerCount++;
  13.             IPort< ICrawlRequestHandler> crawler = new Crawler();
  14.             crawler.Post(c => c.Crawl(this, url));
  15.         }
  16.         else
  17.         {
  18.             this.m_readyToCrawl.Enqueue(url);
  19.         }
  20.     }
  21. }

于是我們便可以向Monitor發(fā)送消息,讓其抓取特定的URL:

 
 
 
 
  1. string[] urls =
  2. {
  3.     "http://www.cnblogs.com/dudu/",
  4.     "http://www.cnblogs.com/TerryLee/",
  5.     "http://www.cnblogs.com/JeffreyZhao/"
  6. };
  7. Random random = new Random(DateTime.Now.Millisecond);
  8. Monitor monitor = new Monitor(10);
  9. foreach (var url in urls)
  10. {
  11.     var urlToCrawl = url;
  12.     monitor.Post(m => m.Crawl(urlToCrawl));
  13.     Thread.Sleep(random.Next(1000, 3000));
  14. }

上面的代碼會每隔1到3秒發(fā)出一個抓取請求。由于我們使用了消息傳遞的方式進(jìn)行通信,因此對于Monitor來說,這一切都是線程安全的。我們可以隨時隨地為Monitor添加抓取任務(wù)。

接受多種消息(協(xié)議)
我們再觀察一下Monitor的簽名:

class Monitor : Actor< Action< Monitor>>, IPort< Monitor>, ICrawlResponseHandler
可以發(fā)現(xiàn),如今的Monitor已經(jīng)和它實(shí)現(xiàn)的協(xié)議沒有一對一的關(guān)系了。也就是說,它可以添加任意功能,可以接受任意類型的消息,我們只要讓它實(shí)現(xiàn)另一個接口即可。于是乎,我們再要一個“查詢”功能2:

 
 
 
 
  1. public interface IStatisticRequestHandelr
  2. {
  3.     void GetCrawledCount(IPort< IStatisticResponseHandler> requester);
  4.     void GetContent(IPort< IStatisticResponseHandler> requester, string url);
  5. }
  6. public interface IStatisticResponseHandler
  7. {
  8.     void ReplyCrawledCount(int count);
  9.     void ReplyContent(string url, string content);
  10. }

為了讓Monior支持查詢,我們還需要為它添加這樣的代碼:

 
 
 
 
  1. public class Monitor : Actor< Action< Monitor>>, IPort< Monitor>,
  2.     ICrawlResponseHandler,
  3.     IStatisticRequestHandelr
  4. {
  5.     ...
  6.     #region IStatisticRequestHandelr Members
  7.     void IStatisticRequestHandelr.GetCrawledCount(IPort< IStatisticResponseHandler> requester)
  8.     {
  9.         requester.Post(r => r.ReplyCrawledCount(this.m_urlContent.Count));
  10.     }
  11.     void IStatisticRequestHandelr.GetContent(IPort< IStatisticResponseHandler> requester, string url)
  12.     {
  13.         string content;
  14.         if (!this.m_urlContent.TryGetValue(url, out content))
  15.         {
  16.             content = null;
  17.         }
  18.         requester.Post(r => r.ReplyContent(url, content));
  19.     }
  20.     #endregion
  21. }

最后,我們來嘗試著使用這個“查詢”功能。首先,我們編寫一個測試用的TestStatisticPort類:

 
 
 
 
  1. public class TestStatisticPort : IPort< IStatisticResponseHandler>, IStatisticResponseHandler
  2. {
  3.     private IPort< IStatisticRequestHandelr> m_statisticPort;
  4.     public TestStatisticPort(IPort< IStatisticRequestHandelr> statisticPort)
  5.     {
  6.         this.m_statisticPort = statisticPort;
  7.     }
  8.     public void Start()
  9.     {
  10.         while (true)
  11.         {
  12.             Console.ReadLine();
  13.             this.m_statisticPort.Post(s => s.GetCrawledCount(this));
  14.         }
  15.     }
  16.     #region IPort< IStatisticResponseHandler> Members
  17.     void IPort< IStatisticResponseHandler>.Post(Action< IStatisticResponseHandler> message)
  18.     {
  19.         message(this);
  20.     }
  21.     #endregion
  22.     #region IStatisticResponseHandler Members
  23.     void IStatisticResponseHandler.ReplyCrawledCount(int count)
  24.     {
  25.         Console.WriteLine("Crawled: {0}", count);
  26.     }
  27.     void IStatisticResponseHandler.ReplyContent(string url, string content) { ... }
  28.     #endregion
  29. }

當(dāng)調(diào)用Start方法時,控制臺將會等待用戶敲擊回車鍵。當(dāng)按下回車鍵時,TestStatisticPort將會向Monitor發(fā)送一個IStatisticRequestHandler.GetCrawledCount消息。Monitor回復(fù)之后,屏幕上便會顯示當(dāng)前已經(jīng)抓取成功的URL數(shù)目。例如,我們可以編寫如下的測試代碼:

 
 
 
 
  1. static void Main(string[] args)
  2. {
  3.     var monitor = new Monitor(5);
  4.     monitor.Post(m => m.Crawl("http://www.cnblogs.com/"));
  5.     TestStatisticPort testPort = new TestStatisticPort(monitor);
  6.     testPort.Start();
  7. }

隨意敲擊幾下回車,結(jié)果如下:

總結(jié)
如今的做法,兼顧了強(qiáng)類型檢查,并使用C# 4.0中的協(xié)變和逆變特性,把上一篇文章中提出的問題解決了,不知您是否理解了這些內(nèi)容?只可惜,我們在C# 3.0中還沒有協(xié)變和逆變。因此,我們還必須思考一個適合C# 3.0的做法。

順便一提,由于F#不支持協(xié)變和逆變,因此本文的做法無法在F#中使用。

注1:關(guān)于協(xié)變和逆變特性,我認(rèn)為腦袋兄的這篇文章講的非常清楚——您看得頭暈了?是的,剛開始了解協(xié)變和逆變,以及它們之間的嵌套規(guī)則時我也頭暈,但是您在掌握之后就會發(fā)現(xiàn),這的確是一個非常有用的特性。

注2:不知您是否發(fā)現(xiàn),與之前internal的Crawl相關(guān)接口不同,Statistic相關(guān)接口是public的。我們在使用接口作為消息時,也可以通過這種辦法來控制哪些消息是可以對外暴露的。這也算是一種額外的收獲吧。


網(wǎng)頁標(biāo)題:C#4.0中的協(xié)變和逆變
URL分享:http://www.5511xx.com/article/dpohose.html