比较两个对象列表,将两者中不同的对象提取到一个单独的列表中
我正在开发一个应用程序,用户可以在其中向一个产品添加物品,并为每个物品指定一个相关数量。物品、产品,以及在一个产品中使用的每个物品的数量,都会分别存放在数据库的独立表中。
现在大部分功能都能正常工作,但我有以下问题:
在更新一个产品时,你也可以改变其关联的物品。这些在屏幕上以列表框的形式显示,通过在同一表单上的组合框中选择一个物品来添加。你也可以通过单独的按钮来修改产品中物品的数量。
在这些操作期间,一个列表会记录用户当前希望将哪些物品与该产品关联,以及它们的数量,如 List<ItemAmount> itemAmounts。
此外,在整个类中我还可以访问下列List。这个List按 name、id 和 amount 存储物品,这些是在使用另一种方法加载表单时,它们在数据库中的保存状态:
//Gets set when the page loads with an update request
List<ItemAmount> oldData = new List<ItemAmount>();
这里的操作工作正常,
oldData 按原样记住已关联的物品,而 itemAmounts 记录用户希望在它们之间进行哪些更改。
最终当用户想向数据库发送更新请求时,itemAmount 的关联需要被更新。为此,我想要一个 ItemAmount,它只包含在 itemAmounts 与 oldData 之间不同的 ItemAmount 对象。我的意思是它们两者都包含一个 ItemAmount 对象,其中 ItemAmount.item.id 的值相同,但 ItemAmount.amount 不同。我已经为获取被删除或新增的物品以及获取同时存在于两个列表中的 ItemAmount 对象的方法写好了代码,如下所示:
//make a list with the ids of old items
List<string> oldItemIds = new List<string>();
foreach (ItemAmount itm in oldData)
oldItemIds.Add(itm.item.id);
//make a list with the ids of updated items
List<string> newItemIds = new List<string>();
foreach (ItemAmount itm in itemAmounts)
newItemIds.Add(itm.item.id);
//List for the indexes of items to delete
List<int> deleteIndexes = new List<int>();
//List for the indexes of items to insert
List<int> insertIndexes = new List<int>();
//List for items that COULD be updated
List<int> possibleUpdatedIndexes = new List<int>();
//fill the lists correctly
int index = 0;
foreach(string newId in newItemIds)
{
if (oldItemIds.Contains(newId))
possibleUpdatedIndexes.Add(Convert.ToInt32(index));
else
insertIndexes.Add(Convert.ToInt32(index));
index++;
}
index = 0;
foreach(string oldId in oldItemIds)
{
if (!newItemIds.Contains(oldId))
{
deleteIndexes.Add(index);
}
index++;
}
这会跟踪用于 INSERT 和 DELETE 命令的索引。
然而,我还没有关于 UPDATE 的实现,因为我目前只有可能发生变更的索引。
我该如何通过 possibleUpdateIndexes 迭代,得到一个仅包含已更改的 ItemAmount 对象的 ItemAmount 列表?
我的猜测是它可以用嵌套的 foreach 循环来实现,但我想应该有更干净的方法。
顺便说一句,我的物品类大致是这样的:
public class Item
{
public string id { get; set; }
public string name { get; set; }
}
public class ItemAmount()
{
public Item item { get; set; }
public int amount { get; set; }
}
解决方案
检测两个内存中对象集合修改的最有效(在内存和速度方面都最佳)的一般算法如下:
static
(
IReadOnlyCollection<TItem> AddItems,
IReadOnlyCollection<TItem> UpdateItems,
IReadOnlyCollection<TItem> DeleteItems
)
GetChanges<TItem, TKey>
(
IEnumerable<TItem> oldItems,
IEnumerable<TItem> newItems,
Func<TItem, TKey> itemKey,
Func<TItem, TItem, bool> dataChanged
)
{
var addItems = new List<TItem>();
var updateItems = new List<TItem>();
var oldItemByKey = oldItems.ToDictionary(itemKey);
foreach (var newItem in newItems)
{
var key = itemKey(newItem);
if (!oldItemByKey.Remove(key, out var oldItem))
addItems.Add(newItem);
else if (dataChanged(oldItem, newItem))
updateItems.Add(newItem);
}
var deleteItems = oldItemByKey.Values;
return (addItems, updateItems, deleteItems);
}
用一次对旧项的遍历来创建一个按键快速查找的字典。然后对新项再进行一次遍历,利用按键的快速 O(1) 字典 Remove 方法,既定位旧项(若存在),又从字典中把它移除。最后,字典中剩下的项就是需要删除的项。时间复杂度为线性 O(N+M),其中 N 和 M 分别是旧项和新项的数量。
在你的场景中,使用将类似如下:
List<ItemAmount> oldData, newData;
var c = GetChanges(oldData, newData, e => e.item.id, (e1, e2) => e1.amount != e2.amount);
// Use c.AddItems, c.DeleteItems, c.UpdateItems
该算法也可以很容易地修改为处理列表,返回索引列表、或要删除的键(ID)的列表,而不是对象列表,但我看不出这样做的好处。除非 oldData 列表是需要后续更新的持久缓存列表。若是这样的话,它其实不应该是一个列表,而应该是类似于算法中使用的临时字典的数据结构。