📜 ⬆️ ⬇️

Strengthening type control: where in the typical C # project there is an unsolicited element of weak typing?

Problem


We used to talk about languages ​​like C # as strictly and statically typed. This, of course, is true, and in many cases the type indicated by us for some linguistic entity well expresses our idea of its type. But there are widespread examples when, out of habit, we (“and everybody does that”) put up with the not quite correct expression of the “desired type” in the “declared type”. The brightest are reference types that are without alternative equipped with the value “null”.
In my current project for the year of active development there was not a single NullReferenceException. I can not without reason to believe that this is a consequence of the use of the techniques described below.

Consider the code snippet:

public interface IUserRepo { User Get(int id); User Find(int id); } 

This interface requires an additional comment: “Get always returns non-null, but throws an Exception if the object is not found; and Find, not finding, returns null. " “Desired”, implied by the author, the return types of these methods are different: “Required User” and “Maybe User”. And the “declared” type is the same. If a language does not force us to explicitly express this difference, then this does not mean that we cannot and should not do this on our own initiative.

Decision


In functional languages, for example, in F #, there is a standard type FSharpOption <T>, which is exactly the same for any type of container, in which there can either be one T value or be absent. Consider what opportunities we would like to have from this type, so that they are convenient to use, including adherents of different coding styles with varying degrees of familiarity with functional languages.
Given this hypothetical type, you can rewrite our repository in this form:

 public interface IUserRepo { User Get(int id); Maybe<User> Find(int id); } 

Immediately make a reservation that the first method can still return null. There is no simple way to prohibit it at the language level. However, you can do this 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 is accepted and successfully respected.
Of course, you can go ahead and embed in the build process of checking for the presence of the null keyword in the source code (with the specified exceptions to this rule). But in this there was no need, just enough internal discipline.
In general, you can go even further, for example, forcibly inject into all appropriate Contract.Ensure methods (Contract.Result <T> ()! = Null) through some AOP solution, for example PostSharp, in this case even team members 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 the value Maybe <User> .Nothing. In this case, no one can forgetfully check the result for null. Let's fantasize further about using such a repository:
')
 //      null var user = repo.Find(userId); //    User,  Maybe<User> 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 null checking, just the condition in the if looks a little different. However, the constant repetition of such checks, firstly, clutters up the code, making the essence of its operations less pronounced, and secondly, it tires 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, the 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); } } } } 

It also requires integrating Maybe <T> with its fairly close relative - IEnumerable <T>, at least in this form:

 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 is clear that I want to have a Maybe type


Let us consider what solutions Nuget can offer us for quick inclusion in the project and compare them according to the above criteria:

Package name Nuget and type typeHasvalueValueFluentapiLINQ supportIEnumerable integrationNotes and source code
Option, classthere isno, only pattern-matchingminimalnotnotgithub.com/tejacques/Option
Strilanc.Value.May, structthere isno, only pattern-matchingrichthere isthere isAccepts null as a valid value in May.
github.com/Strilanc/May
Options, structthere isthere isthe averagethere isthere isAlso available as Either.
github.com/davidsidlinger/options
Nevernull classthere isthere isthe averagenotnotgithub.com/Bomret/NeverNull
Functional.Maybe, structthere isthere isrichthere isthere isgithub.com/AndreyTsvetkov/Functional.Maybe
Maybe no type--minimalnot-extension methods work with the usual null
github.com/hazzik/Maybe
upd: and here is a screencast from mezastel with a similar approach and a detailed explanation: www.techdays.ru/videos/4448.html
WeeGems.Options, structthere isthere isminimalnotnotThere are also other functional utilities: memoization, partial application of functions.
bitbucket.org/MattDavey/weegems

It so happened that in my project my package has grown, it is among the above.

From this table it can be seen that the easiest, 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 if-s. But, alas, 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, the standard solution from Microsoft , as well as functional types in C #, tuples, etc. :) . Wait and see.

UPD: Comrade aikixd wrote an opposing article .

Source: https://habr.com/ru/post/200100/


All Articles