防止IEnumerable偶然重新枚举的技巧?

时间:2013-07-17 18:39:03

标签: c# enumerator

当一个断点似乎神奇地在同一个地方出现两次时,我只是花了一些时间挠头,在一个普通人中。

原来这个错误是一个直接的疏忽:

    protected override void Extract()
    {
        LogGettingOffers();
        var offerIds = CakeMarketingUtility.OfferIds(advertiserId);
        LogExtractingClicks(offerIds);
        foreach (var offerId in offerIds)
        {
            int rowCount;
            var clicks = RetryUtility.Retry(3, 10000, new[] { typeof(Exception) }, () =>
            {
                return CakeMarketingUtility.EnumerateClicks(dateRange, advertiserId, offerId);
            });
            foreach (var clickBatch in clicks.InBatches(1000))
            {
                LogExtractedClicks(offerId, clickBatch);

                // SHOULD BE clickBatch, NOT clicks
                Add(clicks);
            }
        }
        End();
    }

这让我想知道可能会采取什么样的预防措施来编写能够捕获这样的错误的代码。

注意,我不是肯定的,这样做是有道理的 - 也许答案是“不写错误的代码”,我愿意接受......

以下是产生结果的实际代码:

    public static IEnumerable<Click> EnumerateClicks(DateRange dateRange, int advertiserId, int offerId)
    {
        // initialize to start at the first row
        int startAtRow = 1;

        // hard code an upper limit for the max number of rows to be returned in one call
        int rowLimitForOneCall = 5000;

        bool done = false;
        int total = 0;
        while (!done)
        {
            Logger.Info("Extracted a total of {0} rows, checking for more, starting at row {1}..", total, startAtRow);

            // prepare the request
            var request = new ClicksRequest
            {
                start_date = dateRange.FromDate.ToString("MM/dd/yyyy"),
                end_date = dateRange.ToDate.ToString("MM/dd/yyyy"),
                advertiser_id = advertiserId,
                offer_id = offerId,
                row_limit = rowLimitForOneCall,
                start_at_row = startAtRow
            };

            // create the client, call the service and check the response
            var client = new ClicksClient();
            var response = client.Clicks(request);
            if (!response.Success)
            {
                throw new Exception("ClicksClient failed");
            }

            // update the running total
            total += response.RowCount;

            // return result
            foreach (var click in response.Clicks)
                yield return click;

            // update stopping condition for loop
            done = (response.RowCount < rowLimitForOneCall);

            // increment start row for next iteration
            startAtRow += rowLimitForOneCall;
        }

        Logger.Info("Extracted a total of {0}, done.", total);
    }

2 个答案:

答案 0 :(得分:1)

对于这个特定的问题,我会说解决方案是“不要写错误的代码”。特别是当可以在不改变任何状态的情况下生成结果时(比如从列表中枚举元素时),我认为可以从任何可枚举的内容创建多个枚举器。

你可以创建一个IEnumerable包装器,确保只调用GetEnumerator一次,但是如果你真的合法地需要调用它两次呢?你真正想要的是捕捉错误,而不是多次枚举被枚举的枚举数,而这不是你可以轻易放入软件解决方案的东西。

也许问题是clickBatchclicks具有相同的类型,因此编译器无法区分它们。

答案 1 :(得分:1)

有些时候我需要确保我公开的枚举只被调用一次。例如:返回我只有一个可用读取的流信息,或者非常昂贵的查询。

尝试以下扩展类:

public static class Extensions
{
    public static IEnumerable<T> SingleEnumeration<T>(this IEnumerable<T> source)
    {
        return new SingleEnumerator<T>(source);
    }
}

public class SingleEnumerator<T> : IEnumerable<T>
{
    public SingleEnumerator(IEnumerable<T> source)
    {
        this.source = source;
    }

    public IEnumerator<T> GetEnumerator()
    {
        // return an empty stream if called twice (or throw)
        if (source == null)
            return (new T[0]).AsEnumerable().GetEnumerator();

        // return the actual stream
        var result =source.GetEnumerator();
        source = null;
        return result;
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        // return an empty stream if called twice (or throw)
        if (source == null)
            return (new T[0]).AsEnumerable().GetEnumerator();

        var result = source.GetEnumerator();
        source = null;
        return result;
    }

    private IEnumerable<T> source;
}