We strengthen type control: where in a typical C # -project there is an unsolicited element of weak typing?


    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.


    In functional languages, for example, in F #, there is a standard type FSharpOption, which just represents a container for any type, in which there can either be one value of T or be absent. Let's consider what possibilities we would like to have from this type so that it would be convenient to use, including adherents of different coding styles with varying degrees of familiarity with functional languages.
    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.Nothing. In this case, no one can forgetfully forget to check the result for null. We further think about the use of such a repository:

    // забывчивый разработчик забыл проверить на 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;
    	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

    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);

    Maybe integration is also required. with his fairly close relative - IEnumerable, 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’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 TypeHasvalueValueFluentapiLINQ SupportIntegration with IEnumerable Notes and source code
    Option, classthere isno, only pattern-matchingthe minimumnotnotgithub.com/tejacques/Option
    Strilanc.Value.May, structthere isno, only pattern-matchingrichthere isthere isAccepts null as a valid value in May
    Options, structthere isthere isaveragethere isthere isEither type github.com/davidsidlinger/options is also offered.
    Nevernull classthere isthere isaveragenotnotgithub.com/Bomret/NeverNull
    Functional.Maybe, structthere isthere isrichthere isthere isgithub.com/AndreyTsvetkov/Functional.Maybe
    Maybe no type--the minimumnot-extension methods work with the usual null
    upd: here is a screencast from mezastel with a similar approach and a detailed explanation: www.techdays.ru/videos/4448.html
    WeeGems.Options, structthere isthere isthe minimumnotnotThere are also other functional usefulnesses: memoization, partial use of the functions

    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 , as well as functional types in C #, tuples, etc. :). Wait and see.

    UPD: comrade aikixd wrote an opposing article .

    Also popular now: