如何避免LINQ查询中的重复代码

k3bvogb1  于 2023-07-31  发布在  其他
关注(0)|答案(4)|浏览(103)

我有一个代码,可以被认为是重复的:

private IEnumerable<decimal?> GetWidth(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Width != null)
    .Select(s => s.Width);

private IEnumerable<decimal?> GetHeight(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Height != null)
        .Select(s => s.Height);

private IEnumerable<decimal?> GetDiameter(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Diameter != null)
    .Select(s => s.Diameter);

private IEnumerable<decimal?> GetWeight(IEnumerable<Rectangle> rectangles) =>
    rectangles.Where(s => s.Weight != null)
        .Select(s => s.Weight);

字符串
这些方法之间的唯一区别只是字段名称,如Width, Height, Diameter, Weight。有没有可能用一个字符串属性名替换这些名称,并在不使用任何第三方库的情况下只创建一个方法?

ifmq2ha2

ifmq2ha21#

您可以为此创建简单的扩展:

public static class EnumerableExtensions
{
    public static IEnumerable<TValue> ExtractValues<TEntity, TValue>(
        this IEnumerable<TEntity> items,
        Func<TEntity, TValue?>    selector)
    {
        return items.Where(i => selector(i) != null)
            .Select(i => selector(i)!);
    }
}

字符串
然后这样使用:

var heights   = items.ExtractValues(i => i.Height);
var diameters = items.ExtractValues(i => i.Diameter);

k10s72fa

k10s72fa2#

因为你使用IEnumerable,参数只是选择和检查值,你可以 * 反转 * 操作来选择值并过滤空值。您仍然在执行相同的简单操作-检查所有值,但只允许非空值:

var heights=items.Select(s =>s.Height).Where(x=>x!=null);
var diameters=items.Select(s =>s.Diameter).Where(x=>x!=null);
var weights=items.Select(s =>s.Weight).Where(x=>x!=null);

字符串
您可以将其转换为表达式方法:

IEnumerable<TResult> SelectNonNull<TSource,TResult>( 
    this IEnumerable<TSource> source, 
    Func<TSource,TResult?> selector) 
{
    return source.Select(selector).Where(x=>x!=null);
}

...
var heights=items.SelectNotNull(s =>s.Height);
var diameters=items.SelectNotNull(s =>s.Diameter);
var weights=items.SelectNotNull(s =>s.Weight);

oknwwptz

oknwwptz3#

在这个特定的例子中,我将引入一个单独的方法来执行null检查。类似于:

public static T NotNull<T>(this IEnumerable<T?> list) where T : struct 
    => list.Where(t => t.HasValue).Select( t => t.Value);

private IEnumerable<decimal> GetWeight(IEnumerable<Rectangle> rectangles) =>
    rectangles.Select(s => s.Weight).NotNull();

字符串

368yc8dk

368yc8dk4#

如果你真的想用一个字符串来指示要获取的属性,你可以构建一个动态选择作为扩展方法。
此外,通过将Where().Select()切换到Select().Where(),您也只需要一个动态表达式。即

rectangles
    .Where(s => s.Width != null)
    .Select(s => s.Width);

// becomes

rectangles
    .Select(s => s.Width)
    .Where(s => s != null);

字符串
那么,如何构建动态过滤器呢?
你可以这样做--基于我对类似问题的回答

public static class ExtensionMethods
{
    public static IEnumerable<decimal?> GetProperty(
        this IEnumerable<Rectangle> rectangles,
        string property)
    {
        var queryableRectangles = rectangles.AsQueryable();
        
        // w =>
        var param = Expression.Parameter(typeof(Rectangle), "w");
    
        var propertyInfo = typeof(Rectangle).GetProperty(property);
        if (propertyInfo == null)
            throw new Exception($@"Property ""{property}"" was not found");
    
        // w.[property]
        var selector = Expression.Property(param, propertyInfo);
    
        // Bring it all together
        // Select(w => w.[property])
        var selectExpression = Expression.Call(
            typeof(Queryable),
            nameof(System.Linq.Enumerable.Select),
            new Type[] { typeof(Rectangle), typeof(decimal?) },
            queryableRectangles.Expression,
            Expression.Lambda<Func<Rectangle, decimal?>>(selector, new ParameterExpression[] { param })
        );
    
        // Query the collection
        var filteredItems = queryableRectangles.Provider.CreateQuery<decimal?>(selectExpression);
    
        // return the list of values removing null values.
        return filteredItems
            .Where(x => x.HasValue) // Use HasValue instead of != null for Nullable<t>
            .ToList();
    }
}


然后可以在代码中使用它

var heights = rectangles.GetProperty("Height");

UPDATE -完全通用版本

要使这个完全通用是相当容易的Rectangle with T and十进制替换?with TOut, of course you need to update the method signature a little with the generic type parameters to <T,TOut>`

注意从.Where(x => x.HasValue)变回`。其中(x => x!= null)

public static IEnumerable<TOut> GetProperty<T, TOut>(this IEnumerable<T> source, string property)
{
    var queryable = source.AsQueryable();

    // w =>
    var param = Expression.Parameter(typeof(T), "w");

    var propertyInfo = typeof(T).GetProperty(property);
    if (propertyInfo == null)
        throw new Exception($@"Property ""{property}"" was not found");

    // w.[property]
    var selector = Expression.Property(param, propertyInfo);

    // Bring it all together
    // Select(w => w.[property])
    var selectExpression = Expression.Call(
        typeof(Queryable),
        nameof(System.Linq.Enumerable.Select),
        new Type[] { typeof(T), typeof(TOut) },
        queryable.Expression,
        Expression.Lambda<Func<T, TOut>>(selector, new ParameterExpression[] { param })
    );

    // Run query against the database
    var filteredItems = queryable.Provider.CreateQuery<TOut>(selectExpression);

    return filteredItems
        .Where(x => x != null) // revert to != null for non Nullable<T> return types
        .ToList();
}


这也需要稍微调整它的调用方式:

var heights = rectangles.GetProperty<Rectangle, decimal?>("Height");

相关问题