From ac2e5f2db5a4caffb76011004c7c0a535e2babd8 Mon Sep 17 00:00:00 2001 From: sator-imaging <16752340+sator-imaging@users.noreply.github.com> Date: Mon, 12 Aug 2024 11:52:53 +0900 Subject: [PATCH] feat: Nullable support for UnityEngine.Object --- Editor.meta | 8 ++ Runtime.meta | 8 ++ Runtime/NullableUnityObject.cs | 118 ++++++++++++++++++++++++++++ Runtime/NullableUnityObject.cs.meta | 11 +++ 4 files changed, 145 insertions(+) create mode 100644 Editor.meta create mode 100644 Runtime.meta create mode 100644 Runtime/NullableUnityObject.cs create mode 100644 Runtime/NullableUnityObject.cs.meta diff --git a/Editor.meta b/Editor.meta new file mode 100644 index 0000000..8a4aafe --- /dev/null +++ b/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 28b70498b04f473479e562cd3435c838 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime.meta b/Runtime.meta new file mode 100644 index 0000000..7b36a67 --- /dev/null +++ b/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 830b3c630a7da964487db94e033c6ad4 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Runtime/NullableUnityObject.cs b/Runtime/NullableUnityObject.cs new file mode 100644 index 0000000..7514aeb --- /dev/null +++ b/Runtime/NullableUnityObject.cs @@ -0,0 +1,118 @@ +/** Nullable support for `UnityEngine.Object` + ** (c) 2024 https://github.com/sator-imaging + ** Licensed under the MIT License + +How to Use +========== +Use `.Nullable()` extension method when using null-coalescing operator with `UnityEngine.Object` which may be null. + +```cs +var rb = go.GetComponent<Rigidbody>().Nullable() ?? go.AddComponent<Rigidbody>(); +var rb = go.GetComponent<Rigidbody>().Nullable() ?? throw new Exception(); +``` + + */ + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Object = UnityEngine.Object; + +#nullable enable + +namespace SatorImaging.UnityFundamentals +{ + +#pragma warning disable CS0618 // NOTE: to prevent misuse, NullableUnityObject<T> is marked as obsolete + + public static class NullableUnityObjectExtensions + { + // NOTE: don't use [DoesNotReturn] attribute!! + // it will completely stop nullability analysis in context after method call!! + + // TODO: not able to specify method return value by [NotNullIfNotNull("return")] + // any way to mark `self` is not null after this method is called? + // NOTE: if first parameter is `this T self`, it shows warning when `self` type is `GameObject?` or other nullable ref type. + // [AllowNull] won't work as expected in this case. to suppress warning, it must be declared as `this T? self`. + // NOTE: no performance gain so should not set inlining explicitly + //[MethodImpl(MethodImplOptions.AggressiveInlining)] + public static NullableUnityObject<T>? Nullable<T>([AllowNull] this T? self) // must be `this T? self` + where T : Object + { + return (self == null) ? null : new NullableUnityObject<T>(self); + } + + + /// <exception cref="NullReferenceException"></exception> + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [return: NotNull] + [Obsolete("use `.Nullable() ?? throw new...` instead")] + public static T ThrowIfNull<T>([AllowNull] this T? self, string? message = null, Exception? innerException = null) // must be `this T? self` + where T : Object + { + if (self == null) + throw new NullReferenceException(message, innerException); + + return self; + } + } +#pragma warning restore CS0618 + + + /* typedef ================================================================ */ + + /// <summary> + /// > [!CAUTION] + /// > !!! this struct must not be used except for designated usage !!! + /// </summary> + [Obsolete("!!! this struct must not be used except for designated usage !!!")] + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly struct NullableUnityObject<T> : IEquatable<NullableUnityObject<T>>, IEquatable<T> + where T : Object + { + readonly T value; + + [Obsolete("use `Nullable()` extension method for unity object instead of instantiating manually")] + [EditorBrowsable(EditorBrowsableState.Never)] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal NullableUnityObject(T value) + { + //if (value == null) + // throw new ArgumentNullException(nameof(value)); + + this.value = value; + } + + readonly public T Value + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => value; + } + + readonly public bool HasValue + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => value != null; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T(NullableUnityObject<T> self) => self.value; + + readonly public override string ToString() => value.ToString(); + readonly public override int GetHashCode() => value.GetHashCode(); + + // NOTE: UnityEngine.Object.Equals has NOT overriden!! + // use == or != operator!! + readonly public override bool Equals(object? obj) => (obj is NullableUnityObject<T> nuo && nuo.value == value) || (obj is T uo && uo == value); + readonly public bool Equals(T? other) => value == other; + readonly public bool Equals(NullableUnityObject<T> other) => value == other.value; + + public static bool operator ==(NullableUnityObject<T> left, NullableUnityObject<T> right) => left.value == right.value; + public static bool operator !=(NullableUnityObject<T> left, NullableUnityObject<T> right) => left.value != right.value; + public static bool operator ==(NullableUnityObject<T> left, T? right) => left.value == right; + public static bool operator !=(NullableUnityObject<T> left, T? right) => left.value != right; + public static bool operator ==(T? left, NullableUnityObject<T> right) => left == right.value; + public static bool operator !=(T? left, NullableUnityObject<T> right) => left != right.value; + } +} diff --git a/Runtime/NullableUnityObject.cs.meta b/Runtime/NullableUnityObject.cs.meta new file mode 100644 index 0000000..602056c --- /dev/null +++ b/Runtime/NullableUnityObject.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 60ff2b47892d27b44b14eeca5fe82f52 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: