Về tầm quan trọng của GetHashCode
Những người khác đã nhận xét về thực tế rằng bất kỳ IEqualityComparer<T>
triển khai tùy chỉnh nào thực sự nên bao gồm một GetHashCode
phương thức ; nhưng không ai bận tâm để giải thích tại sao trong bất kỳ chi tiết.
Đây là lý do tại sao. Câu hỏi của bạn đặc biệt đề cập đến các phương pháp mở rộng LINQ; gần như tất cả các mã này dựa vào mã băm để hoạt động chính xác, bởi vì chúng sử dụng các bảng băm trong nội bộ để đạt hiệu quả.
Lấy Distinct
ví dụ. Hãy xem xét ý nghĩa của phương pháp mở rộng này nếu tất cả những gì nó sử dụng là một Equals
phương pháp. Làm thế nào để bạn xác định xem một mục đã được quét theo trình tự nếu bạn chỉ có Equals
? Bạn liệt kê toàn bộ bộ sưu tập các giá trị bạn đã xem và kiểm tra sự trùng khớp. Điều này sẽ dẫn đến Distinct
việc sử dụng thuật toán O (N 2 ) trong trường hợp xấu nhất thay vì thuật toán O (N)!
May mắn thay, đây không phải là trường hợp. Distinct
không chỉ sử dụng Equals
; nó sử dụng GetHashCode
là tốt. Trong thực tế, nó hoàn toàn không hoạt động đúng mà không có một IEqualityComparer<T>
nguồn cung cấp phù hợpGetHashCode
. Dưới đây là một ví dụ giả định minh họa điều này.
Nói rằng tôi có loại sau:
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);
}
}
Bây giờ nói rằng tôi có một List<Value>
và tôi muốn tìm tất cả các yếu tố với một tên riêng biệt. Đây là một trường hợp sử dụng hoàn hảo để Distinct
sử dụng một bộ so sánh bình đẳng tùy chỉnh. Vì vậy, hãy sử dụng Comparer<T>
lớp từ câu trả lời của Aku :
var comparer = new Comparer<Value>((x, y) => x.Name == y.Name);
Bây giờ, nếu chúng ta có một loạt các Value
phần tử có cùng thuộc Name
tính, tất cả chúng sẽ thu gọn thành một giá trị được trả về Distinct
, phải không? Hãy xem nào...
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);
}
Đầu ra:
x: 1346013431
x: 1388845617
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Hmm, điều đó đã không làm việc, phải không?
Thế còn GroupBy
? Hãy thử xem:
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);
}
}
Đầu ra:
[KEY = 'x: 1346013431']
x: 1346013431
[KEY = 'x: 1388845617']
x: 1388845617
[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
Một lần nữa: không hoạt động.
Nếu bạn nghĩ về nó, nó sẽ làm cho ý nghĩa đối với Distinct
sử dụng một HashSet<T>
(hoặc tương đương) trong nội bộ, và cho GroupBy
đến việc sử dụng một cái gì đó giống như một Dictionary<TKey, List<T>>
nội bộ. Điều này có thể giải thích tại sao các phương pháp này không hoạt động? Chúng ta hãy cố gắng này:
var uniqueValues = new HashSet<Value>(values, comparer);
foreach (Value x in uniqueValues)
{
Console.WriteLine(x);
}
Đầu ra:
x: 1346013431
x: 1388845617
x: 1576754134
x: 1104067189
x: 1144789201
x: 1862076501
x: 1573781440
x: 646797592
x: 655632802
x: 1206819377
Vâng ... bắt đầu có ý nghĩa?
Hy vọng từ những ví dụ này, rõ ràng tại sao bao gồm một sự phù hợp GetHashCode
trong bất kỳ IEqualityComparer<T>
triển khai nào lại quan trọng đến vậy.
Câu trả lời gốc
Mở rộng câu trả lời của orip :
Có một vài cải tiến có thể được thực hiện ở đây.
- Đầu tiên, tôi sẽ
Func<T, TKey>
thay thế Func<T, object>
; điều này sẽ ngăn quyền anh của các loại khóa giá trị trong thực tế keyExtractor
.
- Thứ hai, tôi thực sự thêm một
where TKey : IEquatable<TKey>
ràng buộc; điều này sẽ ngăn quyền anh trong Equals
cuộc gọi ( object.Equals
lấy object
tham số; bạn cần IEquatable<TKey>
triển khai để lấy TKey
tham số mà không cần quyền anh). Rõ ràng điều này có thể đặt ra một hạn chế quá nghiêm trọng, vì vậy bạn có thể tạo một lớp cơ sở mà không có sự ràng buộc và một lớp dẫn xuất với nó.
Đây là mã kết quả có thể trông như thế nào:
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));
}
}
IEqualityComparer<T>
cái gì rờiGetHashCode
khỏi chỉ là thẳng lên bị hỏng.