linq 是否可以使用lambda表达式创建简单的“EqualityComparer< T>

hmmo2u0o  于 2023-04-03  发布在  其他
关注(0)|答案(9)|浏览(221)

简短问题:

在LINQ to objects中有没有一种简单的方法,可以根据对象的键属性从列表中获取一个不同的对象列表。

长问题:

我正在尝试对对象的列表执行Distinct()操作,这些对象的属性之一是键。

class GalleryImage {
   public int Key { get;set; }
   public string Caption { get;set; }
   public string Filename { get; set; }
   public string[] Tags {g et; set; }
}

我有一个包含GalleryImage[]Gallery对象列表。
由于Web服务的工作方式[sic],我有GalleryImage对象的副本。我认为使用Distinct()来获得一个不同的列表是一件简单的事情。
这是我要使用的LINQ查询:

var allImages = Galleries.SelectMany(x => x.Images);
var distinctImages = allImages.Distinct<GalleryImage>(new 
                     EqualityComparer<GalleryImage>((a, b) => a.id == b.id));

问题是EqualityComparer是一个抽象类。
我不想:

  • GalleryImage上实现IEquatable,因为它是
  • 我必须编写一个单独的类来将IEqualityComparer实现为shown here

是否有我遗漏的EqualityComparer的具体实现?
我本以为会有一种简单的方法来从基于键的集合中获取“不同”对象。

68de4m5k

68de4m5k1#

(这里有两个解决方案-第二个见结尾):
我的MiscUtil库有一个ProjectionEqualityComparer类(以及两个支持类来使用类型推断)。
下面是一个使用它的例子:

EqualityComparer<GalleryImage> comparer = 
    ProjectionEqualityComparer<GalleryImage>.Create(x => x.id);

下面是代码(注解已删除)

// Helper class for construction
public static class ProjectionEqualityComparer
{
    public static ProjectionEqualityComparer<TSource, TKey>
        Create<TSource, TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }

    public static ProjectionEqualityComparer<TSource, TKey>
        Create<TSource, TKey> (TSource ignored,
                               Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }
}

public static class ProjectionEqualityComparer<TSource>
{
    public static ProjectionEqualityComparer<TSource, TKey>
        Create<TKey>(Func<TSource, TKey> projection)
    {
        return new ProjectionEqualityComparer<TSource, TKey>(projection);
    }
}

public class ProjectionEqualityComparer<TSource, TKey>
    : IEqualityComparer<TSource>
{
    readonly Func<TSource, TKey> projection;
    readonly IEqualityComparer<TKey> comparer;

    public ProjectionEqualityComparer(Func<TSource, TKey> projection)
        : this(projection, null)
    {
    }

    public ProjectionEqualityComparer(
        Func<TSource, TKey> projection,
        IEqualityComparer<TKey> comparer)
    {
        projection.ThrowIfNull("projection");
        this.comparer = comparer ?? EqualityComparer<TKey>.Default;
        this.projection = projection;
    }

    public bool Equals(TSource x, TSource y)
    {
        if (x == null && y == null)
        {
            return true;
        }
        if (x == null || y == null)
        {
            return false;
        }
        return comparer.Equals(projection(x), projection(y));
    }

    public int GetHashCode(TSource obj)
    {
        if (obj == null)
        {
            throw new ArgumentNullException("obj");
        }
        return comparer.GetHashCode(projection(obj));
    }
}

第二种解决方案

要仅针对Distinct执行此操作,可以在MoreLINQ中使用DistinctBy扩展:

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
        (this IEnumerable<TSource> source,
         Func<TSource, TKey> keySelector)
    {
        return source.DistinctBy(keySelector, null);
    }

    public static IEnumerable<TSource> DistinctBy<TSource, TKey>
        (this IEnumerable<TSource> source,
         Func<TSource, TKey> keySelector,
         IEqualityComparer<TKey> comparer)
    {
        source.ThrowIfNull("source");
        keySelector.ThrowIfNull("keySelector");
        return DistinctByImpl(source, keySelector, comparer);
    }

    private static IEnumerable<TSource> DistinctByImpl<TSource, TKey>
        (IEnumerable<TSource> source,
         Func<TSource, TKey> keySelector,
         IEqualityComparer<TKey> comparer)
    {
        HashSet<TKey> knownKeys = new HashSet<TKey>(comparer);
        foreach (TSource element in source)
        {
            if (knownKeys.Add(keySelector(element)))
            {
                yield return element;
            }
        }
    }

在这两种情况下,ThrowIfNull看起来像这样:

public static void ThrowIfNull<T>(this T data, string name) where T : class
{
    if (data == null)
    {
        throw new ArgumentNullException(name);
    }
}
3j86kqsm

3j86kqsm2#

基于Charlie Flowers的答案,你可以创建自己的扩展方法来做你想做的事情,在内部使用分组:

public static IEnumerable<T> Distinct<T, U>(
        this IEnumerable<T> seq, Func<T, U> getKey)
    {
        return
            from item in seq
            group item by getKey(item) into gp
            select gp.First();
    }

你也可以创建一个从EqualityComparer派生的泛型类,但听起来你想避免这样做:

public class KeyEqualityComparer<T,U> : IEqualityComparer<T>
    {
        private Func<T,U> GetKey { get; set; }

        public KeyEqualityComparer(Func<T,U> getKey) {
            GetKey = getKey;
        }

        public bool Equals(T x, T y)
        {
            return GetKey(x).Equals(GetKey(y));
        }

        public int GetHashCode(T obj)
        {
            return GetKey(obj).GetHashCode();
        }
    }
sshcrbum

sshcrbum3#

这是我能想到的最好的方法来解决这个问题。不过我还是很好奇是否有一种很好的方法来动态地创建一个EqualityComparer

Galleries.SelectMany(x => x.Images).ToLookup(x => x.id).Select(x => x.First());

创建查找表并从每个查找表中取“top”
注意:这和@charlie建议的一样,但是使用了ILookup --我认为这是一个群组必须具备的功能。

juud5qan

juud5qan4#

那么一个废弃的IEqualityComparer泛型类呢?

public class ThrowAwayEqualityComparer<T> : IEqualityComparer<T>
{
  Func<T, T, bool> comparer;

  public ThrowAwayEqualityComparer(Func<T, T, bool> comparer)   
  {
    this.comparer = comparer;
  }

  public bool Equals(T a, T b)
  {
    return comparer(a, b);
  }

  public int GetHashCode(T a)
  {
    return a.GetHashCode();
  }
}

所以现在你可以使用Distinct和一个自定义比较器。

var distinctImages = allImages.Distinct(
   new ThrowAwayEqualityComparer<GalleryImage>((a, b) => a.Key == b.Key));

您可能可以使用<GalleryImage>,但我不确定编译器是否可以推断出类型(现在无法访问它)。
并且在附加的扩展方法中:

public static class IEnumerableExtensions
{
  public static IEnumerable<TValue> Distinct<TValue>(this IEnumerable<TValue> @this, Func<TValue, TValue, bool> comparer)
  {
    return @this.Distinct(new ThrowAwayEqualityComparer<TValue>(comparer);
  }

  private class ThrowAwayEqualityComparer...
}
2g32fytz

2g32fytz5#

你可以按键值进行分组,然后从每个组中选择最上面的一项。这对你有用吗?

n53p2ov0

n53p2ov06#

here正在讨论这个想法,虽然我希望.NET Core团队采用一种从lambda生成IEqualityComparer<T>的方法,但我建议您对这个想法进行投票和评论,并使用以下内容:
使用方法:

IEqualityComparer<Contact> comp1 = EqualityComparerImpl<Contact>.Create(c => c.Name);
var comp2 = EqualityComparerImpl<Contact>.Create(c => c.Name, c => c.Age);

class Contact { public Name { get; set; } public Age { get; set; } }

验证码:

public class EqualityComparerImpl<T> : IEqualityComparer<T>
{
  public static EqualityComparerImpl<T> Create(
    params Expression<Func<T, object>>[] properties) =>
    new EqualityComparerImpl<T>(properties);

  PropertyInfo[] _properties;
  EqualityComparerImpl(Expression<Func<T, object>>[] properties)
  {
    if (properties == null)
      throw new ArgumentNullException(nameof(properties));

    if (properties.Length == 0)
      throw new ArgumentOutOfRangeException(nameof(properties));

    var length = properties.Length;
    var extractions = new PropertyInfo[length];
    for (int i = 0; i < length; i++)
    {
      var property = properties[i];
      extractions[i] = ExtractProperty(property);
    }
    _properties = extractions;
  }

  public bool Equals(T x, T y)
  {
    if (ReferenceEquals(x, y))
      //covers both are null
      return true;
    if (x == null || y == null)
      return false;
    var len = _properties.Length;
    for (int i = 0; i < _properties.Length; i++)
    {
      var property = _properties[i];
      if (!Equals(property.GetValue(x), property.GetValue(y)))
        return false;
    }
    return true;
  }

  public int GetHashCode(T obj)
  {
    if (obj == null)
      return 0;

    var hashes = _properties
        .Select(pi => pi.GetValue(obj)?.GetHashCode() ?? 0).ToArray();
    return Combine(hashes);
  }

  static int Combine(int[] hashes)
  {
    int result = 0;
    foreach (var hash in hashes)
    {
      uint rol5 = ((uint)result << 5) | ((uint)result >> 27);
      result = ((int)rol5 + result) ^ hash;
    }
    return result;
  }

  static PropertyInfo ExtractProperty(Expression<Func<T, object>> property)
  {
    if (property.NodeType != ExpressionType.Lambda)
      throwEx();

    var body = property.Body;
    if (body.NodeType == ExpressionType.Convert)
      if (body is UnaryExpression unary)
        body = unary.Operand;
      else
        throwEx();

    if (!(body is MemberExpression member))
      throwEx();

    if (!(member.Member is PropertyInfo pi))
      throwEx();

    return pi;

    void throwEx() =>
      throw new NotSupportedException($"The expression '{property}' isn't supported.");
  }
}
z9smfwbn

z9smfwbn7#

这里有一篇有趣的文章,它扩展了LINQ以达到这个目的...
默认的Distinct根据对象的hashcode比较对象-为了轻松地使您的对象与Distinct一起工作,您可以覆盖GetHashcode方法..但您提到您正在从Web服务中检索对象,因此在这种情况下您可能无法做到这一点。

u59ebvdq

u59ebvdq8#

这是currently in .NET 8 preview 2。OP将使用

var distinctImages = allImages.Distinct<GalleryImage>(
    EqualityComparer<GalleryImage>.Create((a, b) => a.id == b.id));

代替他们建议的

var distinctImages = allImages.Distinct<GalleryImage>(
    new EqualityComparer<GalleryImage>((a, b) => a.id == b.id));

更多信息:
https://github.com/dotnet/runtime/pull/75212

ryevplcw

ryevplcw9#

在GalleryImage上实现IEquatable,因为它是生成的
另一种方法是将GalleryImage生成为分部类,然后使用另一个具有继承和IEquatable、Equals、GetHash实现的文件。

相关问题