Search Unity

Question Any Tips for cleaner Lazy Load pattern?

Discussion in 'Scripting' started by sand_lantern, Nov 11, 2020.

  1. sand_lantern

    sand_lantern

    Joined:
    Sep 15, 2017
    Posts:
    210
    So, I've found that I'm doing things like this a ton:
    Code (CSharp):
    1. public class MyScript : MonoBehaviour
    2. {
    3.    private MyLazyComponent _lazyBoy;
    4.    protected MyLazyComponent LazyBoy
    5.    {
    6.       get
    7.       {
    8.          if (!_lazyBoy)
    9.          {
    10.             _lazyBoy = GetComponent<MyLazyComponent> ();
    11.          }
    12.          return _lazyBoy;
    13.       }
    14.    }
    15.  
    16.    public void DoSomething(){
    17.       LazyBoy.DoYourThing();
    18.    }
    19. }
    20.  
    It works nicely and cleans up my functions. But, it sure is a lot of boilerplate. I was looking for some patterns on stack overflow, and the suggestion was to wrap it up in a class, so I ended up trying something like this
    Code (CSharp):
    1. public class Lazy<T> where T : MonoBehaviour
    2. {
    3.     private T _value = null;
    4.     public Lazy(Component ctxt) {
    5.         getValue += ctxt.GetComponent<T>;
    6.     }
    7.  
    8.     private delegate T GetValueFunc();
    9.     private GetValueFunc getValue;
    10.     public static implicit operator T (Lazy<T> d) => d.Value;
    11.     private T Value
    12.     {
    13.         get
    14.         {
    15.             if (!_value)
    16.             {
    17.                 _value = getValue();
    18.                 getValue = null;
    19.             }
    20.             return _value;
    21.         }
    22.     }
    23. }
    24.  
    25. public class MyScript : MonoBehaviour
    26. {
    27.    private Lazy<MyLazyComponent> LazyBoy = new Lazy<MyLazyComponent>(this);
    28.  
    29.    public void DoSomething(){
    30.       LazyBoy.DoYourThing();
    31.    }
    32. }
    33.  

    It looks clean enough, but unfortunately, when I try to put such objects in my MonoBehaviours, I run into the following issue on LazyBoy's initialization:
    Keyword 'this' is not available in the current context


    Now obviously I can initialize my object in Awake or Enable or where ever, but a lot of my scripts don't have any need for those functions, it's a potential point of forgetting to initialize, one more step, and it's basically just moving my boilerplate from one location to another.

    Is there another trick to try, or is this pattern just SOL for cleaning up?
     
    Last edited: Nov 11, 2020
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,744
    I hate having to put random crap in my scenes that actually gives me no value whatsoever by its presence, i.e., it isn't connected to anything.

    I love the lazy pattern, but you're not quite doing it right for Unity. Check this:

    Some super-simple Singleton examples to take and modify:

    Simple Unity3D Singleton (no predefined data):

    https://pastebin.com/SuvBWCpJ

    Unity3D Singleton with Prefab used for predefined data:

    https://pastebin.com/cv1vtS6G

    These are pure-code solutions, do not put anything into any scene, just access it via .Instance!

    Obviously if you actually WANT them to go away when the scene changes, just remove the DontDestroyOnLoad() lines.
     
    eses likes this.
  3. sand_lantern

    sand_lantern

    Joined:
    Sep 15, 2017
    Posts:
    210
    I actually use a pattern similar to the one you listed for quite a few things as well (though I'm using ScriptableObjects instead of MonoBehaviours for it and maybe that's a good discussion to have in its own right), but I don't think it's the same thing that I'm doing here.

    What I'm talking about is more like, getting a Collider2D off of an object to do some stuff with or a sprite to change its color. The sentiment is still the same though. I'm avoiding cluttering my scene object scripts with references to various objects floating in my hierarchy.
     
  4. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    Here's something I've cobbled together as an experiment.
    In theory, this would cache every GameObject/Component reference as they're called, but I've not tested this and I doubt it would perform too well.
    Code (CSharp):
    1. public static class ComponentCache {
    2.     private static readonly Dictionary<GameObject, HashSet<Component>> cache = new Dictionary<GameObject, HashSet<Component>>();
    3.  
    4.     public static T Get<T>(GameObject gameObject) where T : Component {
    5.         T component = GetCachedComponent<T>(gameObject);
    6.  
    7.         if(component == null && gameObject.TryGetComponent(out component)) {
    8.             cache.Add(gameObject, new HashSet<Component> { component });
    9.         }
    10.  
    11.         return component;
    12.     }
    13.  
    14.     private static T GetCachedComponent<T>(GameObject gameObject) where T : Component {
    15.         T component = null;
    16.  
    17.         if(cache.ContainsKey(gameObject)) {
    18.             foreach(Component cachedComponent in cache[gameObject]) {
    19.                 if(cachedComponent is T instance) {
    20.                     component = instance;
    21.                     break;
    22.                 }
    23.             }
    24.         }
    25.  
    26.         return component;
    27.     }
    28. }
    Usage would be something like this:
    Code (CSharp):
    1. public class SomeMonoBehaviour : MonoBehaviour {
    2.    //Get the Rigidbody on this GameObject.
    3.    Rigidbody Body => ComponentCache.Get<Rigidbody>(gameObject);
    4.  
    5.    void FixedUpdate() {
    6.       Body.AddForce(/*etc...*/);
    7.    }
    8. }
     
    sand_lantern likes this.
  5. sand_lantern

    sand_lantern

    Joined:
    Sep 15, 2017
    Posts:
    210
    Something about that looks like a memory leak waiting to happen, but it did give me another idea.

    Code (CSharp):
    1.  
    2. public class MyScript : MonoBehaviour
    3. {
    4.     private Lazy<Collider2D> _lazy = new Lazy<Collider2D>();
    5.     public Collider2D Collider => _lazy.Value(this);
    6. }
    7.  
    8. public class Lazy<T> where T : MonoBehaviour
    9. {
    10.     private T _value = null;
    11.  
    12.     public T Value(Component ctxt)
    13.     {
    14.         if (!_value)
    15.         {
    16.             _value = ctxt.GetComponent<T> ();
    17.         }
    18.         return _value;
    19.     }
    20. }
    Still has some boilerplate, but at least it contains the getter inside of my Lazy class. Want to crunch it down some more if I can though before I think it'd be worth using.
     
  6. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,744
    Well, my knee-jerk reflex when I see the above is to squint and go "hmm... w ... t ... f ... ?"

    But if it makes you feel better to hide it away, by all means. I just like stuff all laid out because you just KNOW that someday in the future it's gonna fail, and an exception thrown from inside some generic<T>()-ish class is gonna be harder to reason about once you have forgotten what trickery you did above.

    I forget more code than I have ever written, so I try to make the code SUPER-simple, so when I come back to it because it is throwing an exception, odds are I have forgotten about the code, but I can still easily reason about it without digging into a bunch of weird generic classes.

    Also, consider if a future teammate might be less comfortable in C# and such a non-standard construct would throw him for a loop.
     
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    There is no way to handle everything inside a field initializer because there is no way to access the containing class from a field initializer since those are executed before the constructor is executed. So the compiler does not allow any access to "this" that way. So the earliest point where you can access this is inside the constructor of your MonoBehaviour. Though there's another pattern. Not as clean as you wish, but this would work. First you could implement an extension method like this:

    Code (CSharp):
    1. public static class ComponentInitializeExt
    2. {
    3.     public static T GetComponentExt<T>(this Component aComp, ref T aBackingField)
    4.     {
    5.         if (aBackingField == null)
    6.             aBackingField = aComp.GetComponent<T>();
    7.         return aBackingField;
    8.     }
    9. }
    10.  
    Now inside your MonoBehaviour you have to declare two things: the actual backing field and a property like this:

    Code (CSharp):
    1. private YourComponent m_YourComponent;
    2. public YourComponent yComp => this.GetComponentExt(ref m_YourComponent);
    Now using yComp will lazy initialize your backing field and return the component as expected. Note this only works because the body of the property is a member method of the class you declare this property in. Therefore it has access to "this". This can not be further simplified.

    Another approach would be to use some reflection based initialization. Though this would be more like some sort of dependency injection. There is no real slick way of doing auto initialization like this. In the end you always need some execution point after the object has been created.
     
    Vryken and sand_lantern like this.
  8. Madgvox

    Madgvox

    Joined:
    Apr 13, 2014
    Posts:
    1,317
    At some point I decided to stop fighting Unity and use

    Code (CSharp):
    1. [SerializeField]
    2. private SomeComponent someField;
    everywhere. The private keeps it out of my intellisense, and the serialization keeps the code side clean at the expense of depending on the editor a bit more.
     
  9. sand_lantern

    sand_lantern

    Joined:
    Sep 15, 2017
    Posts:
    210
    I like this a lot. It's similar to where I was heading with it, but it removes the need for a backing LazyLoad class. Still a little boiler plate, but not as much and I'm not instantiating extra objects just to use as reference holders.
     
  10. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    Yes, I hadn't seen your post as I had this tab open for too long without refreshing -.- Though kinda funny we came up with a similar approach. Instead of a class you could use a struct
     
  11. sand_lantern

    sand_lantern

    Joined:
    Sep 15, 2017
    Posts:
    210
    No need for that. I'm happy with the extension method you came up with.
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    I never tried it before, but I wonder if Lazy<T> works well in Unity for this sort of thing...

    https://docs.microsoft.com/en-us/dotnet/framework/performance/lazy-initialization

    Try something like:
    Code (csharp):
    1.  
    2. private Lazy<MyLazyComponent> _lazyBoy = new Lazy<MyLazyComponent>(() => GetComponent<MyLazyComponent>());
    3. public MyLazyComponent LazyBoy { get { return _lazyBoy.Value; } }
    4.  
    My only concern really would be the Unity ! operator and how it resolves destroyed objects. This wouldn't respect that.

    [edit]
    Sorry, I just saw that @sand_lantern already suggested basically the same thing. Though they wrote their own imp of Lazy<T> instead of using the one built into System... which actually would resolve for the ! operator and destroyed objects.
     
  13. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    Nope, that doesn't work for the same reason. To use GetComponent in your lambda expression you essentially use this.GetComponent. However the access to "this" or any other instance members is not allowed inside field initializers. Only static members can be used in field initializers. So you can not get any reference to the instance before the constructor is executed. This always happens after all field initializers have been executed.
     
  14. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Ah yeah, derp, I forgot that it's in a field initializer.
     
  15. sand_lantern

    sand_lantern

    Joined:
    Sep 15, 2017
    Posts:
    210
    Maybe I'm going mad with power, but here's what I have for posterity sake:

    Code (CSharp):
    1.  
    2. public static T LazyGet<T>(this Component comp, ref T backingField) where T : Component
    3. {
    4.     if (backingField == null || backingField.Equals(null))
    5.     {
    6.         backingField = comp.GetComponent<T> ();
    7.     }
    8.     return backingField;
    9. }
    10.  
    11. public static T LazyGetChild<T> (this Component comp, ref T backingField) where T : Component
    12. {
    13.     if (backingField == null || backingField.Equals(null))
    14.     {
    15.         backingField = comp.GetComponentInChildren<T> ();
    16.     }
    17.     return backingField;
    18. }
    19.  
    20. public static T LazyGetParent<T> (this Component comp, ref T backingField) where T : Component
    21. {
    22.     if (backingField == null || backingField.Equals(null))
    23.     {
    24.         backingField = comp.GetComponentInParent<T> ();
    25.     }
    26.     return backingField;
    27. }
    28.  
    29. public static T LazyNew<T> (this Component _, ref T backingField) where T : new()
    30. {
    31.     if (backingField == null || backingField.Equals(null))
    32.     {
    33.         backingField = new T();
    34.     }
    35.     return backingField;
    36. }
    Edit: Updated to use Bunny83's better null checks.
     
    Last edited: Jan 19, 2023
    TheZampo and Oneiros90 like this.
  16. TheZampo

    TheZampo

    Joined:
    Jul 10, 2021
    Posts:
    2

    I was going to write exactly this. But I thought "Somebody probably dit it already"... and here we are! Thanks!
     
    sand_lantern likes this.
  17. inghy_

    inghy_

    Joined:
    Sep 15, 2021
    Posts:
    5
    Not sure if it can help, but usually I do like this:


    Code (CSharp):
    1. private MyLazyComponent _lazyBoy;
    2. protected MyLazyComponent LazyBoy => _lazyBoy ??= GetComponent<MyLazyComponent>();
    this behaves same as the code you wrote, but looks much cleaner
     
  18. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,003
    Unfortunately, no. It does not behave the same way. The issue are fake null objects. If the referenced object got destroyed and recreated, your lazy loader would not pick up the new instance but keep the old, dead instance. And it's debatable if it looks cleaner ^^

    I just realised my original solution suffers from the same problem, as the generic argument does not have a constraint to Component. Though it's usually better to not have that constraint in order to support interfaces. So a better implementation would be
    Code (CSharp):
    1.     public static class ComponentInitializeExt
    2.     {
    3.         public static T GetComponentExt<T>(this Component aComp, ref T aBackingField)
    4.         {
    5.             if (aBackingField == null || aBackingField.Equals(null))
    6.                 aBackingField = aComp.GetComponent<T>();
    7.             return aBackingField;
    8.         }
    9.     }
     
    inghy_ and Kurt-Dekker like this.
  19. BrianJiang

    BrianJiang

    Joined:
    Jun 11, 2017
    Posts:
    7
    If anyone want to lazy load assets that was directly referenced, try out the SmartReference: https://github.com/Brian-Jiang/SmartReference
    Basically I use asset path instead of direct referencing and you only need to set the loading callback when your game start. And editor looks exactly the same as direct reference.
    Also check my article if you want to know how much time you can save by using indirect referencing: 3 Ways to Reduce Load Time in Runtime for Unity
     
    Last edited: Jul 9, 2023
  20. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,113
    Not directly related to the use cases I'm seeing above, but fits the lazy pattern.

    Here's a lazy value pattern for when you have a value that
    a) you want cached,
    b) you want occasionally invalidated, and
    c) you have the means of re-evaluating locally.

    A good example is a bounding box
    Code (csharp):
    1. LazyValue<Bounds> _bbox;
    2. public Bounds Bounds => _bbox.Value; // re-evaluates if dirty
    3.  
    4. void Awake() {
    5.   _bbox = new LazyValue(computeBoundingBox); // instructs on how to re-evaluate (can use lambda if simple)
    6. }
    7.  
    8. void Update() {
    9.   if(something happens to the geometry) {
    10.     _bbox.Invalidate(); // marks as dirty
    11.   }
    12. }
    13.  
    14. // evaluator
    15. Bounds computeBoundingBox() {
    16.   // some work
    17.   return bounds;
    18. }
    Code (csharp):
    1. using System;
    2.  
    3. public interface ILazyValue<T> where T : struct {
    4.  
    5.   Type UnderlyingType { get; }
    6.   T Value { get; }
    7.   bool Valid { get; set; }
    8.   void Update();
    9.  
    10. }
    11.  
    12. /// <summary>
    13. /// This is used in scenarios where a value is evaluated once, then cached for consumers.
    14. /// Typical usage scenario would be actively computing a distance that doesn't change that frequently.
    15. /// The owner's internal logic is supposed to invalidate the object when the surrounding
    16. /// assumptions change (in the above example, if the two end points change), prompting the external
    17. /// updater to re-evaluate the actual value, then cache it again.
    18. /// </summary>
    19. public class LazyValue<T> : ILazyValue<T> where T : struct {
    20.  
    21.   public Type UnderlyingType => typeof(T);
    22.  
    23.   T _value;
    24.  
    25.   public bool Valid { get; set; } = false;
    26.  
    27.   Func<T> _eval;
    28.  
    29.   public LazyValue(Func<T> eval) {
    30.     if(eval is null) throw new ArgumentNullException();
    31.     _eval = eval;
    32.     _value = default;
    33.   }
    34.  
    35.   public T Value {
    36.     get {
    37.       if(!Valid) Update();
    38.       return _value;
    39.     }
    40.   }
    41.  
    42.   public void Invalidate()
    43.     => Valid = false;
    44.  
    45.   public void Update() {
    46.     _value = _eval();
    47.     Valid = true;
    48.   }
    49.  
    50. }

    Nothing really groundbreaking, but reduces clutter in already complicated classes, editors, and such.
     
    sand_lantern likes this.