【问题标题】:lazily building up an enumerable with breaks in between懒惰地建立一个可枚举的,中间有中断
【发布时间】:2018-04-28 12:04:48
【问题描述】:

我有一个对象集合IEnumerable<object> obs。 我还有另一个对象集合IEnumerable<object> data

对于obs 中的每个ob,我需要在data 中找到与ob 在某个属性中具有相同值的第一项。例如,我可能正在寻找data 中与ob 具有相同ToString() 值的第一项。当找到属性值匹配的第一项时,我对找到的数据项执行一些操作,然后检查obs 中的下一个ob。如果没有找到,我会抛出一个错误。

这是一个幼稚的方法:

foreach (object ob in obs)
{
    foreach (object dataOb in data)
        if (ob.ToString() == dataOb.ToString())
        {
            ... // do something with dataOb
            goto ContinueOuter;
        }
    throw new Exception("No matching data found.");

    ContinueOuter: ;
}

缺点是我每次都计算dataOb.ToString(),没必要。 我可以缓存它:

IDictionary<object, string> dataToDataStr = new Dictionary<object, string>();
foreach (object dataObj in data) // collect all ToString values in advance
    dataToDataStr.Add(dataObj, dataObj.ToString());

foreach (object ob in obs)
{
    foreach (object dataOb in dataToDataStr.Keys)
        if (ob.ToString() == dataToDataStr[dataOb])
        {
            ... // do something with dataOb
            goto ContinueOuter;
        }
    throw new Exception("No matching data found.");

    ContinueOuter: ;
}

缺点是我计算了所有ToString() 值,即使它可能不是必需的。我可能会在数据收集的前半部分找到所有匹配的数据对象。

我怎样才能懒惰地构建dataToDataStr 字典(或任何其他可让我检索对象及其唯一一次计算的 ToString 值的可枚举数据结构)?

这是我想到的代码(与伪代码混合):

IDictionary<object, string> dataToDataStr = new Dictionary<object, string>();
object lastProcessedDataOb = null;

foreach (object ob in obs)
{
    foreach (object dataOb in dataToDataStr.Keys)
        if (ob.ToString() == dataToDataStr[dataOb])
        {
            ... // do something with dataOb
            goto ContinueOuter;
        }

    foreach (object dataOb in data STARTING AFTER lastProcessedDataOb)
    // if lastProcessedDataOb == null, start with the first entry of data
    {
        dataToDataStr.Add(dataOb, dataOb.ToString();
        lastProcessedDataOb = dataOb;

        if (ob.ToString() == dataToDataStr[dataOb])
        {
            ... // do something with dataOb
            goto ContinueOuter;
        }
    }
    throw new Exception("No matching data found.");

    ContinueOuter: ;
}

我知道如果 dataLinkedList 或任何具有索引访问的集合(然后我可以将链表节点或索引存储为 lastProcessedDataOb),这很容易,但事实并非如此 - 它是IEnumerable。也许这里可以使用yield return

【问题讨论】:

  • 也许你应该首先描述为什么你通过 ToString 比较你的对象。那感觉不对。为什么不使用实现 Equals/GetHashcode 的标准方法或正确实现 IEquatable/IComparable 接口?如果计算成本很高,则该实现可能包含缓存机制。这将使此类课程的用户的生活更轻松。
  • 您的评论与问题的重点无关(懒惰地建立可枚举)。另外,我确实写了“我需要在数据中找到与 ob 的某个属性具有相同值的第一项。例如 [...] ToString [...]”。

标签: c# lazy-evaluation enumerable


【解决方案1】:

如果您的集合非常大,并且您真的不想为data 的每个项目评估ToString,您可以使用以下方法:

  1. 为已计算的项目创建缓存
  2. 如果在我的缓存中找到了某个项目 - 太好了,我们找到了匹配项。
  3. 否则 - 通过迭代 data 集合继续填充缓存,直到找到匹配项。这可以通过手动控制数据收集的**Enumerator**(而不是使用foreach)来有效地完成。

    IEnumerable<object> obs;
    IEnumerable<object> data;
    Dictionary<string, object> dataCache = new Dictionary<string, object>();
    
    var dataIterator = data.GetEnumerator();
    foreach (var ob in obs)
    {
        var obText = ob.ToString();
        object matchingDataItem = null;
        if (!dataCache.TryGetValue(obText, out matchingDataItem))
        {
            while (dataIterator.MoveNext())
            {
                var currentData = dataIterator.Current;
                var currentDataText = currentData.ToString();
                if (!dataCache.ContainsKey(currentDataText)) // Handle the case when data collection contains duplicates
                {
                    dataCache.Add(currentDataText, currentData);
                    if (currentDataText == obText)
                    {
                        matchingDataItem = currentData;
                        break;
                    }
                }
            }
        }
        if (matchingDataItem != null)
        {
            Console.WriteLine("Matching item found for " + obText);
        }
        else
        {
            throw new Exception("No matching data found.");
        }
    }
    

这样,您可以保证仅在找到所有 obs 项目时迭代 data 集合,并且您不会为每个项目评估 ToString 一次以上

PS:我希望“ToString”只是一个例子,你在那里有一些复杂的计算,值得这么复杂......

【讨论】:

  • 应该在Add 调用之前检查dataCache 是否已经包含密钥currentDataText。如果密钥存在,请执行continue
  • 您仍然检查字符串是否相等 (currentDataText == obText),以防密钥重复。这不是必需的,因为之前已经通过dataCache.TryGetValue(...) 进行了检查。
  • 也添加了优化,虽然看起来很小。
【解决方案2】:

完全忘记了 LINQ 使用惰性求值... 这应该可以工作(我使用 C# 7 的新值元组表示法):

IEnumerable<(object, string)> objStrPairs = data.Select(o => (o, o.ToString()));
foreach (object ob in obs)
{
    foreach ((object, string) dataPair in objStrPairs)
        if (ob.ToString() == objStrPairs.Item2)
        {
            ... // do something with objStrPairs.Item1
            goto ContinueOuter;
        }
    throw new Exception("No matching data found.");

    ContinueOuter: ;
}

【讨论】:

  • 你为什么还要使用goto?仅出于兼容性原因不推荐使用...
  • @CamiloTerevinto Java 可以命名一个循环——例如外部循环可以命名为“outer”——然后,在嵌套循环的深处,调用continue outer。由于 C# 没有这个特性,goto 是要走的路。
  • 常见的方法是理解 c# 不是 java。通常,我们会在外部循环中添加一个布尔变量,以了解内部循环中发生了什么。您甚至可以对整个答案使用单个 LINQ 语句。
  • @CamiloTerevinto “您甚至可以对整个答案使用单个 LINQ 语句。”那么请分享你的答案!
  • 如果你害怕计算集合中的冗余项(O(N))复杂度,为什么要为每个项一次又一次地迭代数据集合,使其复杂度甚至为 O(N*N)没有到达收集结束...
猜你喜欢
  • 1970-01-01
  • 2019-02-25
  • 2011-03-24
  • 1970-01-01
  • 2021-08-08
  • 2018-09-17
  • 2016-10-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多