.NET Nakama

Improving your .NET skills

Must-Know Concepts Related to LINQ and IEnumerable

April 04, 2022 (~9 Minute Read)
C# BASICS LINQ IENUMERABLE BOXING EXTENSION METHODS LAMBDA EXPRESSIONS CLOSURES

Introduction

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. MyClassName<T>. The 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.

Boxing and Unboxing

In C#, we can treat any value type (e.g., int, 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 object type or any interface type implemented by this value type.
  • Unboxing is converting the object back to a value type.
int num = 123;     // An integer value type assigned with the value 23.
object obj = num;  // Boxing
int i = (int)obj;  // Unboxing

Extension Methods

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 this modifier to specify the type that we would extend with the current method.
  • To call the extension method, we should add a using directive 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., bool separateDashes).
  • 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.).
C# extension method example.
Figure 1. - C# extension method example.

When to use Extension Methods

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 Functions

In C#, 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.

internal class Program
{
	// Define the anonymous function
	public delegate void ShowMesssage(string message);

	static void Main(string[] args)
	{
		// Create the function with one parameter
		ShowMesssage showMesssage = delegate (string message)
		{
			Console.WriteLine($"My message is: {message}");
		};

		// Call the anonymous function
		showMesssage("This is an anonymous function.");

		// Output:
		// My message is: This is an anonymous function.
	}
}

Lambda Expressions

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.

Syntax:

// Expression lambda: One-line expression lambda
(input-parameters) => expression

// Statement lambda: Multiple lines expression lambda are enclosed in braces
(input-parameters) => { <sequence-of-statements> }

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.

internal class Program
{
	static void Main(string[] args)
	{
		// Create the function with one parameter
		Action<string> showMesssage = (string message) => Console.WriteLine($"My message is: {message}");

		// Call the lambda expression
		showMesssage("This is an anonymous function created with a lambda expression.");

		// Output:
		// My message is: This is an anonymous function created with a lambda expression.
	}
}

Closures

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.

internal class Program
{
	static void Main(string[] args)
	{
		string aVariable = "Initial Value";

		// Create the function with one parameter
		Action<string> showMesssage = (string message) =>
		{
			Console.WriteLine($"My message is: {message}");

			// The access to a non-local variable is called closure
			Console.WriteLine($"Non-local variable value: {aVariable}");
		};

		aVariable = "I have access to a non-local variable! :)";


		// Call the lambda expression
		showMesssage("This is an anonymous function created with a lambda expression.");

		// Output:
		// My message is: This is an anonymous function created with a lambda expression.
		// Non-local variable value: I have access to a non-local variable! :)
	}
}

IEnumerable and IEnumerator

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 List<T>, Dictionary<TKey, TValue>, etc.), we can use the IEnumerable<T> interface for the same purpose.

The 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 MoveNext() and Reset() methods. We can use Enumerators to read the data in the collection and not modify them.

IEnumerable vs. IQueryable

We can use the IQueryable and 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.

  IEnumerable IQueryable
Namespace System.Collections System.Linq Namespace
Suitable for querying data from: In-memory collections (such as ArrayList, List, etc.) 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.

Summary

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!

If you liked this article (or not), do not hesitate to leave comments, questions, suggestions, complaints, or just say Hi in the section below. Don't be a stranger 😉!

Dont't forget to follow my feed and be a .NET Nakama. Have a nice day 😁.