Search Unity

Question Clean way to use different structs for the same method?

Discussion in 'Scripting' started by karderos, Apr 1, 2023.

  1. karderos

    karderos

    Joined:
    Mar 28, 2023
    Posts:
    376
    Lets say I have 2 similar but different structs:

    Code (CSharp):
    1.  public struct sA
    2.     {
    3.         public int x;
    4.         public int y;
    5.   public int a;
    6. public int b;
    7. public int z;
    8. }
    9.  
    10. public struct sB
    11.     {
    12.         public int x;
    13.         public int y;
    14. }
    Now I want to make a method like this:

    Code (CSharp):
    1.  
    2. public int getX (sA getXfrom){
    3.  
    4. return getXfrom.x;
    5.  
    6. }
    7.  
    Now if I do:

    Code (CSharp):
    1.  
    2. getX(sA);//works
    3. getX(sB);//ofc doesnt work, method only accepts sA
    4.  
    atm my solution is to just change the method to:

    Code (CSharp):
    1.  
    2. public int getX (int getXfrom){
    3.  
    4. return getXfrom;
    5.  
    6. }
    7.  
    8. getX(sA.x);//
    9. getX(sB.x);//
    10.  
    both work now but I just dodged making the method needing to accept both structs.

    Is there anyway to make a method that is agnostic towards the struct in the parameters, as long as it has a variable called "x" in the struct? what I wanted was something like:

    Code (CSharp):
    1.  
    2. public int getX (sA getXfrom || sB getXfrom){
    3.  
    4. return getXfrom.x;
    5.  
    6. }
    7.  
    I know this is dumb syntax, just hope I made clear what I was looking for, thanks.
     
  2. Nefisto

    Nefisto

    Joined:
    Sep 17, 2014
    Posts:
    335
    You need to create a contract that every class or struct that signs it will have to implement a version of it. This is done through interfaces

    Code (CSharp):
    1. public interface MyContract
    2. {
    3.     public int X { get; set; }
    4. }
    5.  
    6. public class SomeClass
    7. {
    8.     public int GetX (MyContract instance)
    9.         => instance.X;
    10. }

    Now u just need to make sure that any of ur structs implements it

    Code (CSharp):
    1. public struct A : MyContract
    2. {
    3.     public int X { get; set;  }
    4. }
    5.  
    6. public struct B : MyContract
    7. {
    8.     public int X { get; set;  }
    9. }
     
    orionsyndrome and karderos like this.
  3. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    You can also define an implicit conversion operator from one struct to another.
    Code (CSharp):
    1. public struct A
    2. {
    3.     public int x;
    4.     public int y;
    5.     public int a;
    6.     public int b;
    7.  
    8.     public A(int x, int y, int a, int b)
    9.     {
    10.         this.x = x;
    11.         this.y = y;
    12.         this.a = a;
    13.         this.b = b;
    14.     }
    15.  
    16.     // Define automatic conversion from A to B
    17.     public static implicit operator B(A a) => new B(a.x, a.y);
    18. }
    19.  
    20. public struct B
    21. {
    22.     public int x;
    23.     public int y;
    24.  
    25.     public B(int x, int y)
    26.     {
    27.         this.x = x;
    28.         this.y = y;
    29.     }
    30. }
    If A defines an implicit conversion operator to B, then you can pass an object of type A to a method that has a parameter of type B and it will be automatically converted and just works.
    Code (CSharp):
    1. void Example()
    2. {
    3.     Debug.Log(GetX(new A(1, 2, 3, 4)));
    4.     Debug.Log(GetX(new B(1, 2)));
    5. }
    6.  
    7. int GetX(B b) => b.x;
     
    Last edited: Apr 1, 2023
    Bunny83, orionsyndrome and karderos like this.
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,005
    Since you asked for a clean solution, the actual answer is: Don't!

    You essentially want your structs to suppport inheritance. However one of the defining properties of structs is that they don't support inheritance. So the whole premise is already wrong.

    While it is true that structs can implement interfaces, this completely bypasses the point of structs in the first place. In order to treat a struct as one of the interfaces it implements, the struct needs to be boxed (as interfaces are always reference types). This would allocate garbage everytime you box such a struct value.

    While a conversion operator is possible, it's not really a clean solution as well. If you do use such an approach, the conversion should only be from a more complex type to a simpler type. The other way round means you're missing data and carry unnecessary fields along that aren't set during the conversion. So if you use a conversion operator, make sure you do it the way @SisusCo showed in his post.

    Though the cleanest solution would be to use classes instead where you can actually use interfaces properly. Of course, as almost always, what approach may be the best depends on the actual requirements.
     
    karderos, Nefisto and SisusCo like this.
  5. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    That's a really good point about boxing! This can be avoided using generics:
    Code (CSharp):
    1. public int GetX<T>(T aOrB) where T : struct, MyContract => aOrB.X;
    But then that's starting be quite a bit of additional complexity, so it can potentially be simpler to just define two methods with different parameters at that point.

    I also forgot to mention that a third option would be to use composition (have A contain a member of type B):
    Code (CSharp):
    1. public struct A
    2. {
    3.     public B xy;
    4.     public int a;
    5.     public int b;
    6.  
    7.     public int x => xy.x;
    8.     public int y => xy.y;
    9.     public A(B xy, int a, int b)
    10.     {
    11.         this.xy = xy;
    12.         this.a = a;
    13.         this.b = b;
    14.     }
    15. }
     
    karderos and Nefisto like this.
  6. karderos

    karderos

    Joined:
    Mar 28, 2023
    Posts:
    376
    thanks the struct i was gonna apply this to is insane with more than 100 fields and I guess it was gonna be a disaster
     
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    4,005
    This is the next issue you have. Structs should not exceed a size of 16 bytes or they will suffer efficiency and speed as they are not longer easy to copy around (and that's essential for structs). So what exactly made you to decide to create a struct here instead of a class?
     
  8. karderos

    karderos

    Joined:
    Mar 28, 2023
    Posts:
    376
    lack of knowledge it seems, i did not know there was a difference except that it passes as value instead of reference

    I also use it overriding gethashcode for keys in a dictionary, dont know if that would be better with a class.
     
  9. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,331
    You can use a record type for a reference type that has value-based equality and GetHashCode implementations.
    Code (CSharp):
    1. public sealed record A
    2. {
    3.     public int x;
    4.     public int y;
    5.     public int a;
    6.     public int b;
    7.  
    8.     public A(int x, int y, int a, int b)
    9.     {
    10.         this.x = x;
    11.         this.y = y;
    12.         this.a = a;
    13.         this.b = b;
    14.     }
    15. }
    The compiler will then generate all the boring stuff for you.
     
    karderos likes this.