在C#中合并两个列表,并将具有相同id的对象合并为一个列表项

7

我已经考虑过通过自己的方案来解决这个问题,但我想知道.NET是否已经有了我尝试实现的功能 - 如果有,我宁愿使用内置的东西。

假设我有两个Widget对象的实例,让我们称它们为 PartA PartB 。从两个不同的Web服务中获取了每个部件的信息,但是两者都具有匹配的ID。

PartA
{
    ID: 19,
    name: "Percy",
    taste: "",
    colour: "Blue",
    shape: "",
    same_same: "but different"
}

PartB
{
    ID: 19,
    name: "",
    taste: "Sweet",
    colour: "",
    shape: "Hexagon",
    same_same: "but not the same"
}

我希望将它们合并以创建以下内容:
Result
{
    ID: 19,
    name: "Percy",
    taste: "Sweet",
    colour: "Blue",
    shape: "Hexagon",
    same_same: "but different"
}

请注意,same_same的值在每个示例中都不同,但我们认为PartA是主要的,因此结果保留了but different的值。
现在让问题变得更加复杂:
假设我们有两个列表:
List<Widget> PartA = getPartA();
List<Widget> PartB = getPartB();

现在这里有一些伪代码描述我想做的事情:
List<Widget> Result = PartA.MergeWith(PartB).MergeObjectsOn(Widget.ID).toList();

这是一种非常低效的方式。完全手动:循环遍历每个列表并构建一个新的合并对象列表 - 合并也是手动完成的(即硬编码)。虽然能够完成工作,但非常糟糕且难以维护。我正在寻找更好的方法。 - Iain Fraser
3个回答

13
你可以编写自己的扩展方法,像这样:

你可以编写自己的扩展方法,例如:

static class Extensions
{
    public static IEnumerable<T> MergeWith<T>(this IEnumerable<T> source, IEnumerable<T> other) where T : ICanMerge
    {
        var otherItems = other.ToDictionary(x => x.Key);
        foreach (var item in source)
        {
            yield return (T)item.MergeWith(otherItems[item.Key]);
        }
    }
    public static string AsNullIfEmpty(this string s)
    {
        if (string.IsNullOrEmpty(s))
            return null;
        else
            return s;
    }
}

其中ICanMerge类似于:

public interface ICanMerge
{
    object Key { get; }
    ICanMerge MergeWith(ICanMerge other);
}

实现方式如下:

public class Widget : ICanMerge
{
    object ICanMerge.Key { get { return this.ID; } }
    int ID {get;set;}
    string taste {get;set;}
    public ICanMerge MergeWith(ICanMerge other)
    {
        var merged = new Widget();
        var otherWidget = (Widget)other;
        merged.taste = this.taste.AsNullIfEmpty() ?? otherWidget.taste;
        //...
        return merged;
    }
}

那么,只需简单地执行 PartA.MergeWith(PartB).ToList() 就行了。

做得好,Tim!我本来只是期望你能给我指明方向,但你提供了一个解决方案(而且比我目前使用的要好得多)。非常感谢:)我会尝试一下并回复你“接受”。 - Iain Fraser
当我尝试这样做时,它只允许我针对对象(例如Widget)执行MergeWith操作,而不是针对整个列表执行操作,这意味着需要对每个项目进行foreach并找到匹配项后执行合并。我很确定这不是使用方式,但为什么MergeWith不是IEnumerable列表的扩展选项之一呢? - mppowe
@mppowe 你是不是把它改成了 this T source 而不是 this IEnumerable<T> source?这是我能想到的唯一解释。 - Tim S.
没关系...我不知道为什么之前没有显示,现在可以了。也许是我没有给Intellisense足够的时间去适应或者其他原因。无论如何,它确实起作用了。值得注意的一点是,如果列表的大小不同,第一个列表(上面例子中的PartA)必须是较小的那个。否则会出现KeyNotFoundException。再次感谢Tim提供的代码,非常酷。 - mppowe

2
如果你的列表是一对一的(即每个列表中有相同数量的项,并且PartA列表中的每个项都与PartB列表中的某一项匹配),那么我建议使用Zip扩展方法。请注意,Zip实际上并不要求每个列表具有相同数量的项。但是,如果您不能依靠将具有匹配ID的项“配对”,那么我的简单方法就行不通了。
你可以这样做:
var alist = GetPartAWidgets().OrderBy(w => w.ID);
var blist = GetPartBWidgets().OrderBy(w => w.ID);
var merged = alist.Zip(blist, (a,b) => new Widget()
             {
               ID = a.ID,
               Name = string.IsNullOrEmpty(a.Name) ? b.Name : a.Name,
               //etc. 
             });

如果您希望Linq看起来更加简洁,可以将单独的小部件合并逻辑封装在一个函数或扩展方法中,并使用它来代替内联委托。

0
   public interface IMerge<out T>
{
    IEnumerable<IMergeMatched<T>> Matched();

    IEnumerable<IMergeMatched<T>> Matched(Func<T, T, bool> predicate);

    IEnumerable<T> NotMatchedBySource();

    IEnumerable<T> NotMatchedBySource(Func<T, bool> predicate);

    IEnumerable<T> NotMatchedByTarget();

    IEnumerable<T> NotMatchedByTarget(Func<T, bool> predicate);
}

public interface IMergeMatched<out T>
{
    T Source { get; }

    T Target { get; }
}

public static class Enumerable
{
    public static IMerge<TSource> Merge<TSource>(this IEnumerable<TSource> source, IEnumerable<TSource> target,
                                             Func<TSource, TSource, bool> predicate)
    {
        return new Merge<TSource>(source, target, predicate);
    }
}

public class Merge<T> : IMerge<T>
{
    private readonly Func<T, T, bool> _predicate;
    private readonly IEnumerable<T> _source;
    private readonly IEnumerable<T> _target;
    private IEnumerable<IMergeMatched<T>> _matcheds;
    private IEnumerable<T> _notMatchedBySource;
    private IEnumerable<T> _notMatchedByTarget;

    public Merge(IEnumerable<T> source, IEnumerable<T> taget, Func<T, T, bool> predicate)
    {
        _source = source;
        _target = taget;
        _predicate = predicate;
    }

    public IEnumerable<IMergeMatched<T>> Matched()
    {
        if (_matcheds == null)
        {
            Analize();
        }
        return _matcheds;
    }

    public IEnumerable<IMergeMatched<T>> Matched(Func<T, T, bool> predicate)
    {
        return Matched()
            .Where(t => predicate.Invoke(t.Source, t.Target))
            .ToArray();
    }

    public IEnumerable<T> NotMatchedBySource()
    {
        if (_notMatchedBySource == null)
        {
            Analize();
        }
        return _notMatchedBySource;
    }

    public IEnumerable<T> NotMatchedBySource(Func<T, bool> predicate)
    {
        return NotMatchedBySource()
            .Where(predicate)
            .ToArray();
    }

    public IEnumerable<T> NotMatchedByTarget()
    {
        if (_notMatchedByTarget == null)
        {
            Analize();
        }
        return _notMatchedByTarget;
    }

    public IEnumerable<T> NotMatchedByTarget(Func<T, bool> predicate)
    {
        return NotMatchedByTarget()
            .Where(predicate)
            .ToArray();
    }

    private void Analize()
    {
        var macheds = new List<MergeMached<T>>();
        var notMachedBySource = new List<T>(_source);
        var notMachedByTarget = new List<T>(_target);

        foreach (var source in _source)
        {
            foreach (var target in _target)
            {
                var macth = _predicate.Invoke(source, target);
                if (!macth) continue;

                macheds.Add(new MergeMached<T>(source, target));
                notMachedBySource.Remove(source);
                notMachedByTarget.Remove(target);
            }
        }

        _matcheds = macheds.ToArray();
        _notMatchedBySource = notMachedBySource.ToArray();
        _notMatchedByTarget = notMachedByTarget.ToArray();
    }
}

public class MergeMached<T> : IMergeMatched<T>
{
    public MergeMached(T source, T target)
    {
        Source = source;
        Target = target;
    }

    public T Source { get; private set; }

    public T Target { get; private set; }
}

如何使用?

var source = new List<MediaFolder>
            {
                new MediaFolder
                    {
                        Id = "Id1",
                        Name = "Name1",
                        Path = "Path1"
                    },
                new MediaFolder
                    {
                        Id = "Id2",
                        Name = "Name2",
                        Path = "Path2"
                    },
                new MediaFolder
                    {
                        Id = "Id3",
                        Name = "Name3",
                        Path = "Path3"
                    },
                new MediaFolder
                    {
                        Id = "Id4",
                        Name = "Name4",
                        Path = "Path4"
                    },
                new MediaFolder
                    {
                        Id = "Id5",
                        Name = "Name5",
                        Path = "Path5"
                    },
                new MediaFolder
                    {
                        Id = "Id6",
                        Name = "Name6",
                        Path = "Path6"
                    }
            };

        var target = new List<MediaFolder>
            {
                new MediaFolder
                    {
                        Id = "Id1",
                        Name = "Actualizado en el objeto",
                        Path = "Path1"
                    },
                    //Id2 eliminado
                new MediaFolder
                    {
                        Id = "Id3",
                        Name = "Name3",
                        Path = "Actualizado tambien"
                    },
                new MediaFolder
                    {
                        Id = "Id4",
                        Name = "Name4",
                        Path = "Path4"
                    },
                new MediaFolder
                    {
                        Id = "Id5",
                        Name = "Name5",
                        Path = "Path5"
                    },
                new MediaFolder
                    {
                        Id = "Id6",
                        Name = "Name6",
                        Path = "Path6"
                    },
                     new MediaFolder
                    {
                        Id = "Id7",
                        Name = "Nuevo Item 7",
                        Path = "Nuevo Item 7"
                    }
            };

        var merge = source.Merge(target, (x, y) => x.Id == y.Id);

        var toUpdate = merge.Matched((x, y) => x.Name != y.Name | x.Path != y.Path)
            .ToArray();

        var toDelete = merge.NotMatchedBySource();
        var toInsert = merge.NotMatchedByTarget();

        Assert.AreEqual(2, toUpdate.Count());
        Assert.IsTrue(toUpdate.Count(x => x.Source.Id == "Id1" & x.Target.Id == "Id1") > 0);
        Assert.IsTrue(toUpdate.Count(x => x.Source.Id == "Id3" & x.Target.Id == "Id3") > 0);

        Assert.AreEqual("Id7", toInsert.First().Id);
        Assert.AreEqual("Id2", toDelete.First().Id);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接