yield在WCF中的错误使用——99%的开发人员都有可能犯的错误[下篇]
昨天写了《yield在WCF中的错误使用——99%的开发人员都有可能犯的错误[上篇]》,引起了一些讨论。关于yield关键字这个语法糖背后的原理(C#编译器将它翻译成什么)其实挺简单,虽然有时候因为误用它会导致一些问题,但是它本无过错。接下来,我们通过这篇短文简单地谈谈我所理解的yield。
目录
一、先看一个简单的例子
二、了解本质,只需要看看yield最终编译成什么
三、回到WCF的例子
一、先看一个简单的例子
我们现在看一个简单的例子。我们在一个Console应用中编写了如下一段简单的程序:返回类型为IEnumerable<string>的方法GetItems以yield return的方式返回一个包含三个字符串的集合,而在方法开始的时候我们打印一段文字表明定义在方法中的操作开始执行。在Main方法中,我们先调用GetItems方法将“集合对象”返回,然后调用其ToArray方法。在调用该方法之前我们打印一段文字表明对集合对象进行迭代。
static void Main(string[] args) { IEnumerable<string> items = GetItems(); Console.WriteLine("Begin to iterate the collection."); items.ToArray(); } static IEnumerable<string> GetItems() { Console.WriteLine("Begin to invoke GetItems() method"); yield return "Foo"; yield return "Bar"; yield return "Baz"; }
对于上面这段代码,我想肯定有人会认为得到的结果应该是这样:
Begin to invoke GetItems() method Begin to iterate the collection.
但是下面才是真正的执行结果。也就是说,一旦我们在一个返回类型为IEnumerable或者IEnumerable<T>的方式中通过yield return返回集合元素,意味着这个定义在方法中操作会被“延后执行”——操作的真正执行不是发生在方法调用的时候,而是延后到对返回的集合进行迭代的时候。我们大体可以以这样的方式来“解释”这个现象:一旦我们使用了yield return,返回元素的操作会被封装成“可执行的表达式”的方式返回,一旦我们对集合进行迭代的时候,这些表达式才会被执行。
Begin to iterate the collection.
Begin to invoke GetItems() method
二、了解本质,只需要看看yield最终编译成什么
上面我们通过“延迟执行”和“可执行表达式”的形式来解释yield return,仅仅是为了比较好地理解它所体现出来的效果而已,实际上并没有这回事,这与LINQ的延迟加载更不是一回事。yield return仅仅是C#的一个语法糖而已,是编译器玩的一个小花招。如何透过这一层“糖纸”看到本质的东西,只需要看看编译器最终编译后的与之等效的代码是什么样子就可以了。对于上面这个例子来说,不管GetItems方法中以何种方式返回需要的对象,返回值总归是一个实现了IEnumerable <string>接口的某个类型的对象,我们只需要看看这个类型具有怎样的定义就知道C#编译器如果来“解释”yield return。
我们可以直接利用Reflector打开编译后的程序集,然后将.NET Framework的版本调成1.0(不支持C#针对后续版本提供的语法糖),这样就可以以“本质”的方式查看我们编写的代码了。如下面的代码片段所示,GetItems方法中没有发现我们定义的代码,而是直接返回一个类型为<GetItems>d__0的对象,看到这里相信读者朋友们知道为什么执行GetItems方法的时候并没有文字输出的真正原因了吧。
internal class Program { private static IEnumerable<string> GetItems() { return new <GetItems>d__0(-2); } private sealed class <GetItems>d__0 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable }
<GetItems>d__0是自动生成的类型,它实现了IEnumerable<string>接口,也实现了IEnumerator<string>,其 GetEnumerator()方法返回的实际上就是他自己。至于对<GetItems>d__0对象的进行迭代的时候如何返回具体元素,只要看看该类型的定义就一目了然了。如下面的代码片段所示,集合元素的返回实现在MoveNext()方法中,方法开始的操作(Console.WriteLine("Begin to invoke GetItems() method"))发生在第一次迭代的时候。
private sealed class <GetItems>d__0 : IEnumerable<string>, IEnumerable, IEnumerator<string>, IEnumerator, IDisposable { private int <>1__state; private string <>2__current; private bool MoveNext() { switch (this.<>1__state) { case 0: this.<>1__state = -1; Console.WriteLine("Begin to invoke GetItems() method"); this.<>2__current = "Foo"; this.<>1__state = 1; return true; case 1: this.<>1__state = -1; this.<>2__current = "Bar"; this.<>1__state = 2; return true; case 2: this.<>1__state = -1; this.<>2__current = "Baz"; this.<>1__state = 3; return true; case 3: this.<>1__state = -1; break; } return false; } string IEnumerator<string>.Current { [DebuggerHidden] get { return this.<>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return this.<>2__current; } } }
三、回到WCF的例子
再次回到《yield在WCF中的错误使用——99%的开发人员都有可能犯的错误[上篇]》中提到的例子,现在来解释为什么针对如下两段代码,前者抛出的异常不能被WCF正常处理,而后者可以。原因很简单——两段代码抛出异常的时机是不一样的。对于后者,异常在执行GetItems方法的时候会立即抛出来,WCF会捕获这个异常并作为应用级别的异常进行正常处理;对于前者,通过上面的分析我们知道异常实际上发生在对返回“集合对象”进行迭代的时候。具体是什么时候呢?其实就是对返回对象进行序列化的时候,此时抛出的异常将将会视为系统异常来处理。
public class DemoService : IDemoService { public IEnumerable<string> GetItems(string categoty) { if (string.IsNullOrEmpty(categoty)) { throw new FaultException("Invalid category"); } yield return "Foo"; yield return "Bar"; yield return "Baz"; } } public class DemoService : IDemoService { public IEnumerable<string> GetItems(string categoty) { if (string.IsNullOrEmpty(categoty)) { throw new FaultException("Invalid category"); } return new string[] { "Foo", "Bar", "Baz" }; } }
我个人觉得这是WCF值得改进的地方,但是目前来说为了避免这样的问题,我推荐将WCF契约接口操作方法中的返回类型定义成数组,而不是IEnumerable或者IEnumerable<T>(顺便说一下,WCF针对Array、List以及其他集合类型的序列化/反序列化行为是一致的),但是我个人对IEnumerable或者IEnumerable<T>不排斥。
yield在WCF中的错误使用——99%的开发人员都有可能犯的错误[上篇]
yield在WCF中的错误使用——99%的开发人员都有可能犯的错误[下篇]