236 lines
7.9 KiB
C#
236 lines
7.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.Serialization;
|
|
using System.Security.Permissions;
|
|
|
|
namespace NitroxModel.DataStructures.Util
|
|
{
|
|
/// <summary>
|
|
/// Used to give context on whether the wrapped value is nullable and to improve error logging.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 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
|
|
/// </remarks>
|
|
/// <typeparam name="T"></typeparam>
|
|
[Serializable]
|
|
[DataContract]
|
|
public struct Optional<T> : ISerializable, IEquatable<Optional<T>> where T : class
|
|
{
|
|
private delegate bool HasValueDelegate(T value);
|
|
|
|
/// <summary>
|
|
/// List of <see cref="HasValue" /> condition checks for current type (due to being a static on generic class).
|
|
/// </summary>
|
|
private static List<Func<object, bool>> valueChecks;
|
|
|
|
/// <summary>
|
|
/// Has value check that can be replaced and defaults to generating a value check for current <see cref="T" /> based on
|
|
/// global filter conditions that were set.
|
|
/// </summary>
|
|
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<Type, Func<object, bool>> filter in Optional.ValueConditions)
|
|
{
|
|
if (isObj || filter.Key.IsAssignableFrom(type))
|
|
{
|
|
// Only create the list in memory when required.
|
|
valueChecks ??= new List<Func<object, bool>>();
|
|
|
|
// Exclude check for Optional<object> 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<object, bool> 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<T> OrElse(Func<T> elseValue) => HasValue ? Value : elseValue();
|
|
|
|
public T OrNull() => HasValue ? Value : null;
|
|
|
|
internal static Optional<T> Of(T value)
|
|
{
|
|
if (value == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(value), $"Tried to set null on {typeof(Optional<T>)}");
|
|
}
|
|
|
|
return new Optional<T>(value);
|
|
}
|
|
|
|
internal static Optional<T> OfNullable(T value)
|
|
{
|
|
return !valueChecksForT(value) ? Optional.Empty : new Optional<T>(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<T>(OptionalEmpty none)
|
|
{
|
|
return new Optional<T>();
|
|
}
|
|
#pragma warning restore CS0618
|
|
|
|
public static implicit operator Optional<T>?(T obj)
|
|
{
|
|
if (obj == null)
|
|
{
|
|
return null;
|
|
}
|
|
return new Optional<T>(obj);
|
|
}
|
|
|
|
public static implicit operator Optional<T>(T obj)
|
|
{
|
|
return Optional.Of(obj);
|
|
}
|
|
|
|
public static explicit operator T(Optional<T> value)
|
|
{
|
|
return value.Value;
|
|
}
|
|
public bool Equals(Optional<T> other)
|
|
{
|
|
return EqualityComparer<T>.Default.Equals(Value, other.Value);
|
|
}
|
|
|
|
public override bool Equals(object obj)
|
|
{
|
|
return obj is Optional<T> other && Equals(other);
|
|
}
|
|
|
|
public override int GetHashCode()
|
|
{
|
|
return EqualityComparer<T>.Default.GetHashCode(Value);
|
|
}
|
|
|
|
public static bool operator ==(Optional<T> left, Optional<T> right)
|
|
{
|
|
return left.Equals(right);
|
|
}
|
|
|
|
public static bool operator !=(Optional<T> left, Optional<T> 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<Type, Func<object, bool>> 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<T> Of<T>(T value) where T : class => Optional<T>.Of(value);
|
|
public static Optional<T> OfNullable<T>(T value) where T : class => Optional<T>.OfNullable(value);
|
|
|
|
/// <summary>
|
|
/// Adds a condition to the optional of the given type that is checked whenever <see cref="Optional{T}.HasValue" /> is
|
|
/// checked.
|
|
/// </summary>
|
|
/// <param name="hasValueCondition">Condition to add to the <see cref="Optional{T}.HasValue" /> check.</param>
|
|
/// <param arg="T">
|
|
/// Type that should have the extra condition. The given type will also apply to more specific types than
|
|
/// itself.
|
|
/// </param>
|
|
public static void ApplyHasValueCondition<T>(Func<T, bool> hasValueCondition) where T : class
|
|
{
|
|
// Add to global so that the Optional<T> 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<T> : 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<T> : Exception
|
|
{
|
|
public OptionalEmptyException() : base($"Optional <{nameof(T)}> is empty.")
|
|
{
|
|
}
|
|
|
|
public OptionalEmptyException(string message) : base($"Optional <{nameof(T)}> is empty:\n\t{message}")
|
|
{
|
|
}
|
|
}
|
|
}
|