Introduction
In the first part of this series, we took a deep dive into IEnumerable
and began introducing some of the standard LINQ methods. These methods are all defined within the System.Linq.Enumerable
class, mostly as extension methods to IEnumerable
.
If you’re unclear on extension methods or IEnumerable
, please review the previous article:
This article makes heavy use of Lambda Expressions and Anonymous Types. If you are unfamiliar with these concepts, a very brief description of each is provided at the end of this article.
Background
In this article, we’ll conduct a quick survey of the standard LINQ methods. We’ll provide sample code and output for each. This article is simply intended to introduce you to these methods. It is not intended to make you an expert in their use.
To facilitate skim reading, the examples are intentionally as short as I can make them. They use only the simplest of the available overloads for these methods. Think of this as a list of available ingredients. Instructions for baking the cake, with these ingredients, will come in later articles.
The best way to read this article is to take a very quick pass through it. Do not worry about remembering the details. You can always refer back to this article or to the relevant Microsoft documentation. Links to this documentation are included at the end of the article.
This is the second in a series of articles on LINQ. Links to other articles in this series are as follows:
New Sequences
Some of the methods in System.Linq.Enumerable
create entirely new sequences. These include: Empty
, Range
, and Repeat
.
Empty
The Empty
method creates an empty sequence (with no items).
WriteSequence("Empty", Enumerable.Empty<int>());
Range
The Range
method creates a sequence containing a range of integer values.
WriteSequence("Range", Enumerable.Range(1, 5));
Repeat
The Repeat
method creates a sequence containing repeated occurrences of a single item.
WriteSequence("Repeat", Enumerable.Repeat("ABC", 3));
Sequences in Original Order
Some methods return a new sequence where all or a portion of the original sequence is included, in its original order. These methods do not require materialization of the sequence to begin returning items. These methods include: Append
, AsEnumerable
, Cast
, Concat
, DefaultIfEmpty
, OfType
, Prepend
, Range
, Repeat
, Select
, SelectMany
, Skip
, SkipWhile
, Take
, TakeWhile
, Where
, and Zip
.
Since they are the simplest to describe, we’ll tackle them first:
Append
The Append
method adds a single item to the end of a sequence.
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence(berries.Append("strawberry"));
AsEnumerable
The AsEnumerable
method is a bit beyond the scope of this article. Basically, it allows a transition from the other flavor of LINQ (IQueryable
) to the flavor described in this article (IEnumerable
). We’ll revisit this method in later articles and explain in more detail.
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("AsEnumerable", berries.AsEnumerable());
Cast
Sometimes, it is necessary to deal with a weakly-typed sequence, especially when dealing with older .NET data types. In these cases, the sequence may implement IEnumerable
, but not IEnumerable<T>
. When all of the data types in the sequence are known to be of the same data type, the Cast
method allows you to cast all of the items to that data type.
The Cast
method casts every item in the sequence to a different data type.
var intList = new ArrayList { 123, 456, 789 };
IEnumerable<int> ints = intList.Cast<int>();
WriteSequence("Cast", ints);
If any of items are of a different data type, than what is specified, an exception will occur. In this instance, the OfType
method would be preferable.
var list = new ArrayList { "Alpha", 123, "Beta", "Gamma" };
WriteSequence("Cast", list.Cast<string>());
Concat
The Concat
method adds items, from another sequence, to the end of a sequence.
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Concat", berries.Concat(citrus));
DefaultIfEmpty
The DefaultIfEmpty
method gets either the original sequence or a sequence with a single default value, if the original sequence is empty (contains no items).
byte[] bytes = { 1, 2, 3, 4 };
WriteSequence("DefaultIfEmpty: ", bytes.DefaultIfEmpty());
byte[] emptyBytes = { };
WriteSequence("DefaultIfEmpty: ", emptyBytes.DefaultIfEmpty());
WriteSequence("DefaultIfEmpty: ", emptyBytes.DefaultIfEmpty((byte)123));
OfType
Sometimes, it is necessary to deal with a weakly-typed sequence, especially when dealing with older .NET data types. In these cases, the sequence may implement IEnumerable
, but not IEnumerable<T>
. The OfType
method allows you to select items (of a homogenous type) from that sequence and produce an IEnumerable<T>
instance.
var list = new ArrayList { "Alpha", 123, "Beta", "Gamma" };
WriteSequence("OfType", list.OfType<string>());
Prepend
The Prepend
method adds a single item to the start of a sequence.
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Prepend", berries.Prepend("strawberry"));
Select
The Select
method alters every item in a sequence.
string[] berries = { "blackberry", "blueberry", "raspberry" };
WriteSequence("Select", berries.Select(berry => berry + " more"));
SelectMany
The SelectMany
method alters multiple sequences and flattens them into a single sequence.
int[][] manyNumbers = { new int[] { 1, 2 }, new int[] { 3, 4 }, new int[] { 5, 6 } };
WriteSequence("SelectMany", manyNumbers.SelectMany(numbers => numbers.Append(9)));
Skip
The Skip
method skips a fixed number of items at the start of a sequence.
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Skip", letters.Skip(1));
SkipWhile
The SkipWhile
skips items, at the start of a sequence, while they meet a specified condition.
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("SkipWhile", letters.SkipWhile(letter => letter < 'C'));
Take
The Take
method takes a fixed number of items from the start of a sequence.
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Take", letters.Take(1));
TakeWhile
The TakeWhile
method takes items, from the start of a sequence, while they meet a specified condition.
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("TakeWhile", letters.TakeWhile(letter => letter < 'C'));
Where
The Where
method takes all of the items that meet a specified condition.
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
WriteSequence("Where", citrus.Where(name => name.Contains("a")));
Zip
The Zip
method combines a sequence with items from another sequence.
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
char[] letters = { 'A', 'B', 'C', 'D'};
WriteSequence("Zip", citrus.Zip(letters, (fruit, letter) => $"{fruit} {letter}"));
Sequence in New Order
Some methods return a new sequence where all or a portion of the original sequence is included, but in a different order. While execution is still deferred until you begin consuming the sequence, this can be deceptive. In order to return the initial item in the sequence, these methods must first evaluate (and materialize) either a portion or all of the sequence. These methods include: Distinct
, Except
, GroupBy
, GroupJoin
, Intersect
, Join
, OrderBy
, OrderByDescending
, Reverse
, ThenBy
, ThenByDescending
, and Union
.
Distinct
The Distinct
method selects unique items from a sequence. When the same item occurs multiple in a sequence, only one instance of the item is included.
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("Distinct", stooges.Distinct());
Except
The Except
method gets the unique items from a sequence that are not present in another sequence.
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Except: ", fruits.Except(citrus));
GroupBy
The GroupBy
method groups items in a sequence by a unique trait (or key). It produces a sequence of groupings, where each grouping is itself a sequence, with an associated key value.
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("GroupBy: ", stooges.GroupBy(name => GetLastWord(name)));
The code for the GetLastWord
method is as follows:
private static string GetLastWord(string words) =>
words.Substring(words.LastIndexOf(' ') + 1);
GroupJoin
The GroupJoin
method, which is similar to a SQL LEFT OUTER JOIN
, is unusually difficult to demonstrate. For this reason, its full description is provided later in this article, under the heading “GroupJoin and Join”.
var courseFaculty = courses.GroupJoin(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teachers) => new { Course = course.Name, Teachers = teachers });
Intersect
The Intersect
method gets the distinct items common to two sequences.
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Intersect: ", citrus.Intersect(fruits));
Join
The Join
method, which is similar to a SQL LEFT INNER JOIN
, is unusually difficult to demonstrate. For this reason, its full description is provided later in this article, under the heading “GroupJoin and Join”.
var courseTeacher = courses.Join(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teacher) => new { Course = course.Name, Teacher = teacher.Name });
OrderBy
The OrderBy
method sorts the sequence, in ascending order, by a key value obtained from each item in the sequence.
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("OrderBy: ", stooges.OrderBy(name => GetFirstWord(name)));
The code for the GetFirstWord
method is as follows:
private static string GetFirstWord(string words) =>
words.Substring(0, words.IndexOf(' '));
OrderByDescending
The OrderByDescending
method sorts the sequence, in descending order, by a key value obtained from each item in the sequence.
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("OrderByDescending: ", stooges.OrderByDescending(name => GetFirstWord(name)));
The code for the GetFirstWord
method is as follows:
private static string GetFirstWord(string words) =>
words.Substring(0, words.IndexOf(' '));
Reverse
The Reverse
method reverses the order of the sequence.
string[] stooges = { "Moe Howard", "Larry Fine", "Curly Howard", "Moe Howard" };
WriteSequence("Reverse: ", stooges.Reverse());
ThenBy
The ThenBy
method is somewhat unique in that it can only be applied to an IOrderedEnumerable<T>
, generally returned from one of the following methods: OrderBy
, OrderByDescending
, ThenBy
, or ThenByDescending
. This method provides the capability to sort a sequence, in ascending order, by an additional key value, also obtained from each item in the sequence.
var sortedStooges = stooges
.OrderBy(name => GetLastWord(name))
.ThenBy(name => GetFirstWord(name));
WriteSequence("ThenBy: ", sortedStooges);
ThenByDescending
The ThenByDescending
method is somewhat unique in that it can only be applied to an IOrderedEnumerable<T>, generally returned from one of the following methods: OrderBy
, OrderByDescending
, ThenBy
, or ThenByDescending
. This method provides the capability to sort a sequence, in descending order, by an additional key value, also obtained from each item in the sequence.
var sortedStooges = stooges
.OrderBy(name => GetLastWord(name))
.ThenByDescending(name => GetFirstWord(name));
WriteSequence("ThenByDescending: ", sortedStooges);
Union
The Union
method gets the distinct items resulting from the combination of two sequences.
string[] citrus = { "grapefruit", "lemon", "lime", "orange" };
string[] fruits = { "banana", "banana", "lemon", "lime", "lime" };
WriteSequence("Union: ", citrus.Union(fruits));
Singleton Methods
The singleton methods force immediate materialization of at least a portion of the sequence. These methods include: Aggregate
, All
, Any
, Average
, Contains
, Count
, ElementAt
, ElementAtOrDefault
, First
, FirstOrDefault
, Last
, LastOrDefault
, LongCount
, Max
, Min
, SequenceEqual
, Single
, SingleOrDefault
, Sum
, ToArray
, ToDictionary
, ToList
, and ToLookup
.
Aggregate
The Aggregate
method accumulates the items in a sequence to return a singleton, aggregate value. The code below is just an example. It is a terrible implementation of String.Join
, which is far more appropriate if you ever need to simply aggregate strings.
string[] berries = { "blackberry", "blueberry", "raspberry" };
string aggregate = berries.Aggregate((accumulator, item) => accumulator + " " + item);
Console.WriteLine($"Aggregate: {aggregate}");
Warning: If the sequence has zero items, an exception will occur. If the sequence has only one item, the first item is simply returned, without ever invoking the method you provide. For all other sequences, the first invocation of your method will be called with the first two items of the sequence.
All
The All
method gets a Boolean value indicating if all items in a sequence match the specified condition. This method stops when it reaches the first item that does not match the specified condition.
char[] letters = { 'A', 'B', 'C', 'D'};
bool all = letters.All(item => Char.IsLetter(item));
Console.WriteLine($"All: {all}");
Any
The Any
method gets a Boolean value indicating if any items in a sequence match the specified condition. This method stops when it reaches the first item that matches the specified condition.
char[] letters = { 'A', 'B', 'C', 'D'};
bool any = letters.Any(item => Char.IsLetter(item));
Console.WriteLine($"Any: {any}");
Average
The Average
method averages all of the items in the sequence and returns a floating point result.
byte[] bytes = { 1, 2, 3, 4 };
float average = bytes.Average(item => (float)item);
Console.WriteLine($"Average: {average}");
Contains
The Contains
method gets a Boolean value indicating if the sequence contains the specified item. This method stops on the first item that matches the specified item.
char[] letters = { 'A', 'B', 'C', 'D'};
bool contains = letters.Contains('C');
Console.WriteLine($"Contains {contains}");
Count
The Count
method gets the number of items in the sequence.
byte[] bytes = { 1, 2, 3, 4 };
int count = bytes.Count();
Console.WriteLine($"Count: {count}");
The Count
method can also get the number of items in a sequence that match a specified condition.
count = bytes.Count(item => (item & 1) == 0);
Console.WriteLine($"Count: {count}");
Warning: IEnumerable<T>.Count
method must evaluate every item in the sequence and can be quite resource intensive. It should not be confused with the ICollection<T>.Count
property or the Array.Length
property. Both of these (most often) simply return a pre-existing count and are (generally) not resource intensive.
ElementAt
The ElementAt
method gets the item at the specified zero-based index within the sequence.
byte[] bytes = { 1, 2, 3, 4 };
byte elementAt = bytes.ElementAt(1);
Console.WriteLine($"ElementAt: {elementAt}");
elementAt = bytes.ElementAt(10);
Warning: This should not be confused with indexing functionality provided by arrays and lists. Unlike true indexing, the ElementAt
method must evaluate and skip all items prior to the specified index. It is functionally equivalent to the following LINQ: IEnumerable<T>.Skip(index).First()
.
ElementAtOrDefault
The ElementAtOrDefault
method gets the item at the specified zero-based index within the sequence, if the specified item does not-exist, it returns a default value.
byte elementAtOrDefault = bytes.ElementAt(1);
Console.WriteLine($"ElementAtOrDefault: {elementAtOrDefault}");
elementAtOrDefault = bytes.ElementAtOrDefault(-1);
Console.WriteLine($"ElementAtOrDefault: {elementAtOrDefault}");
Warning: This should not be confused with indexing functionality provided by arrays and lists. Unlike true indexing, the ElementAtOrDefault
method must evaluate and skip all items prior to the specified index. It is functionally equivalent to the following LINQ: IEnumerable<T>.Skip(index).FirstOrDefault()
.
First
The First
method gets the first item in the sequence or the first item matching a specified condition. If it is acceptable for the sequence to be empty, the FirstOrDefault
method is preferable. If it is not acceptable for more than one item to be in the sequence, the Single/SingleOrDefault
methods are preference.
string[] berries = { "blackberry", "blueberry", "raspberry" };
string first = berries.First();
Console.WriteLine("First: {first}");
first = berries.First(item => item.StartsWith("r"));
Console.WriteLine($"First: {first}");
byte[] emptyBytes = { };
byte firstEmpty = emptyBytes.First();
FirstOrDefault
The FirstOrDefault
method gets the first item in the sequence or the first item matching a specified condition. It will return a default value, if the sequence is empty. If it is not acceptable for more than one item to be in the sequence, the SingleOrDefault
method is preference.
string[] berries = { "blackberry", "blueberry", "raspberry" };
string firstOrDefault = berries.FirstOrDefault();
Console.WriteLine($"FirstOrDefault: {firstOrDefault}");
firstOrDefault = berries.FirstOrDefault(item => item.StartsWith("r"));
Console.WriteLine($"FirstOrDefault: {firstOrDefault}");
byte[] emptyBytes = { };
byte firstEmpty = emptyBytes.FirstOrDefault();
Console.WriteLine($"FirstOrDefault: {firstEmpty}");
Last
The Last
method gets the last item in the sequence. If it is acceptable for the sequence to be empty, the LastOrDefault
method is preferable.
string last = berries.Last();
Console.WriteLine($"Last: {last}");
last = berries.Last(item => item.StartsWith("b"));
Console.WriteLine($"Last: {last}");
byte[] emptyBytes = { };
byte lastEmpty = emptyBytes.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastEmpty}");
Warning: Unlike the First
method, the Last
method must visit every item in the sequence. This can be very resource intensive for larger sequences.
LastOrDefault
The LastOrDefault
method gets the last item in the sequence or the last item matching a specified condition. It will return a default value, if the sequence is empty.
string lastOrDefault = berries.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastOrDefault}");
lastOrDefault = berries.LastOrDefault(item => item.StartsWith("b"));
Console.WriteLine($"LastOrDefault: {lastOrDefault}");
byte[] emptyBytes = { };
byte lastEmpty = emptyBytes.LastOrDefault();
Console.WriteLine($"LastOrDefault: {lastEmpty}");
Warning: Unlike the FirstOrDefault
method, the LastOrDefault
must visit every item in the sequence. This can be very resource intensive for larger sequences.
LongCount
The LongCount
method gets the number of items in the sequence (as a 64 bit count).
byte[] bytes = { 1, 2, 3, 4 };
long count = bytes.LongCount();
Console.WriteLine($"LongCount: {count}");
The LongCount
method can also get the number of items in a sequence that match a specified condition (as a 64 bit count).
count = bytes.LongCount(item => (item & 1) == 0);
Console.WriteLine($"LongCount: {count}");
Warning: IEnumerable<T>.LongCount
method must evaluate every item in the sequence and can be quite resource intensive. It should not be confused with the ICollection<T>.Count
property or the Array.Length
property. Both of these (most often) simply return a pre-existing count and are (generally) not resource intensive.
Max
The Max
method gets the greatest value in a sequence.
byte[] bytes = { 1, 2, 3, 4 };
byte max = bytes.Max();
Console.WriteLine($"Max: {max}");
string[] textNumbers = { "1", "2", "3", "4" };
max = textNumbers.Max(item => byte.Parse(item));
Console.WriteLine($"Max: {max}");
Min
The Min
method gets the smallest value in a sequence.
byte[] bytes = { 1, 2, 3, 4 };
byte min = bytes.Min();
Console.WriteLine($"Min: {min}");
string[] textNumbers = { "1", "2", "3", "4" };
min = textNumbers.Min(item => byte.Parse(item));
Console.WriteLine($"Min: {min}");
SequenceEqual
The SequenceEquals
method gets a Boolean value indicating if two sequences have an equal number of items that are equal and appear in the same order.
byte[] bytes = { 1, 2, 3, 4 };
bool sequenceEqual = bytes.SequenceEqual(bytes.Take(bytes.Length));
Console.WriteLine($"SequenceEqual: {sequenceEqual}");
sequenceEqual = bytes.SequenceEqual(bytes.Take(bytes.Length - 1));
Console.WriteLine($"SequenceEqual: {sequenceEqual}");
Single
The Single
method gets the first item in the sequence or the first item matching a specified condition. This method throws an exception if there is not exactly one matching item. If it is acceptable for the sequence to be empty, the SingleOrDefault
method is preferable.
string[] berry = { "blackberry" };
string single = berry.Single();
Console.WriteLine($"Single: {single}");
byte singleEmpty = emptyBytes.Single();
string[] berries = { "blackberry", "blueberry", "raspberry" };
single = berries.Single();
SingleOrDefault
The SingleOrDefault
method gets the first item in the sequence and throws an exception if more than one item exists and returns a default value if the sequence is empty.
string[] berry = { "blackberry" };
string singleOrDefault = berry.SingleOrDefault();
Console.WriteLine($"SingleOrDefault: {singleOrDefault}");
byte singleEmpty = emptyBytes.SingleOrDefault();
Console.WriteLine($"SingleOrDefault: {singleEmpty}");
string[] berries = { "blackberry", "blueberry", "raspberry" };
string singleOrDefault = berries.SingleOrDefault();
Sum
The Sum
method gets a sum of all of the values in the sequence.
byte[] bytes = { 1, 2, 3, 4 };
float sum = bytes.Sum(item => (float)item);
Console.WriteLine($"Sum: {sum}");
ToArray
The ToArray
method converts the sequence to an array of TItem[]
.
byte[] bytes = { 1, 2, 3, 4 };
byte[] byteArray = bytes
.Take(2)
.ToArray();
Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.
ToDictionary
The ToDictionary
method converts the sequence to a System.Collections.Generic.Dictionary<TKey, TITem>
.
string[] numberFruits = { "1 Apple", "2 Orange", "3 Cherry" };
Dictionary<int, string> fruitDictionary = numberFruits.ToDictionary(
item => int.Parse(GetFirstWord(item)), item => GetLastWord(item));
Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.
ToList
The ToList
method converts the sequence to a System.Collections.Generic.List<TItem>
.
byte[] bytes = { 1, 2, 3, 4 };
List<byte> byteList = bytes.ToList();
Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.
ToLookup
The ToLookup
method converts the sequence to a System.Linq.Lookup<TKey, TItem>
instance.
ILookup<int, string> fruitLookup = numberFruits.ToLookup(
item => int.Parse(GetFirstWord(item)));
Warning: This method should be avoided unless strictly necessary. Executing this method causes every item in the sequence to be evaluated. It also requires that all members of the sequence are present in memory at the same instant. For larger sequences, this can be very resource intensive.
GroupJoin and Join
Both the GroupJoin and Join methods are unusually complex. Here, we will provide a brief description of each. In both cases, we will use the following data:
var courses = new[]
{
new { Id = 1, Name = "Geometry" },
new { Id = 2, Name = "Physics" },
new { Id = 3, Name = "Parapsychology" },
};
var faculty = new[]
{
new { CourseId = 1, Name = "Pythagoras" },
new { CourseId = 1, Name = "Euclid" },
new { CourseId = 2, Name = "Albert Einstein" },
new { CourseId = 2, Name = "Isaac Newton"}
};
GroupJoin
The GroupJoin method is a bit complex to describe. It joins all of the items in one sequence with matching items from a second sequence. It also groups those matching items. For those familiar with SQL, it is similar to (but frustratingly different from) a LEFT OUTER JOIN
. The simplest overload accepts the following parameters:
- The sequence to join.
- A method to get the join key from items in the original sequence.
- A method to get the join key from items in the sequence to join.
- A method to join an item from the original sequence with a matching item in the sequence to join (or null when no match exists).
var courseFaculty = courses.GroupJoin(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teachers) => new { Course = course.Name, Teachers = teachers });
Console.Write("GroupBy: ");
int index = 0;
foreach (var courseTeachers in courseFaculty)
{
if (index++ >= 1)
Console.Write(", ");
WriteSequence($"[Course={courseTeachers.Course}, Teachers=", courseTeachers.Teachers, false);
Console.Write("]");
}
Console.WriteLine();
Because the results of a GroupJoin are a bit unwieldly, one common technique of dealing with this issue is to “flatten” the results to make them more closely resemble a LEFT OUTER JOIN
.
var flattened = courseFaculty.SelectMany(
courseTeachers => courseTeachers.Teachers.DefaultIfEmpty(),
(courseTeachers, teacher) => new { courseTeachers.Course, Teacher = teacher?.Name });
WriteSequence("GroupBy/SelectMany: ", flattened);
Join
The Join method joins matching items from two different sequences. For those familiar with SQL, it is nearly identical to LEFT INNER JOIN
. The simplest overload accepts the following parameters:
- The sequence to join.
- A method to get the join key from items in the original sequence.
- A method to get the join key from items in the sequence to join.
- A method to join an item from the original sequence with a matching item in the sequence to join.
var courseTeacher = courses.Join(faculty, course => course.Id, teacher => teacher.CourseId,
(course, teacher) => new { Course = course.Name, Teacher = teacher.Name });
WriteSequence("Join: ", courseTeacher);
Anonymous Types
Anonymous types were introduced in .NET 3.0 to accompany LINQ. They provide a syntactical short cut so that a type can be both be declared and initialized in a single step. With anonymous types, the following is possible:
var myAnonymousType = new { Id = 1, Name = "Me" };
Console.WriteLine(myAnonymousType.Name);
Prior to .NET 3.0, to achieve this same goal we would first need to define the type:
private class NotSoAnonymousName
{
private string name;
private int id;
public int Id { get { return id; } set { id = value; } }
public string Name { get { return name; } set { name = value; } }
public override string ToString()
{
return string.Format("{{ Id = {0}, Name = {1} }}", Id, Name);
}
}
And then reference the type by name:
NotSoAnonymousName notSoAnonymousType = new NotSoAnonymousName { Id = 1, Name = "Me" };
Console.WriteLine(myNotSoAnonymousType);
The class declaration is especially verbose, because auto-implemented properties, expression bodied members, and string interpolation were also unavailable at that time. For reference, had they been available, the class declaration could have at least been shortened to:
private class NotSoAnonymousName
{
public int Id { get; set; }
public string Name { get; set; } public override string ToString() =>
$"{{ Id = {Id}, Name = {Name} }}";
}
Lambda Expressions
Lambda expressions were introduced in .NET 3.0 to accompany LINQ. They provide a syntactical short cut to creating and sharing anonymous method declarations. With Lambda expressions, the following is possible:
string[] berries = { "blackberry", "blueberry", "raspberry" };
string firstBerry = berries.First(item => item.StartsWith("r"));
Console.WriteLine($"First: {firstBerry}");
The Lambda expression consists of parameters (left side) and method logic (right side), separated by “=>”:
item => item.StartsWith("r")
Prior to Lambda expressions, to achieve the same goal we would be forced to declare a method:
private static bool StartswithAnR(string item)
{
return item.StartsWith("r");
}
And pass its delegate as a parameter:
firstBerry = berries.First(StartswithAnR);
Console.WriteLine($"First: {firstBerry}");
With the previous Lambda method, we are essentially declaring an anonymous method that accepts one parameter (item) and returns a value. The type of the value (string) is inferred by the way that item is used. The type of the return value (bool) is similarly inferred.
C# also provides a whole bunch of pre-rolled delegates. This allows us to have type-safe method delegates for parameter and variable declarations. One of these is the Func delegate, its final type parameter (bool in this case) is the return value type, all previous type parameters (string in this case) indicate the type of parameters expected by the function/method.
Using the same Lamba expression from before we can assign it to a delegate and then use that delegate:
Func<string, bool> predicate = item => item.StartsWith("r");
bool startsWithAnR = predicate("right");
Console.WriteLine($"startsWithAnR = {startsWithAnR}");
Func<string, string, string> concatenate = (textA, textB) => textA + textB;
string result = concatenate("left ", "right");
Console.WriteLine($"result = {result}");
WriteSequence Methods
The following methods are used throughout this article.
private static void WriteSequence<TItem>(string prefix, IEnumerable<TItem> sequence,
bool writeLine = true)
{
Console.Write(prefix);
int index = 0;
foreach (TItem item in sequence)
{
if (index++ >= 1)
Console.Write(", ");
Console.Write(item);
}
if (writeLine)
Console.WriteLine();
}
private static void WriteSequence<TKey, TItem>(string prefix,
IEnumerable<IGrouping<TKey, TItem>> sequence)
{
Console.Write(prefix);
int index = 0;
foreach (IGrouping<TKey, TItem> grouping in sequence)
{
if (index++ >= 1)
Console.Write(", ");
WriteSequence($"[Key={grouping.Key}, Items=", grouping, false);
Console.Write("]");
}
Console.WriteLine();
}
Further Reading
For further reading, see the following:
Standard Query Operators Overview (C#)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/linq/standard-query-operators-overview
Anonymous Types (C# Programming Guide)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/anonymous-types
Lambda Expressions (C# Programming Guide)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/statements-expressions-operators/lambda-expressions
Extension Methods (C# Programming Guide)
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/extension-methods
String Interpolation (C# Reference)
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated
History
- 4/18/2018 - The original version was uploaded
- 4/18/2018 - Fixed incorrect comment on Join
- 4/21/2018 - Added links to other articles in this series
- 4/25/2018 - Added link to fourth article in series