title | author | date |
---|---|---|
LINQ |
Tony Dwire |
2021-02-03 |
_ _ _ _ _ _ _ _ _ _
(_) (_)(_)(_) (_) _ (_) _(_)(_)(_)(_)_
(_) (_) (_)(_)_ (_) (_) (_)
(_) (_) (_) (_)_ (_) (_) (_)
(_) (_) (_) (_)_ (_) (_) _ (_)
(_) (_) (_) (_)(_) (_) (_) _ (_)
(_) _ _ _ _ _ (_) _ (_) (_) (_)_ _ _(_) _
(_)(_)(_)(_)(_) (_)(_)(_) (_) (_) (_)(_)(_) (_)
-
What is LINQ
-
Before and After LINQ
-
Using complex LINQ
-
Writing our own LINQ-style extension methods
Linq is a collection of interfaces and methods in the System.Linq
namespace that make it easy to work with collections of data.
- LINQ stands for Language Integrated Query
- Many extension methods for
IEnumerable<T>
interface - Also introduces some other interfaces, such as
IGrouping
,IQueryable
, etc. - All of these interfaces implement
IEnumerable<T>
so we will stick to looking at that for this video - Extremely useful for working with collections - maybe the most useful collection library in existence
There are two main ways of using the LINQ functionality. One is via the query language:
var numbers = new int[] { 1, 2, 3, 4, 5 };
var odd =
from number in numbers
where number % 2 == 0
select number;
Console.WriteLine(odd); // { 1, 3, 5 }
The other is via the extension methods that the System.Linq
namespace provides:
var numbers = new int[] { 1, 2, 3, 4, 5 };
var odd = numbers.Where(n => n % 2 == 0);
Console.WriteLine(odd); // { 1, 3, 5 }
IEnumerable<T>
is a forward iterating, read only interface over a collection of T
's
int[] numbers = new int[] { 1, 2, 3, 4, 5 };
IEnumerable<int> enumerable = numbers; // This gives us a way to look at numbers in a forward iterating, read-only way
// Alternatively, we could write it like this:
IEnumerable<int> enumerable = new int[] { 1, 2, 3, 4, 5 };
This may sound limiting, but what we can do with this concept goes very deep. For instance, we can get a sum:
IEnumerable<int> enumerable = new int[] { 1, 2, 3, 4, 5 };
int sum = 0;
var enumerator = enumerable.GetEnumerator();
while(enumerator.MoveNext())
{
sum += enumerator.Current;
}
Assert.Equal(15, sum);
The extension methods in the System.Linq
namespace give us a set of easy to perform, nicely wrapped common operations that we can use on collections implementing IEnumerable<T>
that we do not have to write ourselves. In this, and the next few slides we will go over the more common ones and the way they let us transform our code.
As an example of what we will be looking for, they can transform our code from something like this:
IEnumerable<Person> allPeople = GetSomePeople();
List<string> fourOrMoreLetters = new List<string>();
foreach(var person in allPeople)
{
if(person.Name.Length > 4)
{
fourOrMoreLetters.Add(person.Name);
}
}
To code that looks like this:
var fourOrMoreLetters = GetSomePeople().Select(person => person.Name).Where(name => name.Length > 4);
Where
is probably the most used extension method for me personally. It is used to filter the source and produce a new IEnumerable<T>
that only produces elements that pass the predicate's test.
Given the following class and array:
public class Person
{
public string Name { get; set; }
public string Age { get; set; }
}
var people = new Person[]
{
new Person { Name = "Bob", Age = 30 },
new Person { Name = "Jack", Age = 50 },
new Person { Name = "Beth", Age = 42 }
};
var startsWithB = new List<Person>();
foreach(var person in people)
{
if(person.Name.StartsWith("B"))
{
startsWithB.Add(person);
}
}
var startsWithB = people.Where(person => person.Name.StartsWith("B"));
Select
is the second most common one for me. It is 'map' from other languages. It is used to take one sequence of elements and turn it into another sequence of elements, one for each input.
var names = new List<string>();
foreach(var person in people)
{
names.Add(person.Name);
}
var names = people.Select(person => person.Name);
It is also important to note that the input and output of this method do not have to be particularly related, you could return a random number for each one, for instance.
var doubledAges = people.Select(person => person.Age * 2); // [ 60, 100, 84 ]
var thirdLetters = people.Select(person => person.Name[2]); // [ 'b', 'c', 't' ]
The Any
and All
extension methods determine if any or all of the elements satisfy some predicate.
bool all30OrUnder = true;
foreach(var person in people)
{
if(person.Age > 30)
{
all30OrUnder = false;
break;
}
}
// Using `All`
bool all30OrUnder = people.All(person => person.Age <= 30);
// Using `Any`
bool all30OrUnder = !people.Any(person => person.Age > 30);
These extension methods provide an easy way to get the min, max, average or sum of some predicate over a collection. They have 'default predicate' implementations for the built in numeric types, and other types can specify their own predicate.
int sum = 0;
int count = 0;
foreach(var person in people)
{
sum += person.Age;
count += 1;
}
int average = sum / count;
double average = people.Average(person => person.Age);
Note, for collections of numeric builtin types, you don't have to supply a predicate.
// Don't do this:
double average = (new int[] { 1, 2, 3, 4, 5 }).Average(x => x);
// Instead do this:
double average = (new int[] { 1, 2, 3, 4, 5 }).Average();
These give simple access to the beginning and ending elements of a collection as well as counting items in a collection. They can also be given a predicate to determine the first/last/count where some condition is true.
Person lastStartingWithB = null;
foreach(var person in people)
{
if(person.Name.StartsWith("B"))
{
lastStartingWithB = person;
}
}
var last = people.LastOrDefault(person => person.Name.StartsWith("B"));
int moreThan5Letters = 0;
foreach(var person in people)
{
if(person.Name.Length > 5)
{
moreThan5Letters += 1;
}
}
int moreThan5Letters = people.Count(person => person.Name.Length > 5);
Cast
and OfType
are similar but not exactly the same. Cast
will go through each item in a collection and try to cast it to the destination type, and if it fails it will throw an exception. OfType
will go through each item in a collection, determine if it can be casted, and return only those that can be casted as the destination type.
List<Truck> trucks = new List<Truck>();
foreach(Vehicle vehicle in vehicles)
{
trucks.Add((Truck)vehicle);
}
List<Truck> trucks = vehicles.Cast<Truck>();
List<Helicopter> helicopters = new List<Helicopter>();
foreach(Vehicle vehicle in vehicles)
{
if(vehicle is Helicopter helicopter)
{
helicopters.Add(helicopter);
}
}
List<Helicopter> = vehicles.OfType<Helicopter>();
OrderBy
and OrderByDescending
allow for easy sorting of collections with a specified predicate that returns the thing to be sorted by.
List<Person> orderedByAge = new List<Person>();
foreach (var person in people)
{
int insertIndex = 0;
for (int i = 0; i < orderedByAge.Count; i++)
{
if (person.Age <= orderedByAge[i].Age)
{
break;
}
insertIndex += 1;
}
orderedByAge.Insert(insertIndex, person);
}
List<Person> orderedByAge = people.OrderBy(person => person.Age);
Allows easy grouping of items based on some 'key'.
Dictionary<char, List<Person>> grouped = new Dictionary<char, List<Person>>();
foreach(var person in people)
{
if(!grouped.ContainsKey(person.Name[0]))
{
grouped.Add(person.Name[0], new List<Person>());
}
grouped[person.Name[0]].Add(person);
}
var grouped = people.GroupBy(person => person.Name[0]);
What if we wanted to get the letter of the alphabet with the oldest (on average) people that have that letter as the first letter of their name?
Workflow would be..
- Group everyone by the first letter of their name
- Average the ages for each group
- Order the groups by the average age
- Take the first one
var people = new Person[]
{
new Person { Name = "Tony", Age = 32 },
new Person { Name = "Tabatha", Age = 48 },
new Person { Name = "Bob", Age = 38 }
};
var oldestAverageLetter = people.GroupBy(person => person.Name[0])
.Select(group => new { Letter = group.Key, SummedAges = group.Average(person => person.Age) })
.OrderByDescending(group => group.AverageAge)
.First();
Console.WriteLine(highestLetterAndAge); // { Letter = "T", AverageAge = 40 }
The predicates we have seen so far have all been defined as lambdas. This is not the only way to do it though. Anything that can be used as a delegate matching the required type for the LINQ method can be used.
var youngEnoughPeople = people.Where(IsYoungEnough);
bool IsYoungEnough(Person person)
{
return person.Age < 20;
}
Func<Person, bool> oldEnough = person => person.Age > 100;
var oldEnoughPeople = people.Where(oldEnough);