
We strengthen type control: where in a typical C # -project there is an unsolicited element of weak typing?
Problem
We are used to talking about languages like C # as strongly and statically typed. This, of course, is true, and in many cases the type that we indicate for some linguistic entity expresses well our idea of its type. But there are widespread examples when, out of habit (“and everyone does this”) we put up with the not quite correct expression of the “desired type” in the “declared type”. The most striking are reference types equipped with the null value in no alternative.
In my current project for the year of active development there were no NullReferenceException. I can reasonably believe that this is a consequence of the application of the techniques described below.
Consider the code snippet:
public interface IUserRepo
{
User Get(int id);
User Find(int id);
}
This interface requires additional comment: “Get always returns not null, but throws an Exception if the object is not found; and Find, not finding, returns null ". The “desired” return types implied by the author of these methods are different: “Required User” and “Maybe User”. And the "declared" type is one and the same. If language does not force us to express this difference explicitly, this does not mean that we cannot and should not do it on our own initiative.
Decision
In functional languages, for example, in F #, there is a standard type FSharpOption
Given this hypothetical type, we can rewrite our repository in the following form:
public interface IUserRepo
{
User Get(int id);
Maybe Find(int id);
}
Immediately make a reservation that the first method can still return null. There is no easy way to ban this at the language level. However, this can be done at least at the agreement level in the development team. The success of such an undertaking depends on the people; in my project, such an agreement has been adopted and is being successfully observed.
Of course, you can go further and build in the assembly process checks for the presence of the null keyword in the source code (with the specified exceptions to this rule). But there was no need for this yet, just internal discipline is enough.
In general, one can go even further, for example, forcing Contract.Ensure (Contract.Result into all suitable methods)()! = null) through some AOP solution, for example, PostSharp, in which case even members of a team with low discipline will not be able to return the ill-fated null.
The new version of the interface explicitly declares that Find may not find the object, in which case it will return Maybe
// забывчивый разработчик забыл проверить на null
var user = repo.Find(userId); // возвращает теперь не User, а Maybe
var userName = user.Name; // не компилируется, у Maybe нет Name
var maybeUser = repo.Find(userId); // зато код ниже компилируется,
string userName;
if (maybeUser.HasValue) // таким образом нас заставили НЕ забыть проверить на наличие объекта
{
var user = maybeUser.Value;
userName = user.Name;
}
else
userName = "unknown";
This code is similar to what we would write with a null check, just the condition in if looks a bit different. However, the constant repetition of such checks, firstly, clutters up the code, making the essence of its operations less obvious, and secondly, it bores the developer. Therefore, it would be extremely convenient to have ready-made methods for most standard operations. Here is the previous fluent-style code:
string userName = repo.Find(userId).Select(u => u.Name).OrElse("unknown");
For those who are close to functional languages and do-notation, a completely “functional” style can be supported:
string userName = (from user in repo.Find(userId) select user.Name).OrElse("unknown");
Or, an example is more complicated:
(
from roleAProfile in provider.FindProfile(userId, type: "A")
from roleBProfile in provider.FindProfile(userId, type: "B")
from roleCProfile in provider.FindProfile(userId, type: "C")
where roleAProfile.IsActive() && roleCProfile.IsPremium()
let user = repo.GetUser(userId)
select user
).Do(HonorAsActiveUser);
with its imperative equivalent:
var maybeProfileA = provider.FindProfile(userId, type: "A");
if (maybeProfileA.HasValue)
{
var profileA = maybeProfileA.Value;
var maybeProfileB = provider.FindProfile(userId, type: "B");
if (maybeProfileB.HasValue)
{
var profileB = maybeProfileB.Value;
var maybeProfileC = provider.FindProfile(userId, type: "C");
if (maybeProfileC.HasValue)
{
var profileC = maybeProfileC.Value;
if (profileA.IsActive() && profileC.IsPremium())
{
var user = repo.GetUser(userId);
HonorAsActiveUser(user);
}
}
}
}
Maybe integration is also required.
var admin = users.MaybeFirst(u => u.IsAdmin); // вместо FirstOrDefault(u => u.IsAdmin);
Console.WriteLine("Admin is {0}", admin.Select(a => a.Name).OrElse("not found"));
From the above “dreams” it’s clear what you want in the Maybe type
- access to value presence information
- and to the value itself, if available
- a set of convenient methods (or extension methods) for a streaming call style
- LINQ expression syntax support
- integration with IEnumerable
and other components, when working with which often there are situations of lack of value
Let's consider what solutions Nuget can offer us for quick inclusion in the project and compare them according to the above criteria:
Nuget Package Name and Type Type | Hasvalue | Value | Fluentapi | LINQ Support | Integration with IEnumerable | Notes and source code |
---|---|---|---|---|---|---|
Option, class | there is | no, only pattern-matching | the minimum | not | not | github.com/tejacques/Option |
Strilanc.Value.May, struct | there is | no, only pattern-matching | rich | there is | there is | Accepts null as a valid value in May github.com/Strilanc/May |
Options, struct | there is | there is | average | there is | there is | Either type github.com/davidsidlinger/options is also offered. |
Nevernull class | there is | there is | average | not | not | github.com/Bomret/NeverNull |
Functional.Maybe, struct | there is | there is | rich | there is | there is | github.com/AndreyTsvetkov/Functional.Maybe |
Maybe no type | - | - | the minimum | not | - | extension methods work with the usual null github.com/hazzik/Maybe upd: here is a screencast from mezastel with a similar approach and a detailed explanation: www.techdays.ru/videos/4448.html |
WeeGems.Options, struct | there is | there is | the minimum | not | not | There are also other functional usefulnesses: memoization, partial use of the functions bitbucket.org/MattDavey/weegems |
It so happened that my project has grown its package, it is among the above.
It can be seen from this table that the most “lightweight”, minimally invasive solution is Maybe from hazzik , which does not require changing the API in any way, but simply adds a couple of extension methods to get rid of the same ifs. But, alas, it does not protect the forgetful programmer from receiving a NullReferenceException.
The richest packages are Strilanc.Value.Maybe ( here the author explains, in particular, why he decided that (null). ToMaybe () is not the same as Maybe.Nothing), Functional.Maybe, Options.
Choose to taste. In general, I want, of course, a standard solution from Microsoft
UPD: comrade aikixd wrote an opposing article .