Must-Know Concepts Related to LINQ and IEnumerable
Our previous article (.NET Nakama, 2022 March) taught us about the LINQ architecture and technologies, query syntaxes, etc. As we saw, we can define a generic class by using the
<T> sign after the class name, e.g.
T is the generic type parameter in which we can use any word (e.g., TKey, TCustomer, etc.). Commonly, the name of the generic type parameter starts with a
T to clearly show that it’s a type. We can use a generic class or method for different types without the cost of boxing operations and the risk of runtime casts.
In addition, we saw that the
IEnumerable<T> interface enables the generic collection classes to be enumerated using the
foreach statement. Therefore, the LINQ sources implement the
IEnumerable interface to be enumerated. Moreover, we saw how to use extension methods and lambda expressions in the LINQ method syntax.
In the previous paragraphs, several concepts need further explanation. Therefore, this article will focus on filling the puzzle about Boxing and Unboxing, Extension Methods, Anonymous Functions, Lambda Expressions, Closures, IEnumerator, and IQueryable.
In C#, we can treat any value type (e.g.,
char, etc.) as an object (i.e., reference type). Boxing and Unboxing enable the C# unified view of the type system.
- Boxing is converting a value type to an
objecttype or any interface type implemented by this value type.
- Unboxing is converting the
objectback to a value type.
Extension methods allow us to add additional methods to existing types (value types, reference types, interfaces) without creating a new derived type, recompiling, or modifying the original type.
So, let’s assume that we frequently need to count the words of a text (
string class). Then, we could create the following extension method (e.g., see
CountWords in Figure 1) that operates on the
string class. As we can see in the following example (Figure 1):
- An extension method is a static method of a static class.
- The first parameter of the method uses the
thismodifier to specify the type that we would extend with the current method.
- To call the extension method, we should add a
usingdirective to explicitly import the namespace that contains the extension method.
- When calling the extension method, we do not need to specify the first parameter. However, we need to specify other possible parameters (e.g.,
- We call the extension method as if they were instance methods on the type.
- Note: The extension class and method should be visible to the calling code (see access modifiers, e.g., public, private, internal, etc.).
Extension methods are commonly used when we:
- Don’t control the types to be extended,
- Don’t want to force its implementation via inheritance,
- Want to have clean model classes (e.g., DTOs), etc.
We should be careful when creating extension methods to provide simple logic implementation without the need for dependencies. We should use extension methods to improve readability and maintainability by reducing repeated code (see DRY principle).
anonymous methods are functions without a name that can be defined using the
delegate keyword. The anonymous methods are usually assigned to a variable of the delegate type. Anonymous methods are helpful when we want to create inline methods (with or without input arguments).
In the following example, we defined the
ShowMesssage anonymous function to get a
string argument and do not return a value. As we can see, at runtime, we can assign the functionality of this function. In this example, we are just writing the input message to the console.
We can use Lambda Expressions to easily create Anonymous Functions to be passed as arguments or returned as the value of other functions. To create a lambda expression, we use the lambda declaration operator
=> to separate the lambda’s parameter list from its body.
A lambda expression that returns a value is expressed as a Func delegate type. Otherwise, it is expressed as an Action delegate type. In the following example, we can see how the use of lambda expressions simplifies creating the same anonymous function that we created in the previous example.
Lambda expressions are compelling, and as we saw in .NET Nakama (2022, March), they are also beneficial for writing LINQ query expressions.
In lambda expressions (and anonymous functions), we may need to access a non-local variable for flexibility. However, in general, we can only use a local variable in the code block that it’s declared. So, a non-local variable is declared in a different code block.
The access to a non-local variable from a method or function is called
Closure, which can improve the code’s maintainability when used sparingly. The following example shows that the
showMesssage method has access to the
aVariable value. Notice that the
"Initial Value" of the
aVariable string is not shown.
The IEnumerable interface enables the non-generic collection classes (such as
ArrayList) to be enumerated using the
foreach statement. In the case of generic collection classes (such as
Dictionary<TKey, TValue>, etc.), we can use the
IEnumerable<T> interface for the same purpose.
IEnumerable interface contains only the
GetEnumerator() method, which returns an
IEnumerator that iterates through a collection.
The IEnumerator provides the ability to iterate through the collection by exposing a
Current property and
Reset() methods. We can use Enumerators to read the data in the collection and not modify them.
We can use the
IQueryable<T> interfaces to query data from query providers (such as databases). For that reason, the
IQueryable interfaces represent a query as an expression tree to be executed for different types of data sources.
The IQueryable interfaces inherit from the related
IEnumerable interfaces so that we can enumerate the query result. We can use both of these interfaces to query data. However, as we can see in the following table, they operate quite differently.
|Suitable for querying data from:||In-memory collections (such as ArrayList, List
||Out-of-memory sources (such as databases)|
|When executing a query on the database server (e.g., executing a “SELECT” for SQL).||The query is executed without filtering (e.g., without the “WHERE” clause in SQL). First, the whole data are retrieved in memory (e.g., in the application server), and then the filtering is performed.||The query is executed with all the filters on the database server, and the final data are retrieved.|
Understanding LINQ and IEnumerable can be quite challenging because they involve several concepts. We may don’t know these concepts or we may know the concepts but not their names. This is quite common when we are practical learners or learning on the job (i.e., no time to read the theory).
In this article, we covered several must-know concepts related to LINQ and IEnumerable. It’s essential to understand these concepts to quickly and effectively operate in our daily jobs as software engineers.
In our next article, we will learn about the .NET Collections (Generic, Concurrent, Immutable, ReadOnly, etc.), so stay tuned!