Probably, almost every .NET-developer has come across cases where, for convenience of coding routine actions and reducing boilerplate code when working with standard data types, the capabilities of the standard library are not enough.
And practically in every project there appear assemblies and namespaces of the Common type, ProjectName.Common, etc., containing additions for working with standard data types: Enums enumerations, Nullable structures, strings and collections — IEnumerable <T> enums, arrays, lists and collections.
As a rule, these add-ons are implemented using the extension methods mechanism. One can often observe the existence of monad implementations, also built on the mechanism of extension methods.
(Looking ahead, we will consider issues that arise unexpectedly, and which can be overlooked when your own extensions for IEnumerable <T> are created, and work is being done with IQueryable <T>).
The writing of this article is inspired by the reading of a long-standing article translation of the Check for empty transfers and the ensuing discussion to it.
The article is old, but the topic is still relevant, especially since the code, similar to the example from the article, had to be met in real work from project to project.
In the original article, a question was raised, which in its essence concerns, in general, Common-libraries added to work projects.
The problem is that similar extensions in product projects are added hastily, because developers are engaged in the creation of new features, and the creation and design and debugging of basic infrastructure of time and resources is not allocated.
In addition, as a rule, the developer, adding the Common-Supplement he needs, creates it in such a way that this addition is tailored to the cases of its feature, and it does not occur that once this addition is of a general nature, then it should be as abstract as possible from the subject logic and have a universal character - as is done in standard libraries of platforms.
As a result, in numerous Common-subfolders of projects, there are deposits of the code given in the original article :
public void Foo<T>(IEnumerable<T> items) { if(items == null || items.Count() == 0) { // } }
The author pointed out a problem with the Count () method and proposed to create such an extension method:
public static bool IsNullOrEmpty<T>(this IEnumerable<T> items) { return items == null || !items.Any(); }
But the presence of such a method does not solve all the problems:
Now, note that all standard .NET collections, except the “infinite” IEnumerable <T> sequence itself — arrays, lists, and the collections themselves — implement the standard IReadOnlyCollection <T> interface, which provides the Count property — and no iterators with overheads are needed expenses.
Thus, it is advisable to create two extension methods:
public static bool IsNullOrEmpty<T>(this IReadOnlyCollection<T> items) { return items == null || items.Count == 0; } public static bool IsNullOrEmpty<T>(this IEnumerable<T> items) { return items == null || !items.Any(); }
In this case, when calling IsNullOrEmpty <T>, the appropriate method will be selected by the compiler, depending on the type of object for which the extension is being called. The challenge itself in both cases will look the same.
However, later in the discussion one of the commentators pointed out that, probably, for IQueryable <T> (the interface of the "infinite" sequence for working with queries to the database, inheriting from IEnumerable <T>) the most optimal would be just a call to the Count () method.
This version requires verification, including testing of work with different ORM - EF, EFCore, Linq2Sql, and, if so, then there is a need to create a third method.
In fact, for IQueryable <T> there are own extension-implementations Any (), Count () and other methods of working with collections (the System.Linq.Queryable class), which are designed to work with ORM, unlike similar implementations for IEnumerable <T> (System.Linq.Enumerable class).
In this case, the Queryable version of Any () probably works even more optimally than the Queryable check Count () == 0 .
To call the required Queryable versions of Any () or Count (), if we want to call our IsNullOrEmpty check, we need a new method with an IQueryable <T> input parameter.
Thus, you need to create a third method:
public static bool IsNullOrEmpty<T>(this IQueryable<T> items) { return items == null || items.Count() == 0; }
or
public static bool IsNullOrEmpty<T>(this IQueryable<T> items) { return items == null || !items.Any(); }
As a result, in order to implement the correct for all cases (for all?) Simple null-safe checking of collections for "emptiness", we had to do a little research and implement three extension methods.
And if at the initial stage you create only a part of the methods, for example, only the first two (these methods are not needed; you need to make food features), then you might get this:
For the same reason, all these methods should be implemented in one assembly and one namespace (possible in different classes, for example, EnumerableExtensions and QueryableExtensions), so that if you accidentally disable the namespace or assembly, we do not return to the situation when IQueryable <T> -collections work using ordinary Enumerable extensions.
In my opinion, the abundance of such extensions in almost every project indicates a lack of development of the standard library and the platform model as a whole.
Part of the problems would be automatically eliminated if there was support for Not Nullability in the platform, the other part was the presence in the standard library of a larger number of extensions taking into account a wider range of cases for working with standard data types.
Moreover, they are implemented in a modern way - in the form of extensions using generalizations (Generics).
Additionally we will talk about this in the next article.
PS What is interesting, if you look at Kotlin and its standard library, during the development of which the experience of other languages ​​was clearly studied, first of all, in my opinion - Java, C # and Ruby, then you can easily find out these things - Not Nullability and abundance of extensions, in the presence of which there is no need to add your own “bicycle” microblib implementation for working with standard types.
Source: https://habr.com/ru/post/349852/
All Articles