zoukankan      html  css  js  c++  java
  • yield return

    一次被yield return坑的历程。

     

    事情的经过是这样的:

    我用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函数上下的断点。

    于是,我在 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();
            }
        }
    复制代码
  • 相关阅读:
    CSS的四种基本选择器和四种高级选择器
    Leetcode 897 递增顺序查找树
    Leetcode 872 叶子相似的树
    Leetcode 700 二叉搜索树中的搜索
    Leetcode 二叉树中第二小的节点
    Leetcode 669 修剪二叉搜索树
    Leetcode 653 两数之和IV
    Leetcode 637二叉树的层平均值
    Leetcode 617 合并二叉树
    Leetcode 606 根据二叉树创建字符串
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/7458499.html
Copyright © 2011-2022 走看看