记一次被yield return坑的历程。_.NET_编程开发_程序员俱乐部

中国优秀的程序员网站程序员频道CXYCLUB技术地图
热搜:
更多>>
 
您所在的位置: 程序员俱乐部 > 编程开发 > .NET > 记一次被yield return坑的历程。

记一次被yield return坑的历程。

 2017/8/31 15:08:53  D.H_Lee  程序员俱乐部  我要评论(0)
  • 摘要:事情的经过是这样的:我用C#写了一个很简单的一个通过迭代生成序列的函数。publicstaticIEnumerable<T>Iterate<T>(thisFunc<T,T>f,TinitVal,intlength){Checker.NullCheck(nameof(f),f);Checker.RangeCheck(nameof(length),length,0,int.MaxValue);varcurrent=initVal;while(-
  • 标签:

事情的经过是这样的:

我用C#写了一个很简单的一个通过迭代生成序列的函数。

public static IEnumerable<T> Iterate<T>(this Func<T, T> f, T initVal, int length)
{
    Checker.NullCheck(nameof(f), f);    
    Checker.RangeCheck(nameof(length), length, 0, int.MaxValue);

    var current = initVal;
    while (--length >= 0)
    {
        yield return (current = f(current));
    }
}

其中NullCheck用于检查参数是否为null,如果是则抛出ArgumentNullException异常

对应的,我写了如下单元测试代码去检测这个异常。

public void TestIterate()
{
    Func<int, int> f = null;
    Assert.Throws<ArgumentNullException>(() => f.Iterate(1, 7));
    
    // Other tests
}

但是,这个测试出乎意料的fail了。

一开始,我以为是NullCheck函数的问题,可我把NullCheck直接换成了if语句,还是通不过。

后来我在Iterate函数下断点并调试。结果调试器根本没有停在断点上,直接运行完了测试。

我以为是我测试的方法不对,所以我不断的修改测试代码,甚至还一度以为是.NET的Unit Tests出了bug。

最终,我在这个测试代码发现了问题:

Assert.Throws<ArgumentNullException>(() =>
{
    var seq = f.Iterate(1, 7);
    foreach (int ele in seq)
        Console.WriteLine(ele);
});

当我调试这个测试时,程序停在了我之前在Iterate函数上下的断点。

于是,我在 class="cnblogs_code">var seq = f.Iterate(1, 7); 上下断点,并逐步运行。这时我发现,当程序运行到 var seq = f.Iterate(1, 7); 时并不会进入Iterate函数;而是当程序运行到foreach语句后才进入。

这就要涉及到yield return的具体工作流程。当函数代码中出现yield return,调用这个函数会返回一个IEnumerable<T>或IEnumerator<T>对象,但是并不会执行函数体的任何代码。只有当你执行其返回的或调用返回对象的GetEnumerator方法得到的IEnumerator<T>对象的MoveNext()函数时,函数才会开始执行。

因此,上面两个Check并不会在函数调用时执行,而是在当你开始foreach的时候才执行。

这并不是我想要的结果。我希望在调用函数时就检查参数合法性,如果不合法便直接抛出异常。

解决这个问题有两种途径,一是把它拆成两个函数:

public static IEnumerable<T> Iterate<T>(this Func<T, T> f, T initVal, int length)
{
    Checker.NullCheck(nameof(f), f);    
    Checker.RangeCheck(nameof(length), length, 0, int.MaxValue);
            
    return IterateWithoutCheck(f, initVal, length);
}

private static IEnumerable<T> IterateWithoutCheck<T>(this Func<T, T> f, T initVal, int length)
{
    var current = initVal;
    while (--length >= 0)
    {
        yield return (current = f(current));
    }
}

或者,你也可以将这个函数包装成一个类。

    class FunctionIterator<T> : IEnumerable<T>
    {
        private readonly Func<T, T> f;
        private readonly T initVal;
        private readonly int length;
        
        public FunctionIterator(Func<T, T> f, T initVal, int length)
        {
            Checker.NullCheck(nameof(f), f);
            Checker.RangeCheck(nameof(length), length, 0, int.MaxValue);
            
            this.f = f;
            this.initVal = initVal;
            this.length = length;
        }

        public IEnumerator<T> GetEnumerator()
        {
            T current = initVal;

            for (int i = 0; i < length; ++i)
                yield return (current = f(current));
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

 

  • 相关文章
发表评论
用户名: 匿名