المشكلة: Distinct() لا يعرف التصفية حسب الحقل
\n\nتخيل: لديك قائمة بالعملاء، وفيها تكرارات حسب البريد الإلكتروني. تكتب:
\n\nvar uniqueClients = clients.Distinct().ToList();\n\nوتتفاجأ بأن التكرارات لم تختفِ. لماذا؟ لأن Distinct() القياسي يقارن الكائنات كاملة حسب المرجع (أو حسب Equals إذا قمت بإعادة تعريفه). إنه لا يعلم أن "التفرد" بالنسبة لك هو Email فريد، وليس نسخة فريدة من الكلاس.
هذا السؤال هو من أكثر الأسئلة شيوعاً على Stack Overflow حول LINQ (أكثر من 500 ألف مشاهدة). واليوم سنشرح 3 طرق عملية لحل هذه المشكلة في C#. هيا بنا!
\n\nالطريقة 1: GroupBy + First (الكلاسيكية)
\n\nأبسط وأوضح طريقة، ولا تتطلب تثبيت مكتبات خارجية. الفكرة: تجميع الكائنات حسب الخاصية المطلوبة، ثم أخذ أول عنصر من كل مجموعة.
\n\nvar uniqueByEmail = clients\n .GroupBy(c => c.Email)\n .Select(g => g.First())\n .ToList();\n\nكيف يعمل:
\n- \n
GroupBy(c => c.Email)— يقسم المجموعة إلى مجموعات فرعية، حيث المفتاح هو البريد الإلكتروني. \n Select(g => g.First())— يأخذ أول كائن من كل مجموعة فرعية. \n - يتم تجاهل التكرارات المتبقية. \n
الإيجابيات:
\n- \n
- يعمل "مباشرة من الصندوق" (فقط
using System.Linq;). \n - مفهوم بديهياً حتى للمبتدئين. \n
- يحافظ على الترتيب الأصلي للعناصر (يبقى أول كائن تم العثور عليه). \n
السلبيات:
\n- \n
- أبطأ قليلاً على البيانات الكبيرة (يحتاج لإنشاء مجموعات). \n
- الكود أطول قليلاً مما نرغب. \n
الطريقة 2: MoreLinq و DistinctBy (الأناقة)
\n\nإذا كنت لا تمانع المكتبات الخارجية، فإن MoreLinq تقدم طريقة مثالية DistinctBy. تقوم بالضبط بما تتوقعه من Distinct — التصفية حسب الخاصية.
التثبيت عبر NuGet:
\nInstall-Package MoreLinq\n\nالاستخدام:
\nusing MoreLinq;\n\nvar uniqueByEmail = clients.DistinctBy(c => c.Email).ToList();\n\nسطر واحد فقط وكل شيء جاهز. داخلياً، تستخدم DistinctBy HashSet وتعمل بتعقيد O(n).
الإيجابيات:
\n- \n
- كود موجز وقابل للقراءة للغاية. \n
- أداء عالٍ. \n
- المكتبة مختبرة جيداً (أكثر من 10 ملايين تحميل). \n
السلبيات:
\n- \n
- تحتاج لإضافة تبعية للمشروع. \n
- ليست كل الفرق تسمح بالمكتبات الخارجية. \n
الطريقة 3: IEqualityComparer مخصص (التحكم الكامل)
\n\nهذه الطريقة مناسبة عندما لا تريد إضافة مكتبة، ولكنك تريد إعادة استخدام منطق المقارنة في عدة أماكن. ننشئ المقارن الخاص بنا:
\n\npublic class ClientEmailComparer : IEqualityComparer<Client>\n{\n public bool Equals(Client x, Client y)\n {\n if (ReferenceEquals(x, y)) return true;\n if (x is null || y is null) return false;\n return x.Email == y.Email;\n }\n\n public int GetHashCode(Client obj)\n {\n return obj.Email?.GetHashCode() ?? 0;\n }\n}\n\nالاستخدام:
\nvar comparer = new ClientEmailComparer();\nvar uniqueByEmail = clients.Distinct(comparer).ToList();\n\nالإيجابيات:
\n- \n
- تحكم كامل في منطق المقارنة. \n
- يمكن إعادة استخدام المقارن في
ContainsوExceptوUnionوغيرها. \n - لا يتطلب مكتبات خارجية. \n
السلبيات:
\n- \n
- الكثير من الكود النمطي. \n