You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

保留IEnumerable<double>时单元测试无法捕获异常的解决及疑问

问题描述

我在对GridLevels方法做单元测试时,预期它会抛出ArgumentOutOfRangeException,但调用gridTrading.GridLevels().As<double[]>()时却返回null,异常被吞掉了。我想在**保持返回类型为IEnumerable<double>(不转成List<double>)**的前提下解决这个问题,另外加.As<double[]>()是为了避免多次枚举。最后想问问,坚持用IEnumerable<double>是否合理?

相关代码

单元测试代码

[Fact]
public void GridLevels_ShouldThrow_WhenGivenInvalidGridType()
{
    // Arrange
    const double lowerLimit = 2000;
    const double upperLimit = 10000;
    const int gridCount = 4;
    const int gridType = 4;

    var gridTrading = new GridTrading(lowerLimit, upperLimit, gridCount, (GridType)gridType);

    // Act
    var action = new Action(() => gridTrading.GridLevels().As<double[]>());

    // Assert
    action.Should().Throw<ArgumentOutOfRangeException>().WithMessage("Grid type must be valid");
}

待测试的原方法

public IEnumerable<double> GridLevels()
{
    switch (_gridType)
    {
        case GridType.Arithmetic:
            foreach (var level in CalculateArithmetic())
            {
                yield return level;
            }

            break;

        case GridType.Geometric:
            foreach (var level in CalculateGeometric())
            {
                yield return level;
            }

            break;

        default:
            throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid");
    }

    IEnumerable<double> CalculateArithmetic()
    {
        var step = (_upperLimit - _lowerLimit) / _gridCount;

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit + step * i;
            yield return price;
        }
    }

    IEnumerable<double> CalculateGeometric()
    {
        var step = Math.Pow(_upperLimit / _lowerLimit, 1.0 / _gridCount);

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit * Math.Pow(step, i);
            yield return price;
        }
    }
}

可正常抛出异常但不想用的版本

public List<double> GridLevels()
{
    var list = new List<double>();

    switch (_gridType)
    {
        case GridType.Arithmetic:
            list.AddRange(CalculateArithmetic());
            break;

        case GridType.Geometric:
            list.AddRange(CalculateGeometric());
            break;

        default:
            throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid");
    }

    return list;

    IEnumerable<double> CalculateArithmetic()
    {
        var step = (_upperLimit - _lowerLimit) / _gridCount;

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit + step * i;
            yield return price;
        }
    }

    IEnumerable<double> CalculateGeometric()
    {
        var step = Math.Pow(_upperLimit / _lowerLimit, 1.0 / _gridCount);

        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit * Math.Pow(step, i);
            yield return price;
        }
    }
}
问题根源

这是因为使用yield return的迭代器方法是延迟执行的:GridLevels里的switch逻辑(包括default分支的异常抛出)要等到迭代器被枚举时才会执行。但As<double[]>()在处理未被枚举的空迭代器时,直接返回了null,根本没触发枚举流程,导致异常完全没机会抛出。

解决方案(保持返回IEnumerable<double>)

要让参数校验的异常在调用方法时立刻抛出(而非延迟到枚举阶段),需要把校验逻辑从迭代器执行流程中抽出来,提前到方法开头执行。下面提供两种可行的修改方式:

方式一:提前添加显式校验

public IEnumerable<double> GridLevels()
{
    // 提前校验_gridType合法性,调用方法时立刻抛出异常
    if (!Enum.IsDefined(typeof(GridType), _gridType))
    {
        throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid");
    }

    switch (_gridType)
    {
        case GridType.Arithmetic:
            foreach (var level in CalculateArithmetic())
            {
                yield return level;
            }
            break;
        case GridType.Geometric:
            foreach (var level in CalculateGeometric())
            {
                yield return level;
            }
            break;
    }

    IEnumerable<double> CalculateArithmetic()
    {
        var step = (_upperLimit - _lowerLimit) / _gridCount;
        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit + step * i;
            yield return price;
        }
    }

    IEnumerable<double> CalculateGeometric()
    {
        var step = Math.Pow(_upperLimit / _lowerLimit, 1.0 / _gridCount);
        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit * Math.Pow(step, i);
            yield return price;
        }
    }
}

方式二:拆分校验与迭代器逻辑

如果想保留switch的default分支来抛出异常,可以把校验和迭代器返回逻辑拆分开:

public IEnumerable<double> GridLevels()
{
    // 先执行switch做校验,触发异常
    switch (_gridType)
    {
        case GridType.Arithmetic:
        case GridType.Geometric:
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(_gridType), "Grid type must be valid");
    }

    // 再返回对应的迭代器
    return _gridType switch
    {
        GridType.Arithmetic => CalculateArithmetic(),
        GridType.Geometric => CalculateGeometric(),
        _ => Enumerable.Empty<double>() // 理论上不会走到这里,因为前面已校验
    };

    IEnumerable<double> CalculateArithmetic()
    {
        var step = (_upperLimit - _lowerLimit) / _gridCount;
        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit + step * i;
            yield return price;
        }
    }

    IEnumerable<double> CalculateGeometric()
    {
        var step = Math.Pow(_upperLimit / _lowerLimit, 1.0 / _gridCount);
        for (var i = 0; i <= _gridCount; i++)
        {
            var price = _lowerLimit * Math.Pow(step, i);
            yield return price;
        }
    }
}

两种方式都能让异常在调用GridLevels()时立刻抛出,同时保持返回类型为IEnumerable<double>,后续调用.As<double[]>()也能正常触发枚举,不会吞掉异常。

关于坚持使用IEnumerable<double>的合理性

是否合理取决于你的具体使用场景:

  • 适合使用的场景:
    • 数据量较大,延迟执行可以节省内存(无需一次性加载所有元素)
    • 仅需枚举一次,或者希望将计算延迟到真正需要数据的时候
    • 希望保持接口抽象性,不暴露具体集合类型(如List),方便后续修改内部实现
  • 不适合的场景:
    • 需要多次枚举数据:每次枚举都会重新执行迭代器逻辑(比如CalculateArithmetic会重新计算一遍),若不想重复计算,要么在方法内部缓存结果为数组/List再返回,要么在调用方提前转换
    • 需要立刻完成参数校验并抛出异常:原生迭代器方法会延迟执行校验,需额外处理才能提前抛出
    • 需要集合的额外功能(如索引访问、添加/删除元素):IEnumerable仅支持枚举,没有这些操作

如果你的场景是数据量不大,且希望保持接口抽象性,或仅需枚举一次,那么坚持使用IEnumerable<double>是合理的。若存在多次枚举的情况,建议要么在方法内部缓存结果(仍返回IEnumerable<double>),要么在调用方提前转成数组/List避免重复计算。

内容的提问来源于stack exchange,提问作者nop

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 12:15:49