I have encountered something interesting and wanted to share.
If a struct type and a class type implements an interface, (ie. IDamagable)
and I define two variables of that interface type, (ie. IDamagable target).
both a struct and a class object can be assigned to those variables.
if I try to call a common function that is implemented in both the struct and the class through associated variables, the method is called in both variables of that interface, however,
the method under struct cant alter its variables
while method under class works as expected.
I am trying to figure why, and I tried to call the method directly on the struct (not its variable of the interface) and it works as expected.
Please find the code below;
using UnityEngine;
public interface IDamagable
{
public void Damage();
}
public struct DamagableStruct : IDamagable
{
public int hitpoint;
public void Damage()
{
Debug.Log("Damage() called in struct");
hitpoint--;
}
}
public class DamagableObject : IDamagable
{
public int hitpoint;
public void Damage()
{
Debug.Log("Damage() called in class");
hitpoint--;
}
}
public class testScript : MonoBehaviour
{
IDamagable structTarget;
IDamagable classTarget;
DamagableObject bigBird;
DamagableStruct littleBird;
// Start is called before the first frame update
void Start()
{
littleBird = new DamagableStruct {hitpoint=99 };
bigBird = new DamagableObject {hitpoint=9999 };
structTarget = littleBird;
classTarget = bigBird;
}
// Update is called once per frame
void Update()
{
structTarget.Damage(); // try changing with littleBird.Damage();
classTarget.Damage();
Debug.Log($"struct hitpoints {littleBird.hitpoint}");
Debug.Log($"class hitpoints {bigBird.hitpoint}");
}
}
The above code displays class hitpoints decreasing while struct hitpoints remaining unchanged.
Structs are value types, and always copied. Interfaces doesn’t matter here.
You created littleBird, assigned COPY of littleBird to structTarget, called method on structTarget, and expecting littleBird hitpoints to change, but they are different pieces of data in memory, when with class you have two references to the same piece of memory.
If you cast structTarget back into original type and check its variables, you will see that it was changed as expected.
Note that it’s generally not advised to assign a struct to an interface variable. This will box the struct and create garbage because interfaces always represent reference types. As a more general guideline, since structs are value types they should be treated as readonly when possible. So mutable structs are generally not recommended. Of course there are always excpetions to the rule. Vector3 for example has its 3 fields public and it’s quite common to modify the component values manually. Though this also leads to common pitfalls like that you can not effectivly change the components of a property of a Value type. That’s why
transform.position.x = 5;
does not work. position is a property and this line will only invoke the getter. So you get a copy of the position. The code will actually change the x component of that copy, but that copy is lost immediately afterwards. You can only change the position when the setter of the property is called.
So I would highly recommend to not try to use structs for polymorphism. It has just downsides and no benefits.
Note that Unity’s DOTS framework is a different story, but that’s because most of the magic is done in unsafe or native code. The interfaces are just used as a “marker” in that framework. Though that’s a completely different topic.
I would have to disagree this, since here, in this example, the variable of a particular interface was assigned with a new struct instead of its reference.
Indeed, Unity DOTS is creating confusion regarding how structs should be used.
He isn’t talking about your example, he is just giving an advice.
When you create struct in function, and it doesn’t stored anywhere, then it will be allocated on stack, and erased immidiately after function return, it’s fast and clean.
When you write ISomeInterface val = new SomeStruct(), it will be copied on heap, like usual class, which isn’t slow during execution, but you generate garbage, and when your game’s available RAM is low enough, garbage collector will be called and player will encounter small freeze.
And that’s not all. Afaik if you have struct as class field, it’s stored with rest of your class memory, so when you change it, you simply rewrite small piece of memory, but when you have class or struct with interface, it will be allocated somewhere in heap, not directly in your class memory, and creating new instance won’t just rewrite the memory, you will only assign new pointer, but class itself again will stay as garbage.
That’s why you better use pure structs without interfaces for some temporary calculations and short-living data, and long-living usually reusable classes with interfaces and other kind of stuff.
What he means is that when you have a member or variable with an interface type, it always stores a reference type, much like a member/variable of the “object” type. When you assign a value type to it, its copied into a “box”, and the reference to this box is stored instead. This is called “boxing” and is a source of garbage allocation because boxes, like classes, are allocated on the managed heap memory and must be garbage collected after you’re done with them.
Don’t confuse reference types with “ref” variables, which allow you to pass around a true reference to a value without any allocation/copying.
Implementing interfaces on structs can be done without boxing, but only when using generics with struct constraints. This is how ECS and the job system do it (it’s the only way to get some of the flexibility from C++ templates into C# generics). Still, this doesn’t allow for storing plain interfaces in fields, it’s more about creating smarter generic functions.