using System; using System.Collections.Generic; using System.Runtime.Serialization; using System.Security.Permissions; namespace NitroxModel.DataStructures.Util { /// /// Used to give context on whether the wrapped value is nullable and to improve error logging. /// /// /// Used some hacks to circumvent C#' lack of reverse type inference (usually need to specify the type when returning a /// value using a generic method). /// Code from https://tyrrrz.me/blog/return-type-inference /// /// [Serializable] [DataContract] public struct Optional : ISerializable, IEquatable> where T : class { private delegate bool HasValueDelegate(T value); /// /// List of condition checks for current type (due to being a static on generic class). /// private static List> valueChecks; /// /// Has value check that can be replaced and defaults to generating a value check for current based on /// global filter conditions that were set. /// private static HasValueDelegate valueChecksForT = value => { // Generate new HasValue check based on global filters for types. Type type = typeof(T); bool isObj = type == typeof(object); foreach (KeyValuePair> filter in Optional.ValueConditions) { if (isObj || filter.Key.IsAssignableFrom(type)) { // Only create the list in memory when required. valueChecks ??= new List>(); // Exclude check for Optional if the type doesn't match the type of the filter (because it'll always be null for `o as T`) valueChecks.Add(isObj ? o => !filter.Key.IsInstanceOfType(o) || filter.Value(o) : filter.Value); } } // Update check to just check has values directly for future calls (this is an optimization). if (valueChecks != null) { valueChecksForT = val => { if (ReferenceEquals(val, null)) { return false; } foreach (Func check in valueChecks) { if (!check(val)) { return false; } } return true; }; } else { valueChecksForT = val => !ReferenceEquals(val, null); } // Give initial result based on the updated check delegate return valueChecksForT(value); }; [DataMember(Order = 1)] public T Value { get; private set; } public bool HasValue => valueChecksForT(Value); private Optional(T value) { Value = value; } public T OrElse(T elseValue) { return HasValue ? Value : elseValue; } public Optional OrElse(Func elseValue) => HasValue ? Value : elseValue(); public T OrNull() => HasValue ? Value : null; internal static Optional Of(T value) { if (value == null) { throw new ArgumentNullException(nameof(value), $"Tried to set null on {typeof(Optional)}"); } return new Optional(value); } internal static Optional OfNullable(T value) { return !valueChecksForT(value) ? Optional.Empty : new Optional(value); } public override string ToString() { string str = Value != null ? Value.ToString() : "Nothing"; return $"Optional Contains: {str}"; } private Optional(SerializationInfo info, StreamingContext context) { Value = (T)info.GetValue("value", typeof(T)); } public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue("value", Value); } #pragma warning disable CS0618 // OptionalEmpty is only allowed to be used internally public static implicit operator Optional(OptionalEmpty none) { return new Optional(); } #pragma warning restore CS0618 public static implicit operator Optional?(T obj) { if (obj == null) { return null; } return new Optional(obj); } public static implicit operator Optional(T obj) { return Optional.Of(obj); } public static explicit operator T(Optional value) { return value.Value; } public bool Equals(Optional other) { return EqualityComparer.Default.Equals(Value, other.Value); } public override bool Equals(object obj) { return obj is Optional other && Equals(other); } public override int GetHashCode() { return EqualityComparer.Default.GetHashCode(Value); } public static bool operator ==(Optional left, Optional right) { return left.Equals(right); } public static bool operator !=(Optional left, Optional right) { return !left.Equals(right); } } [Obsolete("Use Optional.Empty instead. This struct is required to trick the compiler for the lack of reverse type inference.")] public struct OptionalEmpty { public OptionalEmpty() { } } public static class Optional { internal static readonly Dictionary> ValueConditions = new(); #pragma warning disable CS0618 // OptionalEmpty is only allowed to be used internally public static OptionalEmpty Empty { get; } = new(); #pragma warning restore CS0618 public static Optional Of(T value) where T : class => Optional.Of(value); public static Optional OfNullable(T value) where T : class => Optional.OfNullable(value); /// /// Adds a condition to the optional of the given type that is checked whenever is /// checked. /// /// Condition to add to the check. /// /// Type that should have the extra condition. The given type will also apply to more specific types than /// itself. /// public static void ApplyHasValueCondition(Func hasValueCondition) where T : class { // Add to global so that the Optional can lazily evaluate which conditions it should add to its checks based on its type. ValueConditions.Add(typeof(T), o => hasValueCondition(o as T)); } } public sealed class OptionalNullException : Exception { public OptionalNullException() : base($"Optional <{nameof(T)}> is null!") { } public OptionalNullException(string message) : base($"Optional <{nameof(T)}> is null:\n\t{message}") { } } [Serializable] public sealed class OptionalEmptyException : Exception { public OptionalEmptyException() : base($"Optional <{nameof(T)}> is empty.") { } public OptionalEmptyException(string message) : base($"Optional <{nameof(T)}> is empty:\n\t{message}") { } } }