Лениво создавая перечислимые с перерывами между

У меня есть коллекция предметов IEnumerable<object> obs, У меня есть другая коллекция предметов IEnumerable<object> data,

Для каждого ob в obs Мне нужно найти первый пункт в data имеет то же значение в определенном свойстве, что и ob, Например, я мог бы искать первый элемент в data это имеет то же самое ToString() значение как ob, Когда найден первый элемент, у которого совпадают значения свойств, я делаю что-то с найденным элементом данных, а затем проверяю следующий ob в obs, Если ничего не найдено, выкидываю ошибку.

Вот наивный подход:

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: ;
}

Я знаю, это легко, если data был LinkedList или любая коллекция с индексированным доступом (тогда я мог бы сохранить узел связанного списка или индекс как lastProcessedDataOb), но это не так - это IEnumerable, Может быть yield return можно использовать здесь?

2 ответа

Решение

Если ваши коллекции действительно большие, и вы действительно не хотите оценивать ToString за каждый предмет data Вы можете использовать следующий подход:

  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.");
        }
    }
    

Таким образом, вы можете гарантировать, что итерация data сбор только до того момента, когда все obs предметы найдены и вы не будете оценивать ToString за каждый товар более одного раза.

PS: я надеюсь, что "ToString" только для примера, и у вас там есть некоторые сложные вычисления, которые стоят таких сложностей...

Совершенно забыл, что 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: ;
}
Другие вопросы по тегам