Se possibile, sarebbe una cattiva pratica usare Nullable per tipi non di valore?

3

After writing this question I found out that Nullable<T> has a type constraint of struct on T so you cannot actually use Nullable for reference types. However, some good answers interpreted the questions as 'if you could' or 'if you would write your own Nullable or NonNullable type'. This interpretation has lead to some nice answers so I've decided to not close or delete the question.

So che il costrutto nullable in C # è stato introdotto per supportare null per i tipi di valore. Tuttavia, la lingua non sembra avere problemi con l'utilizzo di nullable su altri tipi. Mi chiedo se questa è una cattiva pratica. Ho trovato alcuni argomenti per entrambe le parti, ma non riesco a decidere.

Pro

  • Foo? x rende eccezionalmente chiaro che una variabile può essere nullo.

Contro

  • Se Foo non è un tipo di valore, creiamo una struct con un tipo di riferimento che è una cattiva pratica.
  • Perché racchiudere una variabile che può già essere nullo con più codice standard. Commenti o nomi possono già indicare che il valore può essere nullo.
  • Non contrassegnare l'oggetto come nullable non garantisce che non sia nullo. Un uso così incoerente porterà a confusione.
  • Sovraccarico delle prestazioni? (Estremamente insufficiente nella maggior parte dei casi).
  • Rende il codice leggermente più difficile da leggere

Con valori nulli

Foo? x;

// <snip>

x = null;

// <snip>

if(x.HasValue) { x.Value.Bar(); }

Senza

Foo x; // Might be null

// <snip>

x = null;

// snip>

if(x != null) { x.Bar(); }
    
posta Roy T. 02.06.2015 - 11:11
fonte

5 risposte

8

Come altri hanno già detto, questo non è possibile, ma ci sono un paio di semplici costrutti che puoi creare per raggiungere obiettivi simili.

Maybe<T>

public struct Maybe<T> where T : class
{
   public bool HasValue { get; private set; }

   private readonly T _value;
   public T Value
   {
       get
       {
           if(!HasValue){throw new InvalidOperationException();}
           return _value;
       }
   }

   public Maybe(T value) : this()
   {
       if(value==null) { throw new ArgumentNullException("value"); }
       HasValue = true;
       _value = value;
   }
}

(Questo potrebbe anche essere implementato in altri modi, come un IEnumerable<T> con il vincolo che ha esattamente 0 o 1 elementi)

NotNull<T>

public class NotNull<T> where T : class
{
    public readonly T Value;

    public NotNull(T value)
    {
        if(item == null){ throw new ArgumentNullException("value"); }
        Value = value;
    }
}

Maybe<T> essenzialmente solo reimplement Nullable<T> per i tipi di riferimento. Ha vantaggi e svantaggi simili a quelli descritti nel tuo post. Probabilmente è prezioso solo se hai una convenzione in tutto il progetto per usarla per qualsiasi valore in cui null è un valore valido.

NotNull<T> è probabile che sia più direttamente ciò che desideri, come indicato da MainMa. Passando attorno ai valori racchiusi in questo tipo è possibile rimuovere la necessità di verificare se un valore è nullo in una clausola di guardia. Tuttavia, non c'è modo di impedire completamente a NotNull<T> di essere null. Non puoi renderlo una struct a causa del requisito di un costruttore predefinito, che lascerebbe Value null.

(MODIFICA: come altri hanno sottolineato, in C # 6 sarà possibile avere un costruttore senza parametri per una struttura, ma non sarà usato quando si inizializzano gli array o si ottiene il risultato di default(T) , quindi è ancora possibile fidatevi veramente della garanzia che Value non sarà nullo)

Quindi, mentre entrambi questi fattori aggiungono potenzialmente un valore nel segnalare se null deve essere considerato valido o meno, sono entrambi limitati dal fatto che richiedono una costante adesione a una convenzione, oltre ai problemi di standardizzazione e leggibilità. Se il trade-off è degno di questo sarebbe una questione di giudizio, ma ricorda che i contratti di codice o altri strumenti di analisi statica esistono anche come un modo alternativo di affrontare questo problema.

    
risposta data 02.06.2015 - 13:44
fonte
4

Nullable<T> ha un vincolo che ha vinto " Ti permettono di usarlo con le classi, ma solo con le strutture.

Se T non è stato limitato , utilizzare Nullable<T> per le classi sarebbe comunque un errore. Il tuo scopo è semplificare il codice, ma in realtà lo stai rendendo più difficile.

In C #, le classi sono sempre annullabili. Dicendo che l'entità nullable è nullable, non stai rendendo il tuo codice più esplicito per i maintainer , poiché lo scopo di tale costrutto è tutt'altro che chiaro. È come se stessi documentando che un metodo che accetta int come parametro dovrebbe prendere un valore tra int.MinValue e int.MaxValue . Rende il codice più chiaro? Non proprio, perché è solo ridondante con le regole del linguaggio.

Di solito, ciò che gli sviluppatori C # cercano è la cosa opposta: come specificare che qualcosa non può essere null pur essendo una classe. Ad esempio, come fai a sapere che public Product CreateProduct(string name, Price price) non può avere un null nome, né un null prezzo e non può restituire un null ?

Contratti di codice è un modo per andare, ma sono ovviamente limitati a pre-condizioni, post-condizioni e invarianti. Se devi specificare globalmente che Price non può mai essere un null perché non ha senso in un contesto della tua logica aziendale, potresti finire per spostare Price da una classe a una struct.

    
risposta data 02.06.2015 - 12:41
fonte
2

Questa non è una questione di buone o cattive pratiche.

Nullable (il tipo che si trova dietro la notazione type? ) non consente i tipi di riferimento:

public struct Nullable where T : struct

È davvero così facile. Solo i tipi di valore possono essere annullabili utilizzando il tipo Nullable<T> .

    
risposta data 02.06.2015 - 12:30
fonte
2

Penso che sarebbe assolutamente incredibile e almeno alcuni membri del team di Roslyn sembrano pensare è una buona idea .

La modifica proposta alla lingua è la! operatore e una modifica al? che potrebbe essere usato per tipi di riferimento che potrebbero essere usati come.

string! a;//can never be null
string  b;//no change
string? c;//can explicitly be null

La bellezza di questa soluzione è che non vi sono modifiche all'IL generato, quindi i problemi di interoperabilità dovrebbero essere minimi. Ci dovrebbe essere un modo per contrassegnare un assemblaggio come sicuro, il che significa che non può usare solo i riferimenti classici! e?

    
risposta data 04.08.2015 - 10:09
fonte
0

La chiave per far funzionare questo è rendere la proprietà Value privata, accessibile solo tramite un'operazione Bind monade o un metodo Estrai che fornisce un valore predefinito di interesse. Questo evita la possibilità di un valore null mai esposto.

public struct MaybeX<TValue> : IEquatable<MaybeX<TValue>> where TValue:class {
  /// <summary>TODO</summary>
  public static readonly MaybeX<TValue>   Nothing  = new MaybeX<TValue>(null); 

  ///<summary>Create a new MaybeX&lt;T>.</summary>
  public MaybeX(TValue value) : this() { _value = value; }

  ///<summary>The monadic Bind operation of type T to type MaybeX&lt;TResult>.</summary>
  [Pure]
  public  MaybeX<TResult>             Bind<TResult>(Func<TValue, MaybeX<TResult>> selector)
  where TResult:class {
      return! HasValue  ?  MaybeX<TResult>.Nothing  :  selector(Value);
  }

  ///<summary>Extract value of the MaybeX&lt;T>, substituting 
  ///<paramref name="defaultValue"/> as needed.</summary>
  [Pure]
  public  TValue                      Extract(TValue defaultValue) {
      return ! HasValue  ?  defaultValue  :  Value;
  }

  ///<summary>Wraps a T as a MaybeX&lt;T>.</summary>
  [Pure]
  public static implicit operator     MaybeX<TValue>(TValue value) {
      return value == null  ?  MaybeX<TValue>.Nothing  :  new MaybeX<TValue>(value);
  }

  ///<summary>Returns whether this MaybeX&lt;T> has a value.</summary>
  public   bool    HasValue { [Pure]get {return Value != null;} }

  ///<summary>If this MaybeX&lt;T> has a value, returns it.</summary>
  internal TValue  Value    { [Pure]get {return _value;} }  readonly TValue _value;
}

Estendendo quindi la classe base con metodi di estensione compatibili con LINQ come questo

[Pure]
public static class MaybeX {
  [Pure]
  public static MaybeX<TResult>           Select<T, TResult>(
      this MaybeX<T> @this,
      Func<T, TResult> projector
  ) where T : class where TResult : class {
      return ! @this.HasValue ? MaybeX<TResult>.Nothing
                              : projector(@this.Value).ToMaybeX();
  }
  [Pure]
  public static MaybeX<TResult>           SelectMany<TValue, TResult>(
      this MaybeX<TValue> @this,
      Func<TValue, MaybeX<TResult>> selector
  ) where TValue : class where TResult : class {
     return @this.Bind(selector);
  }
  [Pure]
  public static MaybeX<TResult>           SelectMany<TValue, T, TResult>(
      this MaybeX<TValue> @this,
      Func<TValue, MaybeX<T>> selector,
      Func<TValue, T, TResult> projector
  ) where T : class where TValue : class where TResult : class {
      return ! @this.HasValue ? MaybeX<TResult>.Nothing
                              : selector(@this.Value).Map(e => projector(@this.Value,e));
  }
}

anche la sintassi di comprensione di LINQ diventa disponibile. Quanto sopra effettivamente rende la proprietà Valore interna invece di privata al fine di consentire l'implementazione degli alias LINQ in modo più efficiente; questo presuppone che le monadi e i loro metodi di estensione LINQ siano soli nel loro assembly.

Nella mia libreria, annoto questa classe completamente con i Contratti di codice come questo

[Pure]
public  MaybeX<TResult>         Bind<TResult>(
    Func<TValue, MaybeX<TResult>> selector
) where TResult:class {
    selector.ContractedNotNull("selector");
    Contract.Ensures(Contract.Result<MaybeX<TResult>>() != null);

    MaybeX<TResult>.Nothing.AssumeInvariant();
    var result = ! HasValue ? MaybeX<TResult>.Nothing
                            : selector(Value);

    Contract.Assume(result != null);              // struct's never null
    //result.AssumeInvariant();                   // TODO - Why is this inadequate?
    return result;
}

e utilizzare i Contratti di codice per dimostrare in modo statico l'assenza di valori nulli di escape.

Questa piccola classe di metodi di estensione rende la scrittura di molti dei contratti di codice molto più semplice ed efficace:

  /// <summary>Extension methods to enhance Code Contracts and integration with Code Analysis.</summary>
  [Pure]
  public static class ContractExtensions {
#if RUNTIME_NULL_CHECKS
    /// <summary>Throws <c>ArgumentNullException{name}</c> if <c>value</c> is null.</summary>
    /// <param name="value">Value to be tested.</param>
    /// <param name="name">Name of the parameter being tested, for use in the exception thrown.</param>
    [ContractArgumentValidator]  // Requires Assemble Mode = Custom Parameter Validation
    public static void ContractedNotNull<T>([ValidatedNotNull]this T value, string name) where T : class {
      if (value == null) throw new ArgumentNullException(name);
      Contract.EndContractBlock();
    }
#else
    /// <summary>Throws <c>ContractException{name}</c> if <c>value</c> is null.</summary>
    /// <param name="value">Value to be tested.</param>
    /// <param name="name">Name of the parameter being tested, for use in the exception thrown.</param>
    [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "value")]
    [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "name")]
    [ContractAbbreviator] // Requires Assemble Mode = Standard Contract Requires
    public static void ContractedNotNull<T>([ValidatedNotNull]this T value, string name) {
        Contract.Requires(value != null, name);
    }
#endif

    /// <summary>Decorator for an object which is to have it's object invariants assumed.</summary>
    [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", MessageId = "t")]
    public static void AssumeInvariant<T>(this T t) { }

    /// <summary>Asserts the 'truth' of the logical implication 
    <paramref name="condition"/> => <paramref name="contract"/>.</summary>
    public static bool Implies(this bool condition, bool contract) {
        Contract.Ensures((! condition || contract)  ==  Contract.Result<bool>() );
        return ! condition || contract;
    }

    /// <summary>Returns true exactly if lower &lt;= value &lt; lower+height</summary>
    /// <param name="value">Vlaue being tested.</param>
    /// <param name="lower">Inclusive lower bound for the range.</param>
    /// <param name="height">Height of the range.</param>
    public static bool InRange(this int value, int lower, int height) {
        Contract.Ensures( (lower <= value && value < lower+height)  ==  Contract.Result<bool>() );
        return lower <= value && value < lower+height;
    }
  }

  /// <summary>Decorator for an incoming parameter that is contractually enforced as NotNull.</summary>
  [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false)]
  public sealed class ValidatedNotNullAttribute : global::System.Attribute {}
    
risposta data 01.03.2016 - 18:43
fonte

Leggi altre domande sui tag