(转载)yield 语法糖

在定义API的时候,对于一些返回集合对象的方法,很多人喜欢将返回类型定义成IEnumerable<T>,这本没有什么问题。这里要说的是另一个问题:对于返回类型为IEnumerable<T>的方法来说,我们可以使用yield return的方式来输出返回集合的元素。但是如果我们不了解yield 关键字背后的实现机制,很有可能造成很大的问题。

这是一个WCF相关的问题,我想99%的人都有可能会犯这样的错误——即使你对yield了解得非常透彻。闲话少说,我们通过一个简单的实例来说明这个问题。我们定义了如下一个IDemoService接口作为服务契约,唯一的方法GetItems返回一个类型为IEnumerable<string>对象,并且具有唯一字符串参数category。

   1:  [ServiceContract]
   2:  public interface IDemoService
   3:  {
   4:      [OperationContract]
   5:      IEnumerable<string> GetItems(string category);
   6:  }

下面是实现了该契约接口的DemoService的实现:GetItems方法返回一个包含3个字符串的集合,但是在返回之前我们需要对参数实施验证。如果category参数提供的字符串为Null或者是空字符串,抛出一个FaultException异常并提示“Invalid Category”,这样客户端在输入不合法参数的情况下可以得到错误消息。这样的编程方式再正常不过了,不是吗?

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";
    }
}

可是正常并不意味着正确,客户端其实根本无法得到服务端提供给它的错误消息,如下所示的是客户端调用服务时指定一个空字符串参数情况下得到的错误。一个CommunicationException异常被抛出来,得到的错误消息为“An error occurred while receiving the HTTP response tohttp://127.0.0.1:3721/demoservice. This could be due to the service endpoint binding not using the HTTP protocol. This could also be due to an HTTP request context being aborted by the server (possibly due to the service shutting down). See server logs for more details.”

image

这貌似和我们预期的效果不一样,我们希望的是客户端抛出一个FaultException,并提示“Invalid category”。这实际上就是因为“yield”在作祟,不相信的话可以将定义在DemoService的GetItems方法替换成如下的定义,即直接返回一个string[]对像。

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" };
    }
}

再次运行我们的程序,这回可以得到我们期望的结果了。

image

有兴趣的朋友可以思考一下为什么两种貌似等效的方式为何会出现完全不同的结果,具体原因请看[下篇]

yield在WCF中的错误使用——99%的开发人员都有可能犯的错误[上篇] 
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%的开发人员都有可能犯的错误[下篇]


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值