Open Eregerog opened 5 years ago
Setting a public AutoComponent
from somewhere else does not sound like a use case you'd want.
Then again, setting a wrapper without additional boilerplate like a property is not possible in plain C#.
What we could do is something like this:
class LazyComponent<T> where T : Component
{
[NotNull] private readonly GameObject _referenceObject;
public LazyComponent(GameObject referenceObject)
{
this._referenceObject = referenceObject;
}
private T _underlying;
public T Value { get {
if (_underlying != null) return _underlying;
_underlying = _referenceObject.GetComponent<T>();
if (_underlying != null) return _underlying;
_underlying = _referenceObject.AddComponent<T>();
return _underlying;
}}
public static implicit operator T(LazyComponent<T> lazyComponent)
{
return lazyComponent.Value;
}
}
What I don't know is how well generics perform with implicit conversions. I know this can work in Scala.
In the worst case we would have to live with the .Value
call.
There is no setter because setting a public Component
field has two use cases:
LazyComponent
imply the second use case? CachedComponent
or similar, would the AddComponent
behaviour be seen as unexpected? LazyCachedComponent
or GreedyCachedComponent
make sense? GetComponent<T>
is already a really fast call. People only cache it's result for a very minor increase in performance, mostly for repeated usage in Update
or similar. Using the LazyComponent
or whatever adds an additional property access, a potential function call (conversion) as well as a custom null check (function call) to a simple pointer dereference. So this whole mechanism might eliminate the main pro of caching a component reference. Benchmarks are required for further evaluation.
One could optimize the code in expense for readability: introducing an additional boolean flag to save further null checks at the cost of one additional byte (or word?) per instance, and force inlining of a new function backing the Value
getter. Or copy-pasting the initialization code. Anyways, benchmarks.
The whole lazyness mechanism requires a reference to the game object the component is on. This might result in a massive penalty for usability when using this, potentially adding more boilerplate code than this is saving:
// does this even work in C# or do we need a constructor specifically for this? -> even worse
public readonly LazyComponent<Renderer> Renderer = new LazyComponent<Renderer>(this);
So far, the only use case for this is to be a little more lazy in a rare case, thereby potentially sacrificing performance. Bjarne Stroustrup would cry when seeing this non-zero-cost abstraction.
We might add to the usefulness of this when providing a more abstract version:
class ComponentWrapper<T> where T : Component
{
[NotNull] private readonly GameObject _referenceObject;
private readonly Func<T> _getter;
private readonly Func<T> _provider;
public ComponentWrapper(GameObject referenceObject, Func<T> getter, Func<T> provider)
{
this._referenceObject = referenceObject;
this._getter = getter;
this._provider = provider;
}
private T _underlying;
public T Value { get {
if (_underlying != null) return _underlying;
_underlying = _getter();
if (_underlying != null) return _underlying;
_underlying = _provider();
return _underlying;
}}
public static implicit operator T(ComponentWrapper<T> wrapper)
{
return wrapper.Value;
}
}
Implementing this could lead to more in-place definitions for use case number 2, by providing a custom inspector for setting the value from the inspector and whatnot. But that is a lot of trouble to save a little bit of boilerplate, and I don't know if that's worth it. Expecially when my fears about the constructor call bonus boilerplate come true.
Leaving this issue open as reference - It might come in handy for a future feature. At least some further discussion is required.
Consider the following code:
It would be nice to have that as a one-liner in the declarartion, something like:
public AutoComponent<Component> MyComp;
It would implicitly convert to and from Component, is Serializable! and does the same as the code above. I'm currently not sure if it is even possible without Reflection.