Value Type Nullabili

V

Di regola, i reference type sono nullabili, ma lo stesso non si può dire invece per i value type.
Questa regola si applica di certo al C#; difatti, mentre è lecito avere una stringa nulla, non si può avere un intero nullo.

Quando un value type è comodo sia nullabile

In linea di massima ci va bene che i value type non siano nullabili, ma vogliamo davvero che questa regola sia un assoluto?
Personalmente mi sono trovato più volte in una situazione in cui dovevo esplicitamente rappresentare l’assenza di informazione.
Non siate timidi: alzi la mano chi di voi almeno una volta nella sua vita ha scritto nella sua documentazione qualcosa del tipo: “ritorna l’intero xxx, o -1 in caso di valore assente”.
Ma -1 funge allo scopo di identificare assenza di valore, o è solo una paraculata workaround?

Non solo. Nel caso in qualche punto del codice ci dimenticassimo di gestire il valore fittizio, cosa accadrebbe? La nostra applicazione andrebbe in pezzi (leggi eccezione) permettendoci di accorgerci subito del problema, o si tramuterebbe in un fastidioso bug difficile da scovare?

La struct Nullable

A partire dalla versione 2 di C#, ci è stato fornito un modo per gestire in modo nativo ed elegante questa condizione.

Senza troppi giri di parole, diamo uno sguardo a questa semplice implementazione:

public struct Nullable<T> where T : struct
{
  private T _value;
  
  public Nullable(T value)
  {
    _value = value;
    HasValue = true;
  }
  
  public bool HasValue { get; }

  public T Value
  {
    get 
    {
      if (!_hasValue)
        throw new InvalidOperationException();
      return Value;
    }
  }
}

Analizzando velocemente il codice vediamo una struct (e quindi non nullabile) generica, il cui tipo del parametro non può a sua volta essere nullabile. Questa struct, tramite un booleano di appoggio HasValue determina se Value è un valore valido o se invece deve lanciare eccezione.

Boxing

I value type e i reference type si comportano in modo differente quando si parla di boxing.
Prendiamo questo esempio:

var n = 42;
object o = n;

Il tipo di o, sebbene non sia distinguibile da quello di n in C#, in IL è chiaramente differente, ovvero boxed int. Come ci si può aspettare, boxed int è un riferimento all’intero n.
Quando invece si passa al Nullable le cose cambiano drasticamente, difatti si hanno due possibili scenari:

  • HasValue è false, e quindi o è null
  • HasValue è true, e quindi o è boxed T

Il suffisso ?

La struct Nullable è di comodità indubbia, ma c’è da ammetterlo: è verbosa.
Una delle cose che mi è sempre piaciuta del C# rispetto ad altri linguaggi, è la sua compattezza e potenza d’espressione.
Evidentemente i designer del C# la pensano come me e pertanto, nonostante la presenza della struct, hanno trovato un escamotage interessante: il punto interrogativo.

Scrivere Nullable<T> o T? è la stessa identica cosa, quindi perché non sfruttarla appieno?
Ma non è tutto. Le due forme di inizializzazioni seguenti sono equivalenti:

T? x = new T?();
T? x = null;

Personalmente preferisco la seconda, ma il risultato è lo stesso: HasValue == false.

Lifted operators

Supponiamo di aver definito la nostra bellissima struct AwesomeStruct e, da bravi masochisti, aver fatto l’overload di ogni operatore possibile ed immaginabile.
Ora sorge spontanea la domanda: come si comportano gli operatori di Nullable?

La risposta risiede nei lifted operators, ovvero un meccanismo tramite il quale si preserva il comportamento originale dell’operatore nel caso in cui entrambi i valori fossero non nulli, mentre alcune regole speciali subentrano nel caso di valori nulli.
In particolare:

  • Operatori unari (+, -, ++, --, e ~) ritornano null se applicati su un valore nullo
  • Operatori binari (+, -, *, /, %, << e >>) ritornano null se almeno uno dei due valori è nullo
  • Operatori logici (&, |, ^ e !) hanno un comportamento un po’ particolare
    • false & null (e viceversa) ritorna false
    • true | null (e vicevera) ritorna true
    • tutti gli altri casi ritornano null in caso di valori nulli
  • Operatori di ugualianza (== e !=) ritornano true se entrambi sono valori nulli, ma false se solo uno dei due lo è
  • Operatori di confronto (<, <=, >, >=) ritornano false se almeno uno dei due valori è nullo

In particolare l’ultima regola sembra andare in contraddizione con la penultima. Consideriamo questo esempio:

int? x = null;
bool t = x == x;
bool f = x <= x;

Come la fantasia nei nomi può suggerire, t è true, mentre f è false.
Ambiguo, vero? Ma siamo onesti: se proviamo ad usare un operatore di confronto tra due valori nulli, probabilmente il problema è altrove.

Considerazioni finali

I valori nullabili sono estremamente utili. Personalmente ne ho fatto abuso nel caso di bool?, dove potevo esprimere true, false e boh.
Sembra brutto, ma in effetti quel boh è spesso utile come “non mi importa” o “sconosciuto” o altri casi ancora dove una logica ternaria torna obiettivamente utile.
Una volta che avrete imparato a fare attenzione agli operatori (o avrete imparato ad evitarli, come me), l’utilizzo dei Nullable bisogna ammettere che è davvero triviale.

Bibliografia

Skeet, J. (2019). C# in depth (4th ed.). Shelter Island, NY: Manning.

A proposito di me

Nico Caprioli

Si diletta con i computer sin dall'età di 8 anni, per sbarcare nel mondo della programmazione durante il liceo.
Dopo una laurea magistrale in Ingegneria Informatica, passa le sue giornate a battere i tasti sperando di scrivere codice C# valido.

Di Nico Caprioli

Gli articoli più letti

Articoli recenti

Commenti recenti