Search Unity

Help with creating a Generic method

Discussion in 'Scripting' started by timfrombriz, Mar 30, 2021.

  1. timfrombriz

    timfrombriz

    Joined:
    Jun 23, 2014
    Posts:
    30
    I have an abstract class

    Code (CSharp):
    1. public abstract class Animal : MonoBehaviour
    2. {
    3.     public id;
    4. }
    I declare a variant called;

    Code (CSharp):
    1. public class Dog : Animal
    2. {
    3.  
    4. }
    In my AnimalManager class

    Code (CSharp):
    1. public class AnimalManager : MonoBehaviour
    2. {
    3. List<Animal> animals;
    4.  
    5. public static void Example
    6. {
    7.     Dog dog = GetAnimalByID(id)
    8. }
    9.  

    Is there some way to declare by GetAnimalByID to work by a generic type parameter, where it works internally with Animal but returns the type being requested in the Example method?

    I was thinking something like this, which is obviously invalid code, but this is what my brain tells me:


    Code (CSharp):
    1. public static <T> T GetAnimalByID(int id) where T : Animal
    2. {
    3.     foreach Animal animal in animals
    4. {
    5.     if(animal.id == id) return animal
    6. }
    7. return null;
    8. }
    I dont want to cast the type on return, I would prefer to pass in the abstract or derived type and let the caller method decide the type be it Animal or Dog or another derived class of Animal.

    Cheers for any help on pointing me in the right direction.
     
  2. Antistone

    Antistone

    Joined:
    Feb 22, 2014
    Posts:
    2,836
    What you wrote was almost right.

    Code (CSharp):
    1. public static T GetAnimalByID<T>(int id) where T : Animal
    2. {
    3.     foreach Animal animal in animals
    4.     {
    5.         if(animal.id == id) return animal as T;
    6.     }
    7.     return null;
    8. }
    9.  
    10.  
    11. Dog dog = GetAnimalByID<Dog>(id);
     
    Munchy2007 likes this.
  3. timfrombriz

    timfrombriz

    Joined:
    Jun 23, 2014
    Posts:
    30
    Thanks very much for your reply, appreciated.
     
  4. timfrombriz

    timfrombriz

    Joined:
    Jun 23, 2014
    Posts:
    30
    If I wanted a centralized list of all Animals which were all derived types of base Animal, and had hundred of thousands of derived classes (cats, dogs, birds), is there a better way to store these and hand them out?

    For example, If Im downcasting per frame thousands of animals from the centralized list of animals to a dog or cat, for working with the specific types own unique methods/fields, I read theres a performance penalty in the downcast and given this, Im thinking maybe I should consider an alternative approach. Is this a better way to approach this:

    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. public class Animal
    5. {
    6.     public enum AnimalID
    7.     {
    8.         Dog,
    9.         Cat,
    10.         Bird
    11.     }
    12.  
    13.     public AnimalID animalID;
    14.     public int animalIDListIndex;
    15.     public string title;
    16. }
    17.  
    18. public class Dog
    19. {
    20.     public int uniqueDogVariable;
    21. }
    22.  
    23. public class Cat
    24. {
    25.     public string uniqueCatVariable;
    26. }
    27.  
    28. public class Bird
    29. {
    30.     public float uniqueBirdVariable;
    31. }
    32.  
    33. public class AnimalManager
    34. {
    35.     public List<Animal> animals;
    36.     public List<Dog> dogs;
    37.     public List<Cat> cats;
    38.     public List<Bird> birds;
    39.  
    40.     public void SaveAllData()
    41.     {
    42.         foreach (Animal animal in animals)
    43.         {
    44.             Debug.Log(animal.title);
    45.             Debug.Log(animal.animalID);
    46.             switch (animal.animalID)
    47.             {
    48.                 case Animal.AnimalID.Dog:
    49.                     Debug.Log(dogs[animal.animalIDListIndex].uniqueDogVariable);
    50.                     break;
    51.                 case Animal.AnimalID.Cat:
    52.                     Debug.Log(cats[animal.animalIDListIndex].uniqueCatVariable);
    53.                     break;
    54.                 case Animal.AnimalID.Bird:
    55.                     Debug.Log(birds[animal.animalIDListIndex].uniqueBirdVariable);
    56.                     break;
    57.             }
    58.         }
    59.     }
    60. }
    Also in regards to DOTS and the future, maybe designing this with Structs would be a better approach instead? Feedback?
     
  5. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    A dictionary might be better suited instead of multiple lists for each sub-type of Animal.
    The Dictionary key could be the Animal type, and the value could be the List of animals of said type.

    Example:
    Code (CSharp):
    1. public abstract class Animal { }
    2. public class Dog : Animal { }
    3. public class Cat : Animal { }
    4. public class Bird : Animal { }
    Code (CSharp):
    1. public class AnimalManager {
    2.     private readonly Dictionary<Type, List<Animal>> animalsDict = new Dictionary<Type, List<Animal>>();
    3.  
    4.     public T AddAnimal<T>(T animal) where T : Animal {
    5.         Type type = typeof(T);
    6.  
    7.         if(animalsDict.ContainsKey(type)) {
    8.             animalsDict[type].Add(animal);
    9.         }
    10.         else {
    11.             animalsDict.Add(type, new List<Animal> { animal });
    12.         }
    13.  
    14.         return animal;
    15.     }
    16.  
    17.     public List<T> GetAnimalsOfType<T>() where T : Animal {
    18.         List<T> animals = null;
    19.  
    20.         if(animalsDict.TryGetValue(typeof(T), out List<Animal> animalsList)) {
    21.             animals = animalsList as List<T>;
    22.         }
    23.  
    24.         return animals;
    25.     }
    26.  
    27.     public T GetAnimalOfType<T>() where T : Animal {
    28.         T animal = null;
    29.  
    30.         if(animalsDict.TryGetValue(typeof(T), out List<Animal> animalsList)) {
    31.             animal = animalsList[0] as T;
    32.         }
    33.  
    34.         return animal;
    35.     }
    36.  
    37.     public List<Animal> GetAllAnimals() {
    38.         List<Animal> allAnimals = null;
    39.  
    40.         if(animalsDict.Count > 0) {
    41.             allAnimals = new List<Animal>();
    42.  
    43.             foreach(List<Animal> animals in animalsDict.Values) {
    44.                 allAnimals.AddRange(animals);
    45.             }
    46.         }
    47.  
    48.         return allAnimals;
    49.     }
    50.  
    51.     //Other access methods...
    52. }
    Code (CSharp):
    1. public class SomeOtherClass {
    2.     AnimalManager animalManager = new AnimalManager();
    3.  
    4.     void AddExample() {
    5.         animalManager.AddAnimal(new Dog());
    6.         animalManager.AddAnimal(new Cat());
    7.         animalManager.AddAnimal(new Bird());
    8.     }
    9.  
    10.     void GetExample() {
    11.         Cat firstCat = animalManager.GetAnimalOfType<Cat>();
    12.         List<Bird> allBirds = animalManager.GetAnimalsOfType<Bird>();
    13.         List<Animal> allAnimals = animalManager.GetAllAnimals();
    14.     }
    15. }
    I could be wrong on this (someone correct me if I am), but I believe the performance hit of downcasting only applies when you try to cast an object from another object that has the potential to not be a sub-type of what you're casting it to.
    For example:
    Code (CSharp):
    1. List<object> objects;
    2.  
    3. BoxCollider box = objects[0] as BoxCollider;
    4. string str = objects[1] as string;
    5. Transform[] transforms = objects[2] as Transform[];
    6.  
    7. //The objects in the List could be literally anything: a double, a HashSet, etc., so there needs to be a runtime-check to
    8. //ensure that an object is a sub-type of what you're casting it to, or throw an exception otherwise.
    Whereas the use of generics eliminates the need for the additional runtime check, since it is guaranteed that the cast will be a sub-type of the generic:
    Code (CSharp):
    1. List<Collider> colliders;
    2.  
    3. BoxCollider box = objects[0] as BoxCollider;
    4. string str = objects[1] as string; //This will not even compile, since a string is not a type of Collider.
    5.  
    6. //Because generics allow for compile-type checking of types, there's no need for a run-time check.
    Even if this isn't the case, the performance hit of downcasting should be negligible.

    I'd wait and see if this is even necessary.
    As always, use the profiler to find any performance bottlenecks.
     
    timfrombriz likes this.
  6. grizzly

    grizzly

    Joined:
    Dec 5, 2012
    Posts:
    357
    Pattern matching would be a better way to handle this:
    Code (CSharp):
    1. foreach (Animal animal in animals)
    2. {
    3.     switch (animal)
    4.    {
    5.        case Dog dog:
    6.            Debug.Log(dog.uniqueDogVariable);
    7.            break;
    8.  
    9.        case Cat cat:
    10.            Debug.Log(cat.uniqueCatVariable);
    11.            break;
    12.  
    13.        case Bird bird:
    14.            Debug.Log(bird.uniqueBirdVariable);
    15.            break;
    16.     }
    17. }
    However, encapsulating functionality within derived types is even better:
    Code (CSharp):
    1. public abstract class Animal
    2. {
    3.     public abstract void SaveData();
    4. }
    5.  
    6. public class Dog : Animal
    7. {
    8.     public int uniqueDogVariable;
    9.  
    10.     public override void SaveData()
    11.     {
    12.         Debug.Log(uniqueDogVariable);
    13.     }
    14. }
    Then:
    Code (CSharp):
    1. foreach (Animal animal in animals)
    2. {
    3.     animal.SaveData();
    4. }
    No need for separate lists or enums which may or may not correspond to the correct type, etc.
     
    timfrombriz, Bunny83 and Vryken like this.
  7. timfrombriz

    timfrombriz

    Joined:
    Jun 23, 2014
    Posts:
    30
    Never knew about pattern matching.

    I ran some performance tests on a group of 3 million objects, perform two variable sets , one on a base class field and one on a derived/child class field. Dog/Cat/Bird derived classes

    1. Abstract class with derived classes using Downcasting
    295ms

    2. Isolated classes using index to point to child classes
    404ms

    3. Pattern Matching 'for' loop
    302ms

    4. Pattern Matchin 'foreach' loop
    468ms

    Feel free to examine the code to determine if there is any bias or if there is something Im missing
    https://drive.google.com/file/d/1kRM5PaRNZdP9nKTcEJOrW4rBhuR1Fl8I/view?usp=sharing
     
  8. grizzly

    grizzly

    Joined:
    Dec 5, 2012
    Posts:
    357
    Foreach comes with additional overhead under the hood. In performance critical paths use while (or better still, reverse while) loops over for loops and arrays over lists (at least under .NET/Mono). If you're using a different compiler, results may vary.

    As suggested however, encapsulation is the most performant and least error prone overall.
    Code (CSharp):
    1. var n = REPS; while (n-- > 0)
    2. {          
    3.     animalManagerT3.animalT3s[n].SaveData();
    4. }
    You're actually running 30 million iterations per test in your example, not 3 million, but I've attached a standard result for 10 million iterations on my machine:
     

    Attached Files:

    timfrombriz likes this.
  9. timfrombriz

    timfrombriz

    Joined:
    Jun 23, 2014
    Posts:
    30
    Interesting grizzly. Thanks for the further testing. I redid the tests myself with both a reverse while loop and while loop and noticed the results between these two both seemed to fluctuate on who was the fastest. It did however show the for loop was slower than a while loop overall.

    I must ask what the logic is with a reverse while loop being faster than a forward loop or even a for loop. I would presume as long as the exit condition is precalculated, the compiler would output the same loop code? I did some googling and couldnt find an explanation to this logic.

    In C/C++ an interger evaluates as zero(false)/non zero(true) in a reverse while loop, in C# this evaluation is not available so you still need to be explicit in the condition while (n > 0)[C#] vs while (n) [C/C++]. I presume this optimization would help in C/C++ land but not C# land. Even changing (n > 0 ) to (n != 0) didnt change results.

    Code (CSharp):
    1.  
    2.             j = REPS;
    3.             while (j-- != 0)
    4.             {
    5.                 switch (animalManagerT1.animalT1s[j])
    6.                 {
    7.                     case DogT1 dogT1:
    8.                     {
    9.                         dogT1.commonValue = j;
    10.                         dogT1.uniqueDogField = j;
    11.                         break;
    12.                     }
    13.                     case CatT1 catT1:
    14.                     {
    15.                         catT1.commonValue = j;
    16.                         catT1.uniqueCatField = j;
    17.                         break;
    18.                     }
    19.                     case BirdT1 birdT1:
    20.                     {
    21.                         birdT1.commonValue = j;
    22.                         birdT1.uniqueBirdField = go;
    23.                         break;
    24.                     }
    25.                 }
    26.             }

    Code (CSharp):
    1. j = 0;
    2.             while (++j < REPS)
    3.             {
    4.                 switch (animalManagerT1.animalT1s[j])
    5.                 {
    6.                     case DogT1 dogT1:
    7.                     {
    8.                         dogT1.commonValue = j;
    9.                         dogT1.uniqueDogField = j;
    10.                         break;
    11.                     }
    12.                     case CatT1 catT1:
    13.                     {
    14.                         catT1.commonValue = j;
    15.                         catT1.uniqueCatField = j;
    16.                         break;
    17.                     }
    18.                     case BirdT1 birdT1:
    19.                     {
    20.                         birdT1.commonValue = j;
    21.                         birdT1.uniqueBirdField = go;
    22.                         break;
    23.                     }
    24.                 }
    25.             }
    I am still confused how a precalculated exit for loop is slower than a while loop. And the margin is consistent and significant (avg 5%).
     
    Last edited: Apr 9, 2021
  10. timfrombriz

    timfrombriz

    Joined:
    Jun 23, 2014
    Posts:
    30
    Is it correct to say encapsulation could not be applied to the given example of uniqueFields (each with a different type and field name)?
     
  11. grizzly

    grizzly

    Joined:
    Dec 5, 2012
    Posts:
    357
    Our implementations differ. Incrementing in a while condition will result in a non-zero based index (1>n for post, 1>n-1 for pre), so I'd written mine within the body as might be expected in production when iterating over arrays:
    Code (CSharp):
    1.  var i = 0; while (i < 10)
    2. {
    3.     array[i] = i;
    4.     i++;
    5. }
    Which makes it slightly slower than the reverse while-loop.
    If we inspect the IL code, we see a difference in assembly:
    Code (CSharp):
    1. // forward for
    2. for (var i = 0; i < 10; i++)
    3. {
    4.     n=i;          
    5. }
    6.  
    7. // forward while
    8. var i = 0; while (i < 10)
    9. {
    10.     n=i;
    11.     i++;
    12. }
    13.  
    14. IL_000f: br.s IL_0015
    15.  
    16. IL_0011: ldloc.0
    17. IL_0012: ldc.i4.1
    18. IL_0013: add
    19. IL_0014: stloc.0
    20. IL_0015: ldloc.0
    21. IL_0016: ldc.i4.s 10
    22. IL_0018: blt.s IL_0011
    Code (CSharp):
    1. // reverse while
    2. var i = 10; while (i --> 0)
    3. {
    4.     n=i;
    5. }
    6.  
    7. IL_001d: ldloc.1
    8. IL_001e: dup
    9. IL_001f: ldc.i4.1
    10. IL_0020: sub
    11. IL_0021: stloc.1
    12. IL_0022: ldc.i4.0
    13. IL_0023: bgt.s IL_001d
    The reverse while-loop's taking a more optimal path, omitting one branch and replacing load with duplicate.
    Given that the IL code for both forward-loops are identical, I'm not quite sure why there's a difference TBH, but I do see the same consistent margin.
    If encapsulated and implemented as shown I see no reason why not, but ultimately I don't know what your end goal is, so it really depends on your requirements.