Owinąć delegata w Ieequalitycomparer

Kilka Linq.Funkcje wyliczeniowe przyjmują IEqualityComparer<T>. Czy istnieje wygodna Klasa wrapper, która dostosowuje delegate(T,T)=>bool do implementacji IEqualityComparer<T>? Łatwo jest go napisać (jeśli masz problemy z zdefiniowaniem poprawnego hashcode), ale chciałbym wiedzieć, czy jest gotowe rozwiązanie.

W szczególności chcę wykonywać operacje na Dictionarys, używając tylko kluczy do zdefiniowania członkostwa (zachowując wartości zgodnie z różnymi zasadami).

Author: Marcelo Cantos, 2008-09-18

13 answers

Normalnie, chciałbym to rozwiązać, komentując @Sam na odpowiedź (zrobiłem trochę edycji na oryginalnym poście, aby oczyścić go trochę bez zmiany zachowania.)

Oto mój riff z @ Sam ' s answer, z [IMNSHO] krytyczną poprawką do domyślnej polityki haszowania:-

class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => 0 ) // NB Cannot assume anything about how e.g., t.GetHashCode() interacts with the comparer's behavior
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash )
    {
        _comparer = comparer;
        _hash = hash;
    }

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

    public int GetHashCode( T obj )
    {
        return _hash( obj );
    }
}
 46
Author: Ruben Bartelink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:47:19

O znaczeniu GetHashCode

Inni już skomentowali fakt, że każda implementacja IEqualityComparer<T> powinna zawierać metodę GetHashCode {47]}; ale nikt nie zadaje sobie trudu, aby wyjaśnić {48]} dlaczego {49]} w jakikolwiek sposób.

Oto dlaczego. Twoje pytanie konkretnie wspomina o metodach rozszerzenia LINQ; prawie wszystkie z nich polegają na kodach skrótu, aby działać poprawnie, ponieważ wykorzystują tabele skrótu wewnętrznie dla wydajności.

Take Distinct, na przykład. Rozważmy implikacje tej metody rozszerzenia, jeśli wszystkie wykorzystane były metodą Equals. Jak określić, czy element został już zeskanowany w sekwencji, jeśli masz tylko Equals? Wyliczasz na całym zbiorze wartości, na które już spojrzałeś i sprawdzasz dopasowanie. Wynikałoby to z Distinct użycia najgorszego przypadku O (N2) algorytm zamiast O (N)!

Na szczęście tak nie jest. Distinct Nie tylko używać Equals; to używa również GetHashCode. W rzeczywistości, to absolutnie nie działa prawidłowo bez IEqualityComparer<T>, który dostarcza WŁAŚCIWEGO GetHashCode. Poniżej znajduje się wymyślony przykład ilustrujący to.

Powiedzmy, że mam następujący typ:

class Value
{
    public string Name { get; private set; }
    public int Number { get; private set; }

    public Value(string name, int number)
    {
        Name = name;
        Number = number;
    }

    public override string ToString()
    {
        return string.Format("{0}: {1}", Name, Number);
    }
}

Teraz powiedzmy, że mam List<Value> i chcę znaleźć wszystkie elementy o odrębnej nazwie. Jest to idealny przypadek użycia Distinct przy użyciu niestandardowego porównywania równości. Więc użyjmy Comparer<T> klasy z odpowiedzi Aku :

var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);

Teraz, jeśli mieć kilka Value elementów o tej samej właściwości Name, Wszystkie powinny się zwinąć w jedną wartość zwracaną przez Distinct, prawda? Zobaczmy...

var values = new List<Value>();

var random = new Random();
for (int i = 0; i < 10; ++i)
{
    values.Add("x", random.Next());
}

var distinct = values.Distinct(comparer);

foreach (Value x in distinct)
{
    Console.WriteLine(x);
}

Wyjście:

x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Nie zadziałało, prawda?

A co z GroupBy? Spróbujmy:

var grouped = values.GroupBy(x => x, comparer);

foreach (IGrouping<Value> g in grouped)
{
    Console.WriteLine("[KEY: '{0}']", g);
    foreach (Value x in g)
    {
        Console.WriteLine(x);
    }
}

Wyjście:

[KEY = 'x: 1346013431']
x: 1346013431
[KEY = 'x: 1388845717']
x: 1388845717
[KEY = 'x: 1576754134']
x: 1576754134
[KEY = 'x: 1104067189']
x: 1104067189
[KEY = 'x: 1144789201']
x: 1144789201
[KEY = 'x: 1862076501']
x: 1862076501
[KEY = 'x: 1573781440']
x: 1573781440
[KEY = 'x: 646797592']
x: 646797592
[KEY = 'x: 655632802']
x: 655632802
[KEY = 'x: 1206819377']
x: 1206819377
Znowu: nie zadziałało.

Jeśli się nad tym zastanowić, byłoby sensowne, aby Distinct użyć HashSet<T> (lub równoważnego) wewnętrznie, a GroupBy użyć czegoś w rodzaju a Dictionary<TKey, List<T>> wewnętrznie. Czy to może wyjaśnić, dlaczego te metody nie działają? Spróbujmy:

var uniqueValues = new HashSet<Value>(values, comparer);

foreach (Value x in uniqueValues)
{
    Console.WriteLine(x);
}

Wyjście:

x: 1346013431
x: 1388845717
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Tak... zaczyna to mieć sens?

Mam nadzieję, że z tych przykładów jasno wynika, dlaczego włączenie odpowiedniej GetHashCode do dowolnej implementacji IEqualityComparer<T> jest tak ważne.


Oryginalna odpowiedź

Odpowiedź oripa :

Jest kilka ulepszeń, które można tutaj wprowadzić.

  1. Po Pierwsze, chciałbym należy użyć Func<T, TKey> zamiast Func<T, object>; zapobiegnie to umieszczaniu kluczy typu value w samym keyExtractor.
  2. po drugie, dodałbym ograniczenie where TKey : IEquatable<TKey>; zapobiegnie to boks w wywołaniu Equals (object.Equals przyjmuje parametr object; potrzebujesz implementacji IEquatable<TKey>, aby pobrać parametr TKey bez boksu). Oczywiście może to stanowić zbyt poważne ograniczenie, więc można utworzyć klasę bazową bez ograniczenia i klasę pochodną z nim.

Oto co wynikowy kod może wyglądać następująco:

public class KeyEqualityComparer<T, TKey> : IEqualityComparer<T>
{
    protected readonly Func<T, TKey> keyExtractor;

    public KeyEqualityComparer(Func<T, TKey> keyExtractor)
    {
        this.keyExtractor = keyExtractor;
    }

    public virtual bool Equals(T x, T y)
    {
        return this.keyExtractor(x).Equals(this.keyExtractor(y));
    }

    public int GetHashCode(T obj)
    {
        return this.keyExtractor(obj).GetHashCode();
    }
}

public class StrictKeyEqualityComparer<T, TKey> : KeyEqualityComparer<T, TKey>
    where TKey : IEquatable<TKey>
{
    public StrictKeyEqualityComparer(Func<T, TKey> keyExtractor)
        : base(keyExtractor)
    { }

    public override bool Equals(T x, T y)
    {
        // This will use the overload that accepts a TKey parameter
        // instead of an object parameter.
        return this.keyExtractor(x).Equals(this.keyExtractor(y));
    }
}
 172
Author: Dan Tao,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:18:14

Gdy chcesz dostosować sprawdzanie równości, w 99% przypadków interesuje Cię zdefiniowanie kluczy do porównania, a nie samo porównanie.

To może być eleganckie rozwiązanie (koncepcja z metody sortowania list w Pythonie ).

Użycie:

var foo = new List<string> { "abc", "de", "DE" };

// case-insensitive distinct
var distinct = foo.Distinct(new KeyEqualityComparer<string>( x => x.ToLower() ) );

Klasa KeyEqualityComparer:

public class KeyEqualityComparer<T> : IEqualityComparer<T>
{
    private readonly Func<T, object> keyExtractor;

    public KeyEqualityComparer(Func<T,object> keyExtractor)
    {
        this.keyExtractor = keyExtractor;
    }

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

    public int GetHashCode(T obj)
    {
        return this.keyExtractor(obj).GetHashCode();
    }
}
 119
Author: orip,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-08-06 14:41:23

Obawiam się, że nie ma takiego pudełka. Jednak nie jest trudno go stworzyć:

class Comparer<T>: IEqualityComparer<T>
{
    private readonly Func<T, T, bool> _comparer;

    public Comparer(Func<T, T, bool> comparer)
    {
        if (comparer == null)
            throw new ArgumentNullException("comparer");

        _comparer = comparer;
    }

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

    public int GetHashCode(T obj)
    {
        return obj.ToString().ToLower().GetHashCode();
    }
}

...

Func<int, int, bool> f = (x, y) => x == y;
var comparer = new Comparer<int>(f);
Console.WriteLine(comparer.Equals(1, 1));
Console.WriteLine(comparer.Equals(1, 2));
 48
Author: aku,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-18 23:52:01

Tak samo jak odpowiedź dana Tao, ale z kilkoma ulepszeniami:

  1. Opiera się na EqualityComparer<>.Default, aby dokonać rzeczywistego porównania, aby uniknąć boksowania dla typów wartości (struct s), które zaimplementowały IEquatable<>.

  2. Ponieważ EqualityComparer<>.Default używane nie eksploduje na null.Equals(something).

  3. Pod warunkiem statycznego wrappera wokół IEqualityComparer<>, który będzie miał statyczną metodę do tworzenia instancji wywołania comparer - eases. Porównaj

    Equality<Person>.CreateComparer(p => p.ID);
    

    Z

    new EqualityComparer<Person, int>(p => p.ID);
    
  4. Dodano przeciążenie, aby określić IEqualityComparer<> dla klucza.

Klasa:

public static class Equality<T>
{
    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector)
    {
        return CreateComparer(keySelector, null);
    }

    public static IEqualityComparer<T> CreateComparer<V>(Func<T, V> keySelector, 
                                                         IEqualityComparer<V> comparer)
    {
        return new KeyEqualityComparer<V>(keySelector, comparer);
    }

    class KeyEqualityComparer<V> : IEqualityComparer<T>
    {
        readonly Func<T, V> keySelector;
        readonly IEqualityComparer<V> comparer;

        public KeyEqualityComparer(Func<T, V> keySelector, 
                                   IEqualityComparer<V> comparer)
        {
            if (keySelector == null)
                throw new ArgumentNullException("keySelector");

            this.keySelector = keySelector;
            this.comparer = comparer ?? EqualityComparer<V>.Default;
        }

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

        public int GetHashCode(T obj)
        {
            return comparer.GetHashCode(keySelector(obj));
        }
    }
}

Możesz go używać w następujący sposób:

var comparer1 = Equality<Person>.CreateComparer(p => p.ID);
var comparer2 = Equality<Person>.CreateComparer(p => p.Name);
var comparer3 = Equality<Person>.CreateComparer(p => p.Birthday.Year);
var comparer4 = Equality<Person>.CreateComparer(p => p.Name, StringComparer.CurrentCultureIgnoreCase);

Osoba jest prostą klasą:

class Person
{
    public int ID { get; set; }
    public string Name { get; set; }
    public DateTime Birthday { get; set; }
}
 22
Author: ldp615,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-04-15 14:56:18
public class FuncEqualityComparer<T> : IEqualityComparer<T>
{
    readonly Func<T, T, bool> _comparer;
    readonly Func<T, int> _hash;

    public FuncEqualityComparer( Func<T, T, bool> comparer )
        : this( comparer, t => t.GetHashCode())
    {
    }

    public FuncEqualityComparer( Func<T, T, bool> comparer, Func<T, int> hash )
    {
        _comparer = comparer;
        _hash = hash;
    }

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

    public int GetHashCode( T obj )
    {
        return _hash( obj );
    }
}

Z rozszerzeniami: -

public static class SequenceExtensions
{
    public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer ) );
    }

    public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> comparer, Func<T, int> hash )
    {
        return first.SequenceEqual( second, new FuncEqualityComparer<T>( comparer, hash ) );
    }
}
 11
Author: Ruben Bartelink,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-09-15 16:10:38

Odpowiedź Oripa jest świetna.

Oto mała metoda rozszerzenia, aby było jeszcze łatwiej:

public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, object>    keyExtractor)
{
    return list.Distinct(new KeyEqualityComparer<T>(keyExtractor));
}
var distinct = foo.Distinct(x => x.ToLower())
 6
Author: Bruno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-05-27 09:44:57

Odpowiem na własne pytanie. Aby traktować Słowniki jako zestawy, najprostszą metodą wydaje się stosowanie operacji set do dict.Klawiszy, a następnie konwertować z powrotem do słowników z Enumerable.ToDictionary(...).

 2
Author: Marcelo Cantos,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-19 00:09:28

Implementacja at (tekst niemiecki) implementacja Ieequalitycompare with lambda expression dba o wartości null i używa metod rozszerzeń do generowania Ieequalitycomparer.

Aby utworzyć IEqualityComparer w LINQ union wystarczy napisać

persons1.Union(persons2, person => person.LastName)

The comparer:

public class LambdaEqualityComparer<TSource, TComparable> : IEqualityComparer<TSource>
{
  Func<TSource, TComparable> _keyGetter;

  public LambdaEqualityComparer(Func<TSource, TComparable> keyGetter)
  {
    _keyGetter = keyGetter;
  }

  public bool Equals(TSource x, TSource y)
  {
    if (x == null || y == null) return (x == null && y == null);
    return object.Equals(_keyGetter(x), _keyGetter(y));
  }

  public int GetHashCode(TSource obj)
  {
    if (obj == null) return int.MinValue;
    var k = _keyGetter(obj);
    if (k == null) return int.MaxValue;
    return k.GetHashCode();
  }
}

Musisz również dodać metodę rozszerzenia do obsługi wnioskowania typu

public static class LambdaEqualityComparer
{
       // source1.Union(source2, lambda)
        public static IEnumerable<TSource> Union<TSource, TComparable>(
           this IEnumerable<TSource> source1, 
           IEnumerable<TSource> source2, 
            Func<TSource, TComparable> keySelector)
        {
            return source1.Union(source2, 
               new LambdaEqualityComparer<TSource, TComparable>(keySelector));
       }
   }
 2
Author: Fried,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-06-29 11:26:55

Tylko jedna optymalizacja: Możemy użyć gotowego narzędzia EqualityComparer do porównywania wartości, zamiast delegowania go.

To również uczyniłoby implementację czystszą, ponieważ faktyczna logika porównywania pozostaje teraz w GetHashCode() i Equals (), które mogły być już przeciążone.

Oto kod:

public class MyComparer<T> : IEqualityComparer<T> 
{ 
  public bool Equals(T x, T y) 
  { 
    return EqualityComparer<T>.Default.Equals(x, y); 
  } 

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

Nie zapomnij przeciążyć metod GetHashCode() i Equals () na swoim obiekcie.

Ten post mi pomógł: c # compare two generic wartości

Sushil

 1
Author: Sushil,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:54:43

Odpowiedź Oripa jest świetna. Rozwiń odpowiedź oripa:

Myślę, że kluczem rozwiązania jest użycie " metody rozszerzenia "do przeniesienia"anonimowego typu".

    public static class Comparer 
    {
      public static IEqualityComparer<T> CreateComparerForElements<T>(this IEnumerable<T> enumerable, Func<T, object> keyExtractor)
      {
        return new KeyEqualityComparer<T>(keyExtractor);
      }
    }

Użycie:

var n = ItemList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList();
n.AddRange(OtherList.Select(s => new { s.Vchr, s.Id, s.Ctr, s.Vendor, s.Description, s.Invoice }).ToList(););
n = n.Distinct(x=>new{Vchr=x.Vchr,Id=x.Id}).ToList();
 1
Author: matrix,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:10:27
public static Dictionary<TKey, TValue> Distinct<TKey, TValue>(this IEnumerable<TValue> items, Func<TValue, TKey> selector)
  {
     Dictionary<TKey, TValue> result = null;
     ICollection collection = items as ICollection;
     if (collection != null)
        result = new Dictionary<TKey, TValue>(collection.Count);
     else
        result = new Dictionary<TKey, TValue>();
     foreach (TValue item in items)
        result[selector(item)] = item;
     return result;
  }

Umożliwia to wybór właściwości z lambda w następujący sposób: .Select(y => y.Article).Distinct(x => x.ArticleID);

 0
Author: Max,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-10-10 10:15:41

Nie znam istniejącej klasy, ale coś w stylu:

public class MyComparer<T> : IEqualityComparer<T>
{
  private Func<T, T, bool> _compare;
  MyComparer(Func<T, T, bool> compare)
  {
    _compare = compare;
  }

  public bool Equals(T x, Ty)
  {
    return _compare(x, y);
  }

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

Uwaga: jeszcze tego nie skompilowałem i nie uruchomiłem, więc może być jakaś literówka lub inny błąd.

 -2
Author: Gregg,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-18 23:55:44