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<T>.</summary>
public MaybeX(TValue value) : this() { _value = value; }
///<summary>The monadic Bind operation of type T to type MaybeX<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<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<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<T> has a value.</summary>
public bool HasValue { [Pure]get {return Value != null;} }
///<summary>If this MaybeX<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 <= value < 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 {}