اذهب إلى المحتوى

استخدام استعلامات LINQ في dot NET


رضوى العربي

الاستعلامات التكميلية اللغوية (Language Integrated Query - LINQ) هي تَعبيرات برمجية (expressions)، تَجلْب بيانات مُعينة من مَصدر بيانات (data source). تُوفِر استعلامات LINQ نَموذج مُتجانِس لتسهيل التعامل مع البيانات من مُختَلَف أنواع وصِيغْ مصادر البيانات (data sources). عند استخدامك لاستعلامات LINQ، فأنت دومًا تتعامل مع كائنات (objects)، وتَستَخدِم نفس الأنماط البرمجية الأساسية لجَلْب وتَغيير البيانات سواء كانت مُخزَّنة بملفات XML، أو بقواعد البيانات SQL، أو تقنية ADO.NET أو تَجمِيعَات ‎.NET، أو أيّ صيغْْة أخرى مُتاح لها مُوفِّر (provider). يُمكن استخدام LINQ بلغتي C#‎ و VB.

التابع SelectMany (ربط مسطح flat map)

لكل عنصر دخْل، يُعِيد التابع Enumerable.Select عنصر خْرج وحيد مُناظِر. في المقابل، يُسمَح للتابع Enumerable.SelectMany بإعادة أيّ عدد مُناظِر من عناصر الخْرج. بالتالي، قد يكون عدد عناصر مُتتالِية الخْرج مِن التابع SelectMany غَيْر مُساو لعدد عناصر مُتتالية الدخْل.

تُمرَّر دالة مُجردة (Lambda expression) لكِلا التابعين، وينبغي لتلك الدالة إعادة عدد من القيم بما يتوافق مع ما ذُكِر بالأعلى.

مثال:

class Invoice
{
    public int Id { get; set; }
}


class Customer
{
    public Invoice[] Invoices {get;set;}
}


var customers = new[] {
    new Customer {
        Invoices = new[] {
            new Invoice {Id=1},
            new Invoice {Id=2},
        }
    },
    new Customer {
        Invoices = new[] {
            new Invoice {Id=3},
            new Invoice {Id=4},
        }
    },
    new Customer {
        Invoices = new[] {
            new Invoice {Id=5},
            new Invoice {Id=6},
        }
    }
};


var allInvoicesFromAllCustomers = customers.SelectMany(c => c.Invoices);
Console.WriteLine(
    string.Join(",", allInvoicesFromAllCustomers.Select(i => i.Id).ToArray()));

الخْرج:

1,2,3,4,5,6

مِثال حي

يُمكن للاستعلام المُعتمِد على الصيغة (syntax-based query) تَنفيذ مُهمة التابع Enumerable.SelectMany باستخدام عِبارتي from متتاليتين كالتالي:

var allInvoicesFromAllCustomers
    = from customer in customers
    from invoice in customer.Invoices
    select invoice;

التابع Where (تَرشِيح filter)

يُعيد التابع Where مُُعدَّد من الواجهة IEnumerable، والذي يَحوِي جميع العناصر التي تُحقِق شرط الدالة المُجردة (lambda expression) المُمرَّرة.

مِثال:

var personNames = new[]
{
    "Foo", "Bar", "Fizz", "Buzz"
};


var namesStartingWithF = personNames.Where(p => p.StartsWith("F"));
Console.WriteLine(string.Join(",", namesStartingWithF));

الخْرج:

Foo,Fizz

مِثال حي

التابع Any

يُعيد التابع Any القيمة المنطقية true إذا حَوَت التَجمِيعَة أيّة عناصر تُحقِق شرط الدالة المُجردة (lambda expression) المُمرَّرة. انظر المثال التالي:

var numbers = new[] {1,2,3,4,5};


var isNotEmpty = numbers.Any();
Console.WriteLine(isNotEmpty); //True


var anyNumberIsOne = numbers.Any(n => n == 1);
Console.WriteLine(anyNumberIsOne); //True


var anyNumberIsSix = numbers.Any(n => n == 6);
Console.WriteLine(anyNumberIsSix); //False


var anyNumberIsOdd = numbers.Any(n => (n & 1) == 1);
Console.WriteLine(anyNumberIsOdd); //True


var anyNumberIsNegative = numbers.Any(n => n < 0);
Console.WriteLine(anyNumberIsNegative); //False

التابع GroupJoin

class Developer
{
    public int Id { get; set; }
    public string Name { get; set; }
}


class Project
{
    public int DeveloperId { get; set; }
    public string Name { get; set; }
}


var developers = new[] {
    new Developer {
        Id = 1,
        Name = "Foobuzz"
    },
    new Developer {
        Id = 2,
        Name = "Barfizz"
    }
};


var projects = new[] {
    new Project {
        DeveloperId = 1,
        Name = "Hello World 3D"
    },
    new Project {
        DeveloperId = 1,
        Name = "Super Fizzbuzz Maker"
    },
    new Project {
        DeveloperId = 2,
        Name = "Citizen Kane - The action game"
    },
    new Project {
        DeveloperId = 2,
        Name = "Pro Pong 2016"
    }
};


var grouped = developers.GroupJoin(
    inner: projects,
    outerKeySelector: dev => dev.Id,
    innerKeySelector: proj => proj.DeveloperId,
    resultSelector:
    (dev, projs) => new {
        DeveloperName = dev.Name,
        ProjectNames = projs.Select(p => p.Name).ToArray()});
foreach(var item in grouped)
{
    Console.WriteLine(
        "{0}'s projects: {1}",
        item.DeveloperName,
        string.Join(", ", item.ProjectNames));
}


//Foobuzz's projects: Hello World 3D, Super Fizzbuzz Maker
//Barfizz's projects: Citizen Kane - The action game, Pro Pong 2016

التابع Except

var numbers = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbersBetweenSixAndFourteen = new[] { 6, 8, 10, 12 };


var result = numbers.Except(evenNumbersBetweenSixAndFourteen);


Console.WriteLine(string.Join(",", result));
//1, 2, 3, 4, 5, 7, 9

التابع Zip

بِدءًا من الإصدار 4.0 أو أحدث.

var tens = new[] {10,20,30,40,50};
var units = new[] {1,2,3,4,5};


var sums = tens.Zip(units, (first, second) => first + second);


Console.WriteLine(string.Join(",", sums));
//11,22,33,44,55

التابع Aggregate (طوي fold)

يَستقبِل التابع Aggregate المُعامِل seed، الذي يَحوِي قيمة التهيئة للمُراكِم (accumulator). في حالة تمرير سلسلة نصية كالمثال التالي، سيُنشَّئ كائن جديد مع كل خطوة أي بِعَدَد عناصر المُعدَّد.

var elements = new[] {1,2,3,4,5};


var commaSeparatedElements = elements.Aggregate(
    seed: "",
    func: (aggregate, element) => $"{aggregate}{element},");
Console.WriteLine(commaSeparatedElements);
//1,2,3,4,5,

لاستخدام نفس الكائن بجميع الخطوات، مَرِّر كائنًا من النوع StringBuilder للمُعامِل seed كالتالي:

var commaSeparatedElements2 = elements.Aggregate(
    seed: new StringBuilder(),
    func: (seed, element) => seed.Append($"{element},"));
Console.WriteLine(commaSeparatedElements2.ToString());
//1,2,3,4,5,

يَتحكَم المُعامِل resultSelector بصيغة الخْرج:

var commaSeparatedElements3 = elements.Aggregate(
    seed: new StringBuilder(),
    func: (seed, element) => seed.Append($"{element},"),
    resultSelector: (seed) => seed.ToString());
Console.WriteLine(commaSeparatedElements3); //1,2,3,4,5,

إذا لم تُمَرَّر قيمة للمُعامِل seed، تؤول قيمته بشكل افتراضي لأول عنصر بالمُعدَّد.

var seedAndElements = elements.Select(n=>n.ToString());
var commaSeparatedElements4 = seedAndElements.Aggregate(
    func: (aggregate, element) => $"{aggregate}{element},");
Console.WriteLine(commaSeparatedElements4);
//12,3,4,5,

التابع ToLookup

var persons = new[] {
    new { Name="Fizz", Job="Developer"},
    new { Name="Buzz", Job="Developer"},
    new { Name="Foo", Job="Astronaut"},
    new { Name="Bar", Job="Astronaut"},
};


var groupedByJob = persons.ToLookup(p => p.Job);


foreach(var theGroup in groupedByJob)
{
    Console.WriteLine(
        "{0} are {1}s",
        string.Join(",", theGroup.Select(g => g.Name).ToArray()),
        theGroup.Key);
}


//Fizz,Buzz are Developers
//Foo,Bar are Astronauts

التابع Intersect

var numbers1to10 = new[] {1,2,3,4,5,6,7,8,9,10};
var numbers5to15 = new[] {5,6,7,8,9,10,11,12,13,14,15};


var numbers5to10 = numbers1to10.Intersect(numbers5to15);


Console.WriteLine(string.Join(",", numbers5to10));


//5,6,7,8,9,10

التابع Concat

var numbers1to5 = new[] {1, 2, 3, 4, 5};
var numbers4to8 = new[] {4, 5, 6, 7, 8};


var numbers1to8 = numbers1to5.Concat(numbers4to8);


Console.WriteLine(string.Join(",", numbers1to8));


//1,2,3,4,5,4,5,6,7,8

يحتفظ التابع Concat بالعناصر المُكرّرة دون حَذفِها. اِستخدِم التابع Union إذا لم يكن ذلك ما ترغب به.

التابع All

var numbers = new[] {1,2,3,4,5};
var allNumbersAreOdd = numbers.All(n => (n & 1) == 1);


Console.WriteLine(allNumbersAreOdd); //False


var allNumbersArePositive = numbers.All(n => n > 0);
Console.WriteLine(allNumbersArePositive); //True

يُعيِد التابع All القيمة المنطقية false فقط عندما يجد عنصرًا بالمُعدَّد لا يُحقق شرط الدالة المُجردة (lambda expression). بالتالي، إذا كان المُعدَّد فارغًا، سيُعيد التابع القيمة المنطقية true دومًا بغض النظر عن شرط الدالة المجردة، كالمثال التالي:

var numbers = new int[0];
var allNumbersArePositive = numbers.All(n => n > 0);
Console.WriteLine(allNumbersArePositive); //True

التابع Sum

var numbers = new[] {1,2,3,4};


var sumOfAllNumbers = numbers.Sum();
Console.WriteLine(sumOfAllNumbers); //10


var cities = new[] {
    new {Population = 1000},
    new {Population = 2500},
    new {Population = 4000}
};


var totalPopulation = cities.Sum(c => c.Population);
Console.WriteLine(totalPopulation); //7500

التابع SequenceEqual

var numbers = new[] {1,2,3,4,5};
var sameNumbers = new[] {1,2,3,4,5};
var sameNumbersInDifferentOrder = new[] {5,1,4,2,3};


var equalIfSameOrder = numbers.SequenceEqual(sameNumbers);
Console.WriteLine(equalIfSameOrder); //True


var equalIfDifferentOrder = numbers.SequenceEqual(sameNumbersInDifferentOrder);
Console.WriteLine(equalIfDifferentOrder); //False

التابع Min

var numbers = new[] {1,2,3,4};


var minNumber = numbers.Min();
Console.WriteLine(minNumber); //1


var cities = new[] {
    new {Population = 1000},
    new {Population = 2500},
    new {Population = 4000}
};


var minPopulation = cities.Min(c => c.Population);
Console.WriteLine(minPopulation); //1000

التابع Distinct

var numbers = new[] {1, 1, 2, 2, 3, 3, 4, 4, 5, 5};
var distinctNumbers = numbers.Distinct();


Console.WriteLine(string.Join(",", distinctNumbers));


//1,2,3,4,5

التابع Count

IEnumerable<int> numbers = new[] {1,2,3,4,5,6,7,8,9,10};


var numbersCount = numbers.Count();
Console.WriteLine(numbersCount); //10


var evenNumbersCount = numbers.Count(n => (n & 1) == 0);
Console.WriteLine(evenNumbersCount); //5

التابع Cast

يَختلف التابع Cast عن بقية توابع النوع Enumerable من جِهة كَوْنه تابعًا مُوسِّعًا للواجهة IEnumerable لا نظيرتها المُعمَّمة IEnumerable<T>‎. بالتالي، قد يُستَخَدم لتحويل نُسخ من الواجهة غير المُعمَّمة لآخرى مُعمَّمة.

فمثلًا، لا يجتاز المثال التالي عملية التَصرِّيف (compilation)؛ نظرًا لأن التابع First -كغالبية توابع النوع Enumerable- هو تابِع موسِّع للواجهة IEnumerable<T>‎، ولمّا كان النوع ArrayList لا يُنَفِذ تلك الوَاجِهة وإنما يُنَفِذ الواجهة غيْر المُعمَّمة IEnumerable، تَفشل عملية التَصرِّيف.

var numbers = new ArrayList() {1,2,3,4,5};
Console.WriteLine(numbers.First());

في المُقابِل، يَعمل المثال التالي بشكل سليم:

var numbers = new ArrayList() {1,2,3,4,5};
Console.WriteLine(numbers.Cast<int>().First()); //1

لا يُجرِي التابع Cast عملية التحويل بين اﻷنواع (Casting). فمثلًا، في حين تجتاز الشيفرة التالية عملية التَصرِّيف (compilation)، يُبلَّغ عن اعتراض من النوع InvalidCastException خلال زمن التشغيل (runtime).

var numbers = new int[] {1,2,3,4,5};
decimal[] numbersAsDecimal = numbers.Cast<decimal>().ToArray();

إذا أردت إجراء عملية التحويل بين الأنواع (casting) على التَجميعَات بطريقة صحيحة، نفذ التالي:

var numbers= new int[] {1,2,3,4,5};
decimal[] numbersAsDecimal = numbers.Select(n => (decimal)n).ToArray();

التابع Range

يَستقبِل التابع Range مُعامِلين، هما قيمة أول رقم بالمُُعدَّد، ورقم يُعبِر عن عدد العناصر بالمُعَّدد النَاتِج -لا قيمة آخر رقم.

// prints 1,2,3,4,5,6,7,8,9,10
Console.WriteLine(string.Join(",", Enumerable.Range(1, 10)));


// prints 10,11,12,13,14
Console.WriteLine(string.Join(",", Enumerable.Range(10, 5)));

التابع ThenBy

لا يُستخَدَم التابع ThenBy إلا بعد استدعاء التابع OrderBy، مما يَسمَح بترتيب عناصر المُُعدَّد وفقًا لأكثر من معيار.

var persons
{
    new {Id = 1, Name = "Foo", Order = 1},
    new {Id = 1, Name = "FooTwo", Order = 2},
    new {Id = 2, Name = "Bar", Order = 2},
    new {Id = 2, Name = "BarTwo", Order = 1},
    new {Id = 3, Name = "Fizz", Order = 2},
    new {Id = 3, Name = "FizzTwo", Order = 1},
};


var personsSortedByName = persons.OrderBy(p => p.Id).ThenBy(p => p.Order);


Console.WriteLine(string.Join(",", personsSortedByName.Select(p => p.Name)));


//This will display :
//Foo,FooTwo,BarTwo,Bar,FizzTwo,Fizz

التابع Repeat

يُعيد التابع Enumerable.Repeat مُتتالية من عدة عناصر تَحمِل جميعها نفس القيمة. يُنتج المثال التالي مُتتالية مكونة من أربع عناصر، يَحوي كلَا منها السلسلة النصية “Hello”.

var repeats = Enumerable.Repeat("Hello", 4);
foreach (var item in repeats)
{
    Console.WriteLine(item);
}
/* output:
    Hello
    Hello
    Hello
    Hello
*/

التابع Empty

يُنشِئ التابع Empty مُعدَّدًا فارغًا من الواجهة IEnumerable<T>‎. يَستقبِل التابع نوع المُعدَّد كمُعامِل نوع (type parameter). فمثلًا، يُنشِئ المثال التالي مُُعدَّد من النوع int:

IEnumerable<int> emptyList = Enumerable.Empty<int>();

يُخزن التابع Empty كل نوع تم انشائه من المتُعدَّدات الفارغة IEnumerable<T>‎ تخزينًا مؤقتًا (caching) ويُعيِد استخدامه عند الحاجة لتنشئة نسخة جديدة من نفس النوع، وبالتالي:

Enumerable.Empty<decimal>() == Enumerable.Empty<decimal>(); // This is True
Enumerable.Empty<int>() == Enumerable.Empty<decimal>();
// This is False

التابع Select (ربط map)

var persons
{
    new {Id = 1, Name = "Foo"},
    new {Id = 2, Name = "Bar"},
    new {Id = 3, Name = "Fizz"},
    new {Id = 4, Name = "Buzz"},
};


var names = persons.Select(p => p.Name);
Console.WriteLine(string.Join(",", names.ToArray()));


//Foo,Bar,Fizz,Buzz

تُطلِق لغات البرمجة الوَظِيفية (functional languages) اسم الرَبْط (map) على هذا النوع من الدوال.

التابع OrderBy

var persons
{
    new {Id = 1, Name = "Foo"},
    new {Id = 2, Name = "Bar"},
    new {Id = 3, Name = "Fizz"},
    new {Id = 4, Name = "Buzz"},
};


var personsSortedByName = persons.OrderBy(p => p.Name);


Console.WriteLine(string.Join(",", personsSortedByName.Select(p => p.Id).ToArray()));


//2,4,3,1

التابع OrderByDescending

var persons
{
    new {Id = 1, Name = "Foo"},
    new {Id = 2, Name = "Bar"},
    new {Id = 3, Name = "Fizz"},
    new {Id = 4, Name = "Buzz"},
};


var personsSortedByNameDescending = persons.OrderByDescending(p => p.Name);


Console.WriteLine(string.Join(",", personsSortedByNameDescending.Select(p => p.Id).ToArray()));


//1,3,4,2

التابع Contains

var numbers = new[] {1,2,3,4,5};
Console.WriteLine(numbers.Contains(3)); //True
Console.WriteLine(numbers.Contains(34)); //False

التابع First (بحث وجلب find)

var numbers = new[] {1,2,3,4,5};


var firstNumber = numbers.First();
Console.WriteLine(firstNumber); //1


var firstEvenNumber = numbers.First(n => (n & 1) == 0);
Console.WriteLine(firstEvenNumber); //2

عند عدم تواجد أي عناصر تُحقِق شرط الدالة المجردة المُمرَّرة للتابع، يُبلَّغ عن اعتراض من النوع InvalidOperationException مَصحُوبًا بالرسالة التالية "لا تحتوي المتتالية على أية عناصر مُتوافِقة".

var firstNegativeNumber = numbers.First(n => n < 0);

التابع Single

var oneNumber = new[] {5};
var theOnlyNumber = oneNumber.Single();
Console.WriteLine(theOnlyNumber); //5


var numbers = new[] {1,2,3,4,5};


var theOnlyNumberSmallerThanTwo = numbers.Single(n => n < 2);
Console.WriteLine(theOnlyNumberSmallerThanTwo); //1

عند تواجد أكثر من عنصر يُحقِق شرط الدالة المجردة أو عند عدم وجوده نهائيًا كاﻷمثلة التالية، يُبلِّغ التابع Single عن اعتراض من النوع InvalidOperationException.

var theOnlyNumberInNumbers = numbers.Single();
var theOnlyNegativeNumber = numbers.Single(n => n < 0);

التابع Last

var numbers = new[] {1,2,3,4,5};
var lastNumber = numbers.Last();
Console.WriteLine(lastNumber); //5


var lastEvenNumber = numbers.Last(n => (n & 1) == 0);
Console.WriteLine(lastEvenNumber); //4

في المثال التالي، يُبلَّغ عن اعتراض من النوع InvalidOperationException لعدَم وجود أيّ عنصر يُحقِق شرط الدالة المُجردَّة:

var lastNegativeNumber = numbers.Last(n => n < 0);

التابع LastOrDefault

var numbers = new[] {1,2,3,4,5};


var lastNumber = numbers.LastOrDefault();
Console.WriteLine(lastNumber); //5


var lastEvenNumber = numbers.LastOrDefault(n => (n & 1) == 0);
Console.WriteLine(lastEvenNumber); //4


var lastNegativeNumber = numbers.LastOrDefault(n => n < 0);
Console.WriteLine(lastNegativeNumber); //0


var words = new[] { "one", "two", "three", "four", "five" };
var lastWord = words.LastOrDefault();
Console.WriteLine(lastWord); // five


var lastLongWord = words.LastOrDefault(w => w.Length > 4);
Console.WriteLine(lastLongWord); // three


var lastMissingWord = words.LastOrDefault(w => w.Length > 5);
Console.WriteLine(lastMissingWord); // null

التابع SingleOrDefault

var oneNumber = new[] {5};
var theOnlyNumber = oneNumber.SingleOrDefault();
Console.WriteLine(theOnlyNumber); //5


var numbers = new[] {1,2,3,4,5};


var theOnlyNumberSmallerThanTwo = numbers.SingleOrDefault(n => n < 2);
Console.WriteLine(theOnlyNumberSmallerThanTwo); //1


var theOnlyNegativeNumber = numbers.SingleOrDefault(n => n < 0);
Console.WriteLine(theOnlyNegativeNumber); //0

يَختلف عن التابع Single عند عدم تواجد أي عنصر يُحقِق شرط الدالة المجردة، ففي هذه الحالة، لا يُبلِّغ التابع SingleOrDefault عن اعتراض، وإنما يُعيد القيمة الافتراضية للنوع الذي يحتويه المُُعدَّد. أما في حالة تواجد أكثر من عنصر، فإنه -مثل التابع Single- يُبلِّغ عن اعتراض من النوع InvalidOperationException كالمثال التالي:

var theOnlyNumberInNumbers = numbers.SingleOrDefault();

التابع FirstOrDefault

var numbers = new[] {1,2,3,4,5};


var firstNumber = numbers.FirstOrDefault();
Console.WriteLine(firstNumber); //1


var firstEvenNumber = numbers.FirstOrDefault(n => (n & 1) == 0);
Console.WriteLine(firstEvenNumber); //2


var firstNegativeNumber = numbers.FirstOrDefault(n => n < 0);
Console.WriteLine(firstNegativeNumber); //0


var words = new[] { "one", "two", "three", "four", "five" };


var firstWord = words.FirstOrDefault();
Console.WriteLine(firstWord); // one


var firstLongWord = words.FirstOrDefault(w => w.Length > 3);
Console.WriteLine(firstLongWord); // three


var firstMissingWord = words.FirstOrDefault(w => w.Length > 5);
Console.WriteLine(firstMissingWord); // null

التابع Skip

يقوم التابع Skip بتَخطِي أول مجموعة من العناصر بالمُعدَّد. عدد تلك العناصر يُحدِّده مُعامل دَخْل يُمرَّر للتابع. عندما يَصِل التابع Skip إلى أول عنصر بعد تلك المجموعة، يبدأ بإعادة قيم العناصر عند تَعدِّيدها.

var numbers = new[] {1,2,3,4,5};


var allNumbersExceptFirstTwo = numbers.Skip(2);
Console.WriteLine(string.Join(",", allNumbersExceptFirstTwo.ToArray()));


//3,4,5

التابع Take

يَجِلْب التابع Take أول مجموعة عناصر من المُعدَّد، ويحدِّد عددها مُعامل دَخْل يُمرَّر للتابع.

var numbers = new[] {1,2,3,4,5};
var threeFirstNumbers = numbers.Take(3);


Console.WriteLine(string.Join(",", threeFirstNumbers.ToArray()));


//1,2,3

التابع Reverse

var numbers = new[] {1,2,3,4,5};
var reversed = numbers.Reverse();


Console.WriteLine(string.Join(",", reversed.ToArray()));


//5,4,3,2,1

التابع OfType

var mixed = new object[] {1,"Foo",2,"Bar",3,"Fizz",4,"Buzz"};
var numbers = mixed.OfType<int>();


Console.WriteLine(string.Join(",", numbers.ToArray()));


//1,2,3,4

التابع Max

var numbers = new[] {1,2,3,4};


var maxNumber = numbers.Max();
Console.WriteLine(maxNumber); //4


var cities = new[] {
    new {Population = 1000},
    new {Population = 2500},
    new {Population = 4000}
};


var maxPopulation = cities.Max(c => c.Population);
Console.WriteLine(maxPopulation); //4000

التابع Average

var numbers = new[] {1,2,3,4};


var averageNumber = numbers.Average();
Console.WriteLine(averageNumber);
// 2,5

يَحسِب التابع Average في المثال باﻷعلى قيمة مُتوسِط مُعدَّد من النوع العَدَدي.

var cities = new[] {
    new {Population = 1000},
    new {Population = 2000},
    new {Population = 4000}
};


var averagePopulation = cities.Average(c => c.Population);
Console.WriteLine(averagePopulation);
// 2333,33

يَحِسب التابع في المثال بالأعلى قيمة مُتوسِط المُعدَّد بالاعتماد على مُفوِّض (delegated function) يُمرَّر له.

التابع GroupBy

var persons = new[] {
    new { Name="Fizz", Job="Developer"},
    new { Name="Buzz", Job="Developer"},
    new { Name="Foo", Job="Astronaut"},
    new { Name="Bar", Job="Astronaut"},
};
var groupedByJob = persons.GroupBy(p => p.Job);
foreach(var theGroup in groupedByJob)
{
    Console.WriteLine(
        "{0} are {1}s",
        string.Join(",", theGroup.Select(g => g.Name).ToArray()),
        theGroup.Key);
}
//Fizz,Buzz are Developers
//Foo,Bar are Astronauts

في المثال التالي، تُجَمَّع الفواتير وفقًا لقيمة الدولة، ويُنشَّئ لكلًا منها كائن جديد يحتوي على عدد السِجلات، والقيمة المدفوعة الكلية، ومتوسط القيمة المدفوعة:

var a = db.Invoices.GroupBy(i => i.Country)
    .Select(g => new { Country = g.Key,
                      Count = g.Count(),
                      Total = g.Sum(i => i.Paid),
                      Average = g.Average(i => i.Paid) });

إذا أردنا المدفوعات فقط بدون مجموعات:

var a = db.Invoices.GroupBy(i => 1)
    .Select(g => new { Count = g.Count(),
                      Total = g.Sum(i => i.Paid),
                      Average = g.Average(i => i.Paid) });

إذا أردنا أكثر من تِعداد:

var a = db.Invoices.GroupBy(g => 1)
    .Select(g => new { High = g.Count(i => i.Paid >= 1000),
                      Low = g.Count(i => i.Paid < 1000),
                      Sum = g.Sum(i => i.Paid) });

التابع ToDictionary

يُحول التابع ToDictionary مُعامِل مُعدَّد من الوَاجِهة IEnumerable -يُدعى المصدر source- إلى قاموس (Dictionary)، بالاستعانة بمُعامِل دالة مُجردة تُمرَّر للتابع -تُدعى keySelector- لتحديد مفاتيح القاموس (keys).

يُبلِّغ التابع عن اعتراض من النوع ArgumentException إذا كانت الدالة المُجردَّة keySelector غير مُتباينة (الدالة المُتباينة injective هي دالة تعيد قيمة فريدة (unique) لكل عنصر بمُعامل التَجمِيعَة source).

تتوفر توابع التحميل الزائد (overloads) من التابع ToDictionary، والتي تَستقبِل دوال مجردة لتخصيص مفاتيح (keys) وقيم (value) القاموس.

var persons = new[] {
    new { Name="Fizz", Id=1},
    new { Name="Buzz", Id=2},
    new { Name="Foo", Id=3},
    new { Name="Bar", Id=4},
};

إذا ما خُصِّصَت دالة اختيار المفتاح فقط، سيُنشَّئ قاموس من النوع Dictionary<TKey,TVal>‎. بحيث يكون نوع المفتاح TKey من نفس نوع القيمة العائدة من الدالة KeySelector، بينما يكون نوع القيمة TVal من نفس نوع الكائن اﻷصلي ويحمل قيمته.

var personsById = persons.ToDictionary(p => p.Id);
// personsById is a Dictionary<int,object>


Console.WriteLine(personsById[1].Name); //Fizz
Console.WriteLine(personsById[2].Name); //Buzz

أما إذا خُصّصت دالة اختيار القيمة ValueSelector أيضًا، فسيُنشَّئ قاموس من النوع Dictionary<TKey,TVal>‎. بحيث يكون نوع المفتاح TKey من نفس نوع القيمة العائدة من الدالة KeySelector، ويكون نوع القيمة TVal من نفس نوع القيمة العائدة من الدالة ValueSelector ويحمل قيمتها الفِعلية تِباعًا.

var namesById = persons.ToDictionary(p => p.Id, p => p.Name);
//namesById is a Dictionary<int,string>


Console.WriteLine(namesById[3]); //Foo
Console.WriteLine(namesById[4]); //Bar

كما ذُكر باﻷعلى، يجب أن تكون المفاتيح -التي تعيدها دالة اختيار المفاتيح- فريدة. في المثال التالي، يُبلَّغ عن اعتراض لمُخالفة هذا الشرط:

var persons = new[] {
    new { Name="Fizz", Id=1},
    new { Name="Buzz", Id=2},
    new { Name="Foo", Id=3},
    new { Name="Bar", Id=4},
    new { Name="Oops", Id=4}
};


var willThrowException = persons.ToDictionary(p => p.Id)

إذا لم يكن بالإمكان تخصيص مفتاح فريد لكل عنصر بالتَجمِيعَة، يُمكِنك استخدام التابع ToLookUp. ظاهريًا، يعمل التابع ToLookup بصورة مشابهة للتابع ToDictionary، بِخلاف إمكانية ربطه لكل مفتاح بتَجمِيعَة من القيم التي تَحمِل نفس المفتاح.

التابع Union

var numbers1to5 = new[] {1,2,3,4,5};
var numbers4to8 = new[] {4,5,6,7,8};


var numbers1to8 = numbers1to5.Union(numbers4to8);


Console.WriteLine(string.Join(",", numbers1to8));


//1,2,3,4,5,6,7,8

يَحذِف التابع Union العناصر المُكرّرة. اِستخدِم التابع Concat إذا لم يكن ذلك ما ترغب به.

التابع ToArray

var numbers = new[] {1,2,3,4,5,6,7,8,9,10};
var someNumbers = numbers.Where(n => n < 6);


Console.WriteLine(someNumbers.GetType().Name);
//WhereArrayIterator`1


var someNumbersArray = someNumbers.ToArray();


Console.WriteLine(someNumbersArray.GetType().Name);
//Int32[]

التابع ToList

var numbers = new[] {1,2,3,4,5,6,7,8,9,10};
var someNumbers = numbers.Where(n => n < 6);


Console.WriteLine(someNumbers.GetType().Name);
//WhereArrayIterator`1


var someNumbersList = someNumbers.ToList();


Console.WriteLine(
    someNumbersList.GetType().Name + " - " +
    someNumbersList.GetType().GetGenericArguments()[0].Name);
//List`1 - Int32

التابع List.ForEach

التابع ForEach مُعرف بالصنف List<T>‎، وليس بأيّ من الوَاجِهتين IQueryable<T>‎ أو IEnumerable<T>‎. ومِنْ ثَمَّ إذا أردت استدعاء هذا التابع من خلال كائن من احدى هاتين الوَاجِهتين، فلديك خيارين:

  • الخيار اﻷول: استدعاء التابع ToList أولًا

عند استدعاء التابع ToList، سيحدث شيئًا من اثنين اعتمادًا على وَاجِهة الكائن. إذا كان الكائن من الوَاجِهة IEnumerable<T>‎، ستُجرَى عملية تِعِداد (enumeration) للمُعدَّد، مما يؤدي إلى نَسخ النتيجة إلى قائمة جديدة (List). أما إذا كان الكائن من الوَاجِهة IQueryable<T>‎، فستُجرَى عملية اتصال بقاعدة البيانات لتنفيذ عبارة استعلام (query) مُعيّنة. وأخيرًا، سيُستدعَى التابع ForEach لكل عنصر بالقائمة. مثلًا:

IEnumerable<Customer> customers = new List<Customer>();


customers.ToList().ForEach(c => c.SendEmail());

تُعاني هذه الطريقة مِن استهلاك غير ضروري للذاكرة بسبب الاضطرار لإنشاء مؤقت لقائمة (List).

  • الخيار الثاني: استخدام التابع المُوسِّع (Extension Method)

أضِف التابع المُوسِّع التالي للواجهة IEnumerable<T>‎ كالتالي:

public static void ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
{
    foreach(T item in enumeration)
    {
        action(item);
    }
}

ثم اِستَدعه مباشرة من خلال الوَاجِهة كالتالي:

IEnumerable<Customer> customers = new List<Customer>();


customers.ForEach(c => c.SendEmail());

تنبيه: صُممت توابع الاستعلامات التكميلية اللغوية LINQ لتكون نَقيَّة (pure methods)، مما يَعنيّ عدم تَسَبّبهم بأيّ تأثيرات جانبية (side effects). يَنحرِف التابع ForEach عن بقية التوابع من هذه الناحية لأن هدفه اﻷوحد هو إحِداث تأثير جانبي. بدلًا من ذلك، قد تُنفِذ التِكرار (loop) باستخدام الكلمة المفتاحية foreach، وهذا، في الواقع، ما تَفعله الدالة المُوسِّعة باﻷعلى داخليًا.

في حالة كنت تُريد إجراء التكرار على مُتغير من النوع List، يُمكنك استدعاء التابع ForEach مُباشِرة دون الحاجة إلى أي توابع مُوسِّعة، كالتالي:

public class Customer {
    public void SendEmail()
    {
        // Sending email code here
    }
}


List<Customer> customers = new List<Customer>();


customers.Add(new Customer());
customers.Add(new Customer());


customers.ForEach(c => c.SendEmail());

التابع ElementAt

يُبلِّغ التابع ElementAt عن اعتراض من النوع ArgumentOutOfRangeException عند تمرِّير فِهرَس (index) خارج مدى (Range) المُتعدَّد.

var names = new[] {"Foo","Bar","Fizz","Buzz"};


var thirdName = names.ElementAt(2);
Console.WriteLine(thirdName); //Fizz

سيُبلَّغ عن اعتراض ArgumentOutOfRangeException في المثال التالي:

var names = new[] {"Foo","Bar","Fizz","Buzz"};


var minusOnethName = names.ElementAt(-1);
var fifthName = names.ElementAt(4);

التابع ElementAtOrDefault

var names = new[] {"Foo","Bar","Fizz","Buzz"};


var thirdName = names.ElementAtOrDefault(2);
Console.WriteLine(thirdName); //Fizz


var minusOnethName = names.ElementAtOrDefault(-1);
Console.WriteLine(minusOnethName); //null


var fifthName = names.ElementAtOrDefault(4);
Console.WriteLine(fifthName); //null

التابع SkipWhile

var numbers = new[] {2,4,6,8,1,3,5,7};


var oddNumbers = numbers.SkipWhile(n => (n & 1) == 0);


Console.WriteLine(string.Join(",", oddNumbers.ToArray()));


//1,3,5,7

التابع TakeWhile

var numbers = new[] {2,4,6,1,3,5,7,8};


var evenNumbers = numbers.TakeWhile(n => (n & 1) == 0);


Console.WriteLine(string.Join(",", evenNumbers.ToArray()));


//2,4,6

التابع DefaultIfEmpty

var numbers = new[] {2,4,6,8,1,3,5,7};


var numbersOrDefault = numbers.DefaultIfEmpty();
Console.WriteLine(numbers.SequenceEqual(numbersOrDefault)); //True


var noNumbers = new int[0];


var noNumbersOrDefault = noNumbers.DefaultIfEmpty();
Console.WriteLine(noNumbersOrDefault.Count()); //1
Console.WriteLine(noNumbersOrDefault.Single()); //0


var noNumbersOrExplicitDefault = noNumbers.DefaultIfEmpty(34);
Console.WriteLine(noNumbersOrExplicitDefault.Count()); //1
Console.WriteLine(noNumbersOrExplicitDefault.Single()); //34

التابع Join

class Developer
{
    public int Id { get; set; }
    public string Name { get; set; }
}


class Project
{
    public int DeveloperId { get; set; }
    public string Name { get; set; }
}


var developers = new[] {
    new Developer {
        Id = 1,
        Name = "Foobuzz"
    },
    new Developer {
        Id = 2,
        Name = "Barfizz"
    }
};


var projects = new[] {
    new Project {
        DeveloperId = 1,
        Name = "Hello World 3D"
    },
    new Project {
        DeveloperId = 1,
        Name = "Super Fizzbuzz Maker"
    },
    new Project {
        DeveloperId = 2,
        Name = "Citizen Kane - The action game"
    },
    new Project {
        DeveloperId = 2,
        Name = "Pro Pong 2016"
    }
};


var denormalized = developers.Join(
    inner: projects,
    outerKeySelector: dev => dev.Id,
    innerKeySelector: proj => proj.DeveloperId,
    resultSelector:
    (dev, proj) => new {
        ProjectName = proj.Name,
        DeveloperName = dev.Name});


foreach(var item in denormalized)
{
    Console.WriteLine("{0} by {1}", item.ProjectName, item.DeveloperName);
}


//Hello World 3D by Foobuzz
//Super Fizzbuzz Maker by Foobuzz
//Citizen Kane - The action game by Barfizz
//Pro Pong 2016 by Barfizz

مثال آخر عن التابع Join مع تطبيق الضم الخارجي اليساري (left outer join):

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}
class Pet
{
    public string Name { get; set; }
    public Person Owner { get; set; }
}
public static void Main(string[] args)
{
    var magnus = new Person { FirstName = "Magnus", LastName = "Hedlund" };
    var terry = new Person { FirstName = "Terry", LastName = "Adams" };
    var barley = new Pet { Name = "Barley", Owner = terry };
    var people = new[] { magnus, terry };
    var pets = new[] { barley };
    var query =
        from person in people
        join pet in pets on person equals pet.Owner into gj
        from subpet in gj.DefaultIfEmpty()
        select new
    {
        person.FirstName,
        PetName = subpet?.Name ?? "-" // Use - if he has no pet
    };
    foreach (var p in query)
        Console.WriteLine($"{p.FirstName}: {p.PetName}");
}

ترجمة -وبتصرف- للفصل LINQ والفصل ForEach من كتاب ‎.NET Framework Notes for Professionals


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...