在List上使用LINQ左连接有问题

本文关键字:LINQ 连接 有问题 List FileInfo | 更新日期: 2023-09-27 17:49:31

我有两个List<FileInfo>列表,SourceFilesDestFiles。我想建立一个LINQ查询,它将返回一个项目的列表,其文件名在Source中,但不在Dest中,即左连接。

我的SourceFiles的数据集是:

folder1'a.txt
folder1'b.txt
folder1'c.txt
folder1'd.txt

DestFiles is:

folder2'a.txt
folder2'b.txt
folder2'c.txt

所以查询应该返回folder1'd.txt

在MSDN示例之后,我尝试使用LINQ语法:

var queryX = from s in SourceFiles
             join d in DestFiles
             on s.Name equals d.Name
             into SourceJoinDest
             from joinRow in SourceJoinDest.DefaultIfEmpty()
             select new
             {
                 joinRow.FullName
             };

和使用扩展方法:

var query = SourceFiles.GroupJoin(DestFiles,
                                    source => source.Name,
                                    dest => dest.Name,
                                    (source,dest) => new
                                    {
                                        path = source.FullName
                                    }).Select(x => x.path.DefaultIfEmpty())

但是这些都不起作用;LINQ语法版本返回Object reference not sent to an instance of an object,扩展版本返回Enumeration yielded no results.

我意识到这些查询只返回FullName属性集,而不是完整的FileInfo对象;我的代码接受每个FullName并返回一个FileInfo,并对查询中的每个项目进行此操作以重建列表。但是,如果有一种方法可以直接从查询返回FileInfo,那将是伟大的。

在List<FileInfo>上使用LINQ左连接有问题

我不认为Join是这里的理想工具。基本上你要找的是Except。内置的Except没有通过lambda指定属性的重载。您必须创建自己的IEqualityComparer。但是,您可以这样做:

var excepts = SourceFiles.Where(c => !DestFiles.Any(p => p.Name == c.Name)).ToList();

或者,只选择完整路径,您可以在末尾使用Select

var excepts = SourceFiles.Where(c => !DestFiles.Any(p => p.Name == c.Name))
                         .Select(f => f.FullName).ToList();

我建议使用扩展方法来快速完成ExceptIntersect

public static IEnumerable<U> Except<R, S, T, U>(this IEnumerable<R> mainList, 
                                                IEnumerable<S> toBeSubtractedList,
                                                Func<R, T> mainListFunction, 
                                                Func<S, T> toBeSubtractedListFunction,
                                                Func<R, U> resultSelector)
{
    return EnumerateToCheck(mainList, toBeSubtractedList, mainListFunction, 
                            toBeSubtractedListFunction, resultSelector, false);
}
static IEnumerable<U> EnumerateToCheck<R, S, T, U>(IEnumerable<R> mainList, 
                                                   IEnumerable<S> secondaryList,
                                                   Func<R, T> mainListFunction, 
                                                   Func<S, T> secondaryListFunction,
                                                   Func<R, U> resultSelector,
                                                   bool ifFound)
{
    foreach (var r in mainList)
    {
        bool found = false;
        foreach (var s in secondaryList)
        {
            if (object.Equals(mainListFunction(r), secondaryListFunction(s)))
            {
                found = true;
                break;
            }
        }
        if (found == ifFound)
            yield return resultSelector(r);
    }
    //or may be just
    //return mainList.Where(r => secondaryList.Any(s => object.Equals(mainListFunction(r), secondaryListFunction(s))) == ifFound)
    //               .Select(r => resultSelector(r));
    //but I like the verbose way.. easier to debug..
}
public static IEnumerable<U> Intersect<R, S, T, U>(this IEnumerable<R> mainList, 
                                                   IEnumerable<S> toIntersectList,
                                                   Func<R, T> mainListFunction,
                                                   Func<S, T> toIntersectListFunction,
                                                   Func<R, U> resultSelector)
{
    return EnumerateToCheck(mainList, toIntersectList, mainListFunction, 
                            toIntersectListFunction, resultSelector, true);
}

现在你可以这样做:

var excepts = SourceFiles.Except(DestFiles, p => p.Name, p => p.Name, p => p.FullName)
                         .ToList();

与其使用join,不如使用.Except()

var enumerable = sourceFiles.Except(destFiles, new FileInfoComparer<FileInfo>((f1, f2)=>f1.Name == f2.Name, f=>f.Name.GetHashCode()));

.Except()接受一个IEqualityComparer<T>,你可以自己编写或者使用一个接受lambda的包装器。

    class FileInfoComparer<T> : IEqualityComparer<T>
    {
        public FileInfoComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
        {
            _equals = equals;
            _getHashCode = getHashCode;
        }
        readonly Func<T, T, bool> _equals;
        public bool Equals(T x, T y)
        {
            return _equals(x, y);
        }
        readonly Func<T, int> _getHashCode;
        public int GetHashCode(T obj)
        {
            return _getHashCode(obj);
        }
    } 

用几个样例数据运行它会得到一个包含"d.txt"

FileInfo对象。

你差一点就成功了。但是您只需要获取那些没有加入目标文件的源文件:

var query = from s in SourceFiles
            join d in DestFiles
                on s.Name equals d.Name into g
            where !g.Any() // empty group!
            select s;