起因
今天在公司做一個需求的時候,寫的是面條代碼,一個方法直接從頭寫到尾,其中用到了GroupBy
,且GroupBy
的KeySelector
是多個屬性而不是單個屬性。
但是公司最近推行Clean Code,要讓代碼有可讀性。且作為一個有追求的程序員,肯定是不能寫面條代碼的,要對代碼進行拆分。
重構前GroupBy
大概是這樣子的:
var groups = data.GroupBy(m => new { m.PropertyA, m.PropertyB})
個人對於短的Linq比較習慣於用方法而不是用關鍵字的那種寫法。
一開始這樣寫是沒問題的,但是重構的時候問題就來了:這個groups
是什么類型?
重構以后這個groups
是要作為參數進入到別的方法中的,方法簽名顯然是不能用var
做類型推導,必須指定確定的類型。
我們知道GroupBy
出來的東西是個泛型的東西,簽名是IEnumerable<IGrouping<TKey, TSource>>
,這個TSource
類型是沒問題,我沒有對Source
做修改,就是data
本身的類型。
但是這個Key
就有問題了。
我沒有指定Key
的類型,這里應該是匿名類型,於是定義了一個類型承接Key
,代碼變成了:
class EntityKey
{
public int PropertyA { get set; }
public string PropertyB { get set; }
}
......
var groups = data.GroupBy(m => new EntityKey { PropertyA = m.PropertyA, PropertyB = m.PropertyB});
但是后來我發現這樣有問題,GroupBy
指定的Key
失效了。也就是說,groups
的分組數量與data
的長度一致,每一個group
里面只有一個對象。
分析
發現這個問題后,我仔細思考了一下,大致猜到了問題出在哪里。
GroupBy
這種東西,判斷兩個對象是不是一個分組,必然用到了相等判斷。
雖然我沒有看匿名類型反編譯生成后的IL
代碼,不知道之前用的是怎么做的Key相等判斷,但是引用類型的肯定是直接用對象的HashCode
做判斷。
這樣子肯定是不行的,要解決引用類型的相等判斷問題。
重現
根據猜測,我寫了一個Sample程序最小化的重現了這個問題:
class Program
{
static void Main(string[] args)
{
var list = new List<Student>();
list.Add(new Student(1, "Cat", 10, "University1"));
list.Add(new Student(2, "Dog", 10, "University1"));
list.Add(new Student(3, "Pig", 10, "University2"));
list.Add(new Student(4, "Fish", 12, "University1"));
var groups = list.GroupBy(m => new {m.Age, m.Class});
foreach (var group in groups)
{
Console.WriteLine("Age:{0},Class:{1}", group.Key.Age, group.Key.Class);
foreach (var student in group)
{
Console.WriteLine(student);
}
}
}
class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public string Class { get; set; }
public Student(int id, string name, int age, string @class)
{
Id = id;
Name = name;
Age = age;
Class = @class;
}
public override string ToString()
{
return $"Id={Id},Name={Name},Age={Age},Class={Class}";
}
}
class StudentKey
{
public int Age { get; set; }
public string Class { get; set; }
}
}
這時候輸出結果是
Age:10,Class:University1
Id=1,Name=Cat,Age=10,Class=University1
Id=2,Name=Dog,Age=10,Class=University1
Age:10,Class:University2
Id=3,Name=Pig,Age=10,Class=University2
Age:12,Class:University1
Id=4,Name=Fish,Age=12,Class=University1
將new {m.Age, m.Class}
替換為new StudentKey {Age = m.Age, Class = m.Class}
,結果卻變成了
Age:10,Class:University1
Id=1,Name=Cat,Age=10,Class=University1
Age:10,Class:University1
Id=2,Name=Dog,Age=10,Class=University1
Age:10,Class:University2
Id=3,Name=Pig,Age=10,Class=University2
Age:12,Class:University1
Id=4,Name=Fish,Age=12,Class=University1
Id=1
和Id=2
變成了兩組。
解決問題
解決問題方式有幾種。
第一種
最簡單,就是直接將StudentKey
從class
變成struct
。
但是這樣有個問題,class
是堆內存,struct
是棧內存。
雖然實際情況不一定會出現內存異常什么的,但是總歸是改變了一些東西,存在隱患。
第二種
第一種方式被我自己否決后,於是打開了Google搜了一下,在StackOverflow和MSDN以及查看GroupBy
源碼之后,得到了GroupBy
的運行原理。
GroupBy
在沒有傳comparer
的時候,會創建一個基於當前TSource
類型的默認的comparer
。
但不管是默認的comparer
還是我們自己傳的comparer
,都會調用Equals
和GetHashCode
兩個方法,所以我們需要重載這兩個方法。
第二種方法就是我們在類型上重載Equals
和GetHashCode
兩個方法。
可以實現IEquatable<TKey>
使用下面的代碼,也可以不實現接口,使用重載的Equals
方法。
但是不論如何,一定要重載GetHashCode
。
修改后StudentKey
如下
class StudentKey : IEquatable<StudentKey>
{
public int Age { get; set; }
public string Class { get; set; }
public override int GetHashCode()
{
return Age.GetHashCode() ^ Class.GetHashCode();
}
// public override bool Equals(object obj)
// {
// var model = obj as StudentKey;
// if (model == null)
// {
// return false;
// }
//
// return model.Age == Age && model.Class == Class;
// }
public bool Equals(StudentKey other)
{
return Age == other.Age && Class == other.Class;
}
}
第三種
第三種就是傳一個comparer
給GroupBy
參數,實現一個IEqualityComparer<TKey>
。
代碼如下:
list.GroupBy(m => new StudentKey {Age = m.Age, Class = m.Class}, new StudentKeyComparer());
......
class StudentKeyComparer: IEqualityComparer<StudentKey>
{
public bool Equals(StudentKey x, StudentKey y)
{
return x.Age == y.Age && x.Class == y.Class;
}
public int GetHashCode(StudentKey obj)
{
return obj.Age.GetHashCode() ^ obj.Age.GetHashCode();
}
}
這種相對於第二種方式,最大的區別在於不用侵入實體類添加代碼,但是原理是類似的。
總結
本文是在c#開發過程中碰到的一個GroupBy
的分組的Key
失效的問題。
了解其分組原理后,通過實現Equals
和GetHashCode
或者傳入自定義的comparer
,解決GroupBy
的分組Key
失效的問題。