diff --git a/osu.Framework/Graphics/Performance/LifetimeEntry.cs b/osu.Framework/Graphics/Performance/LifetimeEntry.cs index f67bdfc5d1..f14a09eda2 100644 --- a/osu.Framework/Graphics/Performance/LifetimeEntry.cs +++ b/osu.Framework/Graphics/Performance/LifetimeEntry.cs @@ -1,24 +1,30 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; namespace osu.Framework.Graphics.Performance { /// - /// An object for a to consume, which provides a and . + /// An object for a to consume, which provides a and . /// /// /// Management of the object which the refers to is left up to the consumer. /// - public class LifetimeEntry + public class LifetimeEntry : LifetimeEntryBase + { + } + + /// + /// The required base type for the to consume, which provides a and . + /// + /// The implemented class itself. Used to provide derived category information for base categories using the Curiously Recurring Template Pattern(CRTP). + public abstract class LifetimeEntryBase where TDerived : LifetimeEntryBase { private double lifetimeStart = double.MinValue; /// - /// The time at which this becomes alive in a . + /// The time at which this becomes alive in a . /// public double LifetimeStart { @@ -30,7 +36,7 @@ public double LifetimeStart private double lifetimeEnd = double.MaxValue; /// - /// The time at which this becomes dead in a . + /// The time at which this becomes dead in a . /// public double LifetimeEnd { @@ -42,15 +48,15 @@ public double LifetimeEnd /// Invoked before or changes. /// It is used because cannot be used to ensure comparator stability. /// - internal event Action RequestLifetimeUpdate; + internal event Action? RequestLifetimeUpdate; /// /// Invoked after or changes. /// - public event Action LifetimeChanged; + public event Action? LifetimeChanged; /// - /// Update of this . + /// Update of this . /// protected virtual void SetLifetimeStart(double start) { @@ -59,7 +65,7 @@ protected virtual void SetLifetimeStart(double start) } /// - /// Update of this . + /// Update of this . /// protected virtual void SetLifetimeEnd(double end) { @@ -68,27 +74,44 @@ protected virtual void SetLifetimeEnd(double end) } /// - /// Updates the stored lifetimes of this . + /// Updates the stored lifetimes of this . /// /// The new value. /// The new value. protected void SetLifetime(double start, double end) { - RequestLifetimeUpdate?.Invoke(this); + // Due to the type constraints of C#, we cannot declare `LifetimeEntry where T = LifetimeEntry` to limit the type of `this` to be `T`. But when used correctly, this will always be the case. + // aka. We can't stop anyone from writing code like this: + // ```csharp + // public class MyEntry : LifetimeEntry { } + // LifetimeEntry foo = new(); + // ``` + // We prevent users from inadvertently writing such code by declaring `LifetimeEntry` as `abstract`, but we cannot prevent it completely. + // ```csharp + // public class NewEntry : LifetimeEntry where T : LifetimeEntry { } + // NewEntry a = new(); + // ``` + // Happily, however, the compiler correctly prevents code like this from compiling. This is also enough to deter users from writing incorrect code: + // ```csharp + // public class ErrorEntry : NewEntry { } + // LifetimeEntryManager> error1 = new(); // Compiler error. + // LifetimeEntryManager error2 = new(); // Compiler error. + // ``` + RequestLifetimeUpdate?.Invoke((TDerived)this); lifetimeStart = start; lifetimeEnd = Math.Max(start, end); // Negative intervals are undesired. - LifetimeChanged?.Invoke(this); + LifetimeChanged?.Invoke((TDerived)this); } /// - /// The current state of this . + /// The current state of this . /// internal LifetimeEntryState State { get; set; } /// - /// Uniquely identifies this in a . + /// Uniquely identifies this in a . /// internal ulong ChildId { get; set; } } diff --git a/osu.Framework/Graphics/Performance/LifetimeEntryManager.cs b/osu.Framework/Graphics/Performance/LifetimeEntryManager.cs index dd42cc464f..437d84d761 100644 --- a/osu.Framework/Graphics/Performance/LifetimeEntryManager.cs +++ b/osu.Framework/Graphics/Performance/LifetimeEntryManager.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -#nullable disable - using System; using System.Collections.Generic; using System.Diagnostics; + using JetBrains.Annotations; + using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Statistics; @@ -16,49 +16,58 @@ namespace osu.Framework.Graphics.Performance /// /// Provides time-optimised updates for lifetime change notifications. /// This is used in specialised s to optimise lifetime changes (see: ). + /// If you wish to implement your own , please use the generic version . /// /// /// The time complexity of updating lifetimes is O(number of alive items). /// - public class LifetimeEntryManager + public class LifetimeEntryManager : LifetimeEntryManager + { + } + + /// + /// Provides time-optimised updates for lifetime change notifications. + /// + /// Implementation of . Note that your own class must inherit from the generic version rather than . + public class LifetimeEntryManager where T : LifetimeEntryBase { /// - /// Invoked immediately when a becomes alive. + /// Invoked immediately when a becomes alive. /// - public event Action EntryBecameAlive; + public event Action? EntryBecameAlive; /// - /// Invoked immediately when a becomes dead. + /// Invoked immediately when a becomes dead. /// - public event Action EntryBecameDead; + public event Action? EntryBecameDead; /// - /// Invoked when a crosses a lifetime boundary. + /// Invoked when a crosses a lifetime boundary. /// - public event Action EntryCrossedBoundary; + public event Action? EntryCrossedBoundary; /// /// Contains all the newly-added (but not yet processed) entries. /// - private readonly List newEntries = new List(); + private readonly List newEntries = []; /// /// Contains all the currently-alive entries. /// - private readonly List activeEntries = new List(); + private readonly List activeEntries = []; /// /// Contains all entries that should come alive in the future. /// - private readonly SortedSet futureEntries = new SortedSet(new LifetimeStartComparator()); + private readonly SortedSet futureEntries = new(new LifetimeStartComparator()); /// /// Contains all entries that were alive in the past. /// - private readonly SortedSet pastEntries = new SortedSet(new LifetimeEndComparator()); + private readonly SortedSet pastEntries = new(new LifetimeEndComparator()); - private readonly Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)> eventQueue = - new Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)>(); + private readonly Queue<(T, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)> eventQueue = + new(); /// /// Used to ensure a stable sort if multiple entries with the same lifetime are added. @@ -68,8 +77,8 @@ public class LifetimeEntryManager /// /// Adds an entry. /// - /// The to add. - public void AddEntry(LifetimeEntry entry) + /// The to add. + public void AddEntry(T entry) { entry.RequestLifetimeUpdate += requestLifetimeUpdate; entry.ChildId = ++currentChildId; @@ -81,9 +90,9 @@ public void AddEntry(LifetimeEntry entry) /// /// Removes an entry. /// - /// The to remove. - /// Whether was contained by this . - public bool RemoveEntry(LifetimeEntry entry) + /// The to remove. + /// Whether was contained by this . + public bool RemoveEntry(T entry) { entry.RequestLifetimeUpdate -= requestLifetimeUpdate; @@ -160,7 +169,7 @@ public void ClearEntries() /// /// Invoked when the lifetime of an entry is going to changed. /// - private void requestLifetimeUpdate(LifetimeEntry entry) + private void requestLifetimeUpdate(T entry) { // Entries in the past/future sets need to be re-sorted to prevent the comparer from becoming unstable. // To prevent, e.g. CompositeDrawable alive children changing during enumeration, the entry's state must not be updated immediately. @@ -185,7 +194,7 @@ private void requestLifetimeUpdate(LifetimeEntry entry) /// The . /// Either , , or null. [CanBeNull] - private SortedSet futureOrPastEntries(LifetimeEntryState state) + private SortedSet? futureOrPastEntries(LifetimeEntryState state) { switch (state) { @@ -274,9 +283,9 @@ public bool Update(double startTime, double endTime) } /// - /// Updates the state of a single . + /// Updates the state of a single . /// - /// The to update. + /// The to update. /// The start of the time range. /// The end of the time range. /// Whether is part of the new entries set. @@ -284,7 +293,7 @@ public bool Update(double startTime, double endTime) /// Whether should be mutated by this invocation. /// If false, the caller is expected to handle mutation of based on any changes to the entry's state. /// Whether the state of has changed. - private bool updateChildEntry(LifetimeEntry entry, double startTime, double endTime, bool isNewEntry, bool mutateActiveEntries) + private bool updateChildEntry(T entry, double startTime, double endTime, bool isNewEntry, bool mutateActiveEntries) { LifetimeEntryState oldState = entry.State; @@ -343,11 +352,11 @@ private bool updateChildEntry(LifetimeEntry entry, double startTime, double endT /// /// Retrieves the new state for an entry. /// - /// The . + /// The . /// The start of the time range. /// The end of the time range. /// The state of . Can be either , , or . - private LifetimeEntryState getState(LifetimeEntry entry, double startTime, double endTime) + private LifetimeEntryState getState(T entry, double startTime, double endTime) { // Consider a static entry and a moving time range: // [-----------Entry-----------] @@ -367,7 +376,7 @@ private LifetimeEntryState getState(LifetimeEntry entry, double startTime, doubl return LifetimeEntryState.Current; } - private void enqueueEvents(LifetimeEntry entry, LifetimeEntryState oldState, LifetimeEntryState newState) + private void enqueueEvents(T entry, LifetimeEntryState oldState, LifetimeEntryState newState) { Debug.Assert(oldState != newState); @@ -394,11 +403,11 @@ private void enqueueEvents(LifetimeEntry entry, LifetimeEntryState oldState, Lif } /// - /// Compares by . + /// Compares by . /// - private sealed class LifetimeStartComparator : IComparer + private sealed class LifetimeStartComparator : IComparer { - public int Compare(LifetimeEntry x, LifetimeEntry y) + public int Compare(T? x, T? y) { ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); @@ -409,11 +418,11 @@ public int Compare(LifetimeEntry x, LifetimeEntry y) } /// - /// Compares by . + /// Compares by . /// - private sealed class LifetimeEndComparator : IComparer + private sealed class LifetimeEndComparator : IComparer { - public int Compare(LifetimeEntry x, LifetimeEntry y) + public int Compare(T? x, T? y) { ArgumentNullException.ThrowIfNull(x); ArgumentNullException.ThrowIfNull(y); diff --git a/osu.Framework/Graphics/Performance/LifetimeEntryState.cs b/osu.Framework/Graphics/Performance/LifetimeEntryState.cs index d7a094a6e8..558daa3f99 100644 --- a/osu.Framework/Graphics/Performance/LifetimeEntryState.cs +++ b/osu.Framework/Graphics/Performance/LifetimeEntryState.cs @@ -4,27 +4,27 @@ namespace osu.Framework.Graphics.Performance { /// - /// The state of a . + /// The state of a . /// public enum LifetimeEntryState { /// - /// The hasn't been processed within the yet. + /// The hasn't been processed within the yet. /// New, /// - /// The is currently dead and becomes alive when current time >= . + /// The is currently dead and becomes alive when current time >= . /// Future, /// - /// The is currently alive. + /// The is currently alive. /// Current, /// - /// The is currently dead and becomes alive when current time < . + /// The is currently dead and becomes alive when current time < . /// Past, }