۱۳۸۸/۰۸/۰۶

پيدا كردن آيتم‌هاي تكراري در يك ليست به كمك LINQ


گاهي از اوقات نياز مي‌شود تا در يك ليست، آيتم‌هاي تكراري موجود را مشخص كرد. به صورت پيش فرض متد Distinct براي حذف مقادير تكراري در يك ليست با استفاده از LINQ موجود است كه البته آن‌هم اما و اگرهايي دارد كه در ادامه به آن پرداخته خواهد شد، اما باز هم اين مورد پاسخ سؤال اصلي نيست (نمي‌خواهيم موارد تكراري را حذف كنيم).

براي حذف آيتم‌هاي تكراري از يك ليست جنريك مي‌توان متد زير را نوشت:
public static List<T> RemoveDuplicates<T>(List<T> items)
{
return (from s in items select s).Distinct().ToList();
}
براي مثال:
public static void TestRemoveDuplicates()
{
List<string> sampleList =
new List<string>() { "A1", "A2", "A3", "A1", "A2", "A3" };
sampleList = RemoveDuplicates(sampleList);
foreach (var item in sampleList)
Console.WriteLine(item);
}
اين متد بر روي ليست‌هايي با نوع‌هاي اوليه مانند string‌ و int و امثال آن درست كار مي‌كند. اما اكنون مثال زير را در نظر بگيريد:
public class Employee
{
public int ID { get; set; }
public string FName { get; set; }
public int Age { get; set; }
}

public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates<Employee>(lstEmp);

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
اگر متد TestRemoveDuplicates را اجرا نمائيد، ركورد تكراري اين ليست جنريك حذف نخواهد شد؛ زيرا متد distinct بكارگرفته شده نمي‌داند اشيايي از نوع كلاس سفارشي Employee را چگونه بايد با هم مقايسه نمايد تا بتواند موارد تكراري آن‌ها را حذف كند.
براي رفع اين مشكل بايد از آرگومان دوم متد distinct جهت معرفي وهله‌اي از كلاسي كه اينترفيس IEqualityComparer را پياده سازي مي‌كند، كمك گرفت.
public static IEnumerable<TSource> Distinct<TSource>(this IEnumerable<TSource> source, IEqualityComparer<TSource> comparer);
كه نمونه‌اي از پياده سازي آن به شرح زير مي‌تواند باشد:

public class EmployeeComparer : IEqualityComparer<Employee>
{
public bool Equals(Employee x, Employee y)
{
//آيا دقيقا يك وهله هستند؟
if (Object.ReferenceEquals(x, y)) return true;

//آيا يكي از وهله‌ها نال است؟
if (Object.ReferenceEquals(x, null) ||
Object.ReferenceEquals(y, null))
return false;

return x.Age == y.Age && x.FName == y.FName && x.ID == y.ID;
}

public int GetHashCode(Employee obj)
{
if (Object.ReferenceEquals(obj, null)) return 0;
int hashTextual = obj.FName == null ? 0 : obj.FName.GetHashCode();
int hashDigital = obj.Age.GetHashCode();
return hashTextual ^ hashDigital;
}
}
اكنون اگر يك overload براي متد RemoveDuplicates با درنظر گرفتن IEqualityComparerتهيه كنيم، به شكل زير خواهد بود:
public static List<T> RemoveDuplicates<T>(List<T> items, IEqualityComparer<T> comparer)
{
return (from s in items select s).Distinct(comparer).ToList();
}
به اين صورت متد آزمايشي ما به شكل زير (كه وهله‌اي از كلاس EmployeeComparer‌ به آن ارسال شده) تغيير خواهد كرد:
public static void TestRemoveDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

lstEmp = RemoveDuplicates(lstEmp, new EmployeeComparer());

foreach (var item in lstEmp)
Console.WriteLine(item.FName);
}
پس از اين تغيير، حاصل اين متد تنها دو ركورد غيرتكراري مي‌باشد.

سؤال: براي يافتن آيتم‌هاي تكراري يك ليست چه بايد كرد؟
احتمالا مقاله "روش‌هايي براي حذف ركوردهاي تكراري" را به خاطر داريد. اينجا هم مي‌توان كوئري LINQ ايي را نوشت كه ركوردها را بر اساس سن، گروه بندي كرده و سپس گروه‌هايي را كه بيش از يك ركورد دارند، انتخاب نمايد.
public static void FindDuplicates()
{
List<Employee> lstEmp = new List<Employee>()
{
new Employee(){ ID=1, Age=20, FName="F1"},
new Employee(){ ID=2, Age=21, FName="F2"},
new Employee(){ ID=1, Age=20, FName="F1"},
};

var query = from c in lstEmp
group c by c.Age into g
where g.Count() > 1
select new { Age = g.Key, Count = g.Count() };

foreach (var item in query)
{
Console.WriteLine("Age {0} has {1} records", item.Age, item.Count);
}
}


Vote on iDevCenter