C# 4.0中的协变和逆变

开发 后端
本文介绍如何使用C# 4.0中所引入的“协变和逆变”特性来改进消息执行方式。

在上一篇文章中,我们实现了一个简单的爬虫,并指出了这种方式的缺陷。现在,我们就来看一下,如何使用C# 4.0中所引入的“协变和逆变”特性来改进这种消息执行方式,这也是我认为在“普适Actor模型”中最合适的做法。这次,我们动真格的了,我们会一条一条地改进前文提出的缺陷。

协变和逆变
在以前的几篇文章中,我们一直挂在嘴边的说法便是消息于Actor类型的“耦合”太高。例如在简单的爬虫实现中,Crawler接受的消息为Crawl(Monitor, string),它的第一个参数为Monitor类型。但是在实际应用中,这个参数很可能需要是各种类型,唯一的“约束”只是它必须能够接受一个ICrawlResponseHandler类型的消息,这样我们就能把抓取的结果传递给它。至于操作Crawler对象的是Monitor还是Robot,还是我们单元测试时动态创建的Mock对象(这很重要),Crawler一概不会关心。

但就是这个约束,在以前的实现中,我们必须让这个目标继承Actor< ICrawlResponseHandler>,这样它也就无法接受其他类型的消息了。例如Monitor还要负责一些查询操作我们该怎么办呢?幸运的是,在.NET 4.0(C# 4.0)中,我们只需要让这个目标实现这样一个接口即可:

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

瞅到out关键字了没?事实上,还有一个东西您在这里还没有看到,这便是Action委托在.NET 4.0中的签名:

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


就在这样一个简单的示例中,协变和逆变所需要的in和out都出现了。这意味着如果有两个类型Parent和Child,其中Child是Parent的子类(或Parent接口的实现),那么实现了IPort< Child>的对象便可以自动赋值给IPort< Parent>类型的参数或引用1。使用代码来说明问题可能会更清楚一些:

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


这意味着,我们可以把ICrawlRequestHandler和ICrawlResponseHandler类型写成下面的形式:

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


如今,Monitor和Crawler便可以写成如下模样:

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


Monitor的具体实现和上篇文章区别不大,您可以参考文章末尾给出的完整代码,并配合前文的分析来理解,这里我们只关注被标红的两行代码。

在第一行中我们创建了一个Crawler类型的对象,并把它赋值给IPort< ICrawlerRequestHandler>类型的变量中。请注意,Crawler对象并没有实现这个接口,它只是实现了IPort< Crawler>及ICrawlerRequestHandler。不过由于IPort< T>支持协变,于是IPort< Crawler>被安全地转换成了IPort< ICrawlerRequestHandler>对象。

第二行中再次发生了协变:ICrawlRequestHandler.Crawel的第一个参数需要IPort< ICrawlResponseHandler>类型的对象,但是this是Monitor类型的,它并没有实现这个接口。不过,和上面描述的一样,由于IPort< T>支持协变,因此这样的类型转化是安全的,允许的。于是在Crawler类便可以操作一个“抽象”,而不是具体的Monitor类型来办事了。

神奇不?但就是这么简单。

“内部”消息控制
在上一篇文章中,我们还提出了Crawler实现的另一个缺点:没有使用异步IO。WebClient本身的DownloadStringAsync方法可以进行异步下载,但是如果在异步完成的后续操作(如分析链接)会在IO线程池中运行,这样我们就很难对任务所分配的运算能力进行控制。我们当时提出,可以把后续操作作为消息发送给Crawler本身,也就是进行“内部”消息控制——可惜的是,我们当时无法做到。不过现在,由于Crawler实现的是IPort< Crawler>接口,因此,我们可以把Crawler内部的任何方法作为消息传递给自身,如下:

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


我们准备了一个private的Crawled方法,如果抓取成功了,我们会把这个方法的调用封装在一条消息中重新发给自身。请注意,这是个私有方法,因此这里完全是在做“内部”消息控制。

开启抓取任务
在上一篇文章中,我们为Monitor添加了一个Start方法,它的作用是启动URL。我们知道,对单个Actor来说消息的处理是线程安全的,但是这个前提是使用“消息”传递的方式进行通信,如果直接调用Start公有方法,便会破坏这种线程安全特性。不过现在的Monitor已经不受接口的限制,可以自由接受任何它可以执行的消息,因此我们只要对外暴露一个Crawl方法即可:

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


于是我们便可以向Monitor发送消息,让其抓取特定的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.  
  8. Random random = new Random(DateTime.Now.Millisecond);  
  9. Monitor monitor = new Monitor(10);  
  10. foreach (var url in urls)  
  11. {  
  12.     var urlToCrawl = url;  
  13.     monitor.Post(m => m.Crawl(urlToCrawl));  
  14.     Thread.Sleep(random.Next(1000, 3000));  
  15. }  
  16.  

上面的代码会每隔1到3秒发出一个抓取请求。由于我们使用了消息传递的方式进行通信,因此对于Monitor来说,这一切都是线程安全的。我们可以随时随地为Monitor添加抓取任务。

接受多种消息(协议)
我们再观察一下Monitor的签名:

class Monitor : Actor< Action< Monitor>>, IPort< Monitor>, ICrawlResponseHandler
可以发现,如今的Monitor已经和它实现的协议没有一对一的关系了。也就是说,它可以添加任意功能,可以接受任意类型的消息,我们只要让它实现另一个接口即可。于是乎,我们再要一个“查询”功能2:

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


为了让Monior支持查询,我们还需要为它添加这样的代码:

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


最后,我们来尝试着使用这个“查询”功能。首先,我们编写一个测试用的TestStatisticPort类:

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


当调用Start方法时,控制台将会等待用户敲击回车键。当按下回车键时,TestStatisticPort将会向Monitor发送一个IStatisticRequestHandler.GetCrawledCount消息。Monitor回复之后,屏幕上便会显示当前已经抓取成功的URL数目。例如,我们可以编写如下的测试代码:

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


随意敲击几下回车,结果如下:

Crawl Statistic


总结
如今的做法,兼顾了强类型检查,并使用C# 4.0中的协变和逆变特性,把上一篇文章中提出的问题解决了,不知您是否理解了这些内容?只可惜,我们在C# 3.0中还没有协变和逆变。因此,我们还必须思考一个适合C# 3.0的做法。

顺便一提,由于F#不支持协变和逆变,因此本文的做法无法在F#中使用。

注1:关于协变和逆变特性,我认为脑袋兄的这篇文章讲的非常清楚——您看得头晕了?是的,刚开始了解协变和逆变,以及它们之间的嵌套规则时我也头晕,但是您在掌握之后就会发现,这的确是一个非常有用的特性。

注2:不知您是否发现,与之前internal的Crawl相关接口不同,Statistic相关接口是public的。我们在使用接口作为消息时,也可以通过这种办法来控制哪些消息是可以对外暴露的。这也算是一种额外的收获吧。

【编辑推荐】

  1. C#调用Windows API函数
  2. 详解C#调用Outlook API
  3. C#连接Access、SQL Server数据库
  4. 介绍C#调用API的问题
  5. C#调用Excel与附加代码
责任编辑:yangsai 来源: 博客园
相关推荐

2011-01-14 10:27:18

C#.netasp.net

2009-05-27 11:30:20

C#Visual Stud协变

2012-03-13 09:32:15

C#协变

2009-06-03 14:50:17

C# 4.0泛型协变性

2022-04-18 20:12:03

TypeScript静态类型JavaScrip

2020-08-03 08:13:51

Vue3TypeScript

2020-09-29 06:37:30

Java泛型

2009-02-03 09:33:26

动态类型动态编程C# 4.0

2009-10-20 15:03:29

ExpandoObje

2009-07-22 09:27:04

Scala变高变宽

2020-02-11 14:14:52

this函数

2015-12-01 18:03:44

EMUI4.0

2023-01-29 09:15:42

2013-10-31 09:36:43

程序员程序高手

2022-05-30 16:57:44

TypeScriptTS前端

2011-08-08 10:47:45

超微机箱服务器

2022-04-13 11:18:48

渗透测试Mock

2009-09-01 09:38:45

COM互操作性

2010-01-20 09:17:46

点赞
收藏

51CTO技术栈公众号