LINQ (Language INtegrated Queries)
Before diving into the details of LINQ, let's provide a brief introduction to functional types. In most object-oriented programming languages, functional types are supported. You can pass functions as parameters to other functions or return functions. In C#, you can use Func<TIn1, TIn2, ..., TIn14, TResult> (which can have 0 to 14 input types) to represent a function that takes parameters of types TIn1, TIn2, ..., TIn14 and returns a TResult, and Action<TIn1, TIn2, ..., TIn14>, which is the same thing except it doesn't return anything. Both types are delegates, essentially the C# equivalent of function pointers. Additionally, you can declare inline functions, or what are actually known as lambda functions, like so:
e => e.ToString(); // If the type of e can be inferred
(int e) => e.ToString();
(e, f) => e.ToString() + f.ToString(); // If the types of e and f can be inferred
(int e, long f) => {
return e.ToString() + f.ToString();
};
If functions are small and you want to use them locally, consider using inline lambda functions. In LINQ, the following methods are offered for all classes that implement IEnumerable<T>:
- Projection/Map using Select(Func<TValue, TResult>): If you have an IEnumerable<TValue> and a function that takes TValue and returns TResult, you can obtain an IEnumerable<TResult> using Select.
- Filtering using Where(Func<TValue, bool>): If you have an IEnumerable<TValue> and a predicate that takes TValue and returns a boolean value, you can obtain an IEnumerable<TValue> containing only the values that satisfy the predicate using Where.
- Sorting using OrderBy(Func<TValue, TKey>), ThenBy(Func<TValue, TKey>), and the variations for descending order, OrderByDescending and ThenByDescending (ThenBy and ThenByDescending belong to IOrderedEnumerable and are used after OrderBy and OrderByDescending). The provided function selects a field or a computation over TValue by which the enumeration should be ordered.
- Element Extraction using First, FirstOrDefault, Last, LastOrDefault: To extract the first or last element, with exception throwing if the sequence is empty, or returning the default value for methods with OrDefault. A predicate similar to Where can also be provided to take the first element that satisfies the predicate.
- Counting using Count(): A predicate can also be used to count the elements that satisfy the predicate.
- Skipping and Taking a number of elements from a collection using Skip(int) and Take(int), respectively.
- Checking a Property using All(Func<TValue, bool>) and Any(Func<TValue, bool>): To test whether all or at least one element satisfies a predicate.
There are more methods available, but these are the most common ones. You can find all the methods for IEnumerable in your IDE for more information.
All these methods can be used with method-chaining to transform data into a well-defined flow, as shown in the example.
var list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
Console.WriteLine("The processed list is: ");
foreach (var item in list.Where(e => e % 2 == 0)
.Select(e => e.ToString() + e.ToString())
.OrderBy(e => e.Length)
.ThenByDescending(e => e))
{
Console.Write("{0} ", item);
}
As it can be observed, LINQ operations are modeled like database operations, which is useful as it demonstrates how various concepts in computer science, such as collections as data structures and databases, can be unified through the lens of functional programming.
What has been presented is the syntax using methods. However, there is also a query syntax for LINQ that can be useful in certain cases, although the method syntax is generally recommended.
// We can write LINQ using the method syntax.
var enumerable = list.Where(e => e % 2 == 0).Select(e => e.ToString() + e.ToString());
// Alternatively, using the query syntax.
var enumerable = from e in Generate(1, 23) where e % 2 == 0 select e.ToString() + e.ToString();
For the compiler, the query syntax is translated into method syntax and doesn't represent any difference between the two. It's merely a syntax that, in certain cases, makes it easier to express a specific data transformation compared to the method-chaining syntax.