Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Interfaces and generics

Discussion in 'Scripting' started by Flynn_Prime, May 2, 2023.

  1. Flynn_Prime

    Flynn_Prime

    Joined:
    Apr 26, 2017
    Posts:
    383
    Good evening. I'm hoping for some advice from the experts. I can't seem to get this working:

    Code (CSharp):
    1. public abstract class Stats<TEnum> : SerializedScriptableObject where TEnum : Enum
    2. {
    3.     public Dictionary<TEnum, float> stats = new();
    4. }
    Code (CSharp):
    1. public interface IDamageable<T> where T : Stats<Enum>
    2. {
    3.     T _Stats { get; }
    4.  
    5.     void Damage(float amount);
    6. }
    Code (CSharp):
    1. public class Player : MonoBehaviour, IDamageable<CharacterStats>
    2. {
    3.     private CharacterStats stats;
    4.  
    5.     public CharacterStats _Stats
    6.     {
    7.         get => stats;
    8.     }
    9. }
    I get the error CS0311: The type 'CharacterStats' cannot be used as a type parameter 'T' in the generic type or method 'IDamageable<T>'. There is no implicit reference conversion from 'CharacterStats' to 'Stats<System.Enum>'

    Any help would be much appreciated as alway! Thanks
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,842
    What does the declaration of CharacterStats look like?
     
  3. Flynn_Prime

    Flynn_Prime

    Joined:
    Apr 26, 2017
    Posts:
    383
    At the moment:
    Code (CSharp):
    1. using System.Collections;
    2. using UnityEngine;
    3. using System;
    4.  
    5. public enum CharacterStat
    6. {
    7.     MovementSpeed,
    8.     MaxHealth,
    9.     Attack,
    10.     Defense,
    11.     Evasion
    12. };
    13.  
    14. [CreateAssetMenu(menuName = "Stats/CharacterStats")]
    15. public class CharacterStats : Stats<CharacterStat>
    16. {  
    17. }
    I have literally just managed to get something working, although I'm not sure if it's likely to break or cause issues further down the line:

    Code (CSharp):
    1. public interface IDamageable<TEnum, TStats> where TStats : Stats<TEnum> where TEnum : Enum
    2. {
    3.     TStats _Stats { get; }
    4.  
    5.     void Damage(int amount);
    6. }
    Code (CSharp):
    1. public class Player : MonoBehaviour, IDamageable<CharacterStat, CharacterStats>
    No errors and seems to function as I'd like for now, but my classes are still pretty bare bones.
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,524
    That's exactly what I would have suggested :) There's no way around specifying both types because you can not use the System.Enum base type as direct replacement in your Dictionary. While your "IDamageable" interface could be made into a covariant interface by using the out keyword on the generic argument, this does not work for the dictionary since a dictionary is a collection and allows reading and writing values. An interface / type can only support either covariance or contravariance but not both.

    Though even covariance would not help here since covariance can only return a less specific type. In your case your actual type uses the type Enum as key in the dictionary and then you use your interface with a more specific type. So this does not work. The other way round would work with a covariant interface. But of course that wouldn't make much sense in your case. By introducing a second argument you can actually specify the exact types at each level.

    However I also see potential problems with thie approach. The point of an interface is usually to generalize the access to an otherwise abstract object. A common misconception when it comes to generics is that people often see generics as something similar to inheritance / polymorphism. However generics are essentially the opposite of polymorphism. Generics do not generalize different objects to one common denominator but actually split the same functionality into separate classes / types.

    Inheritance is about using the same data and allow extension in derived classes. Polymorphism (virtual / abstract methods) allow to treat objects the same way but they can exchange, override their functionality / methods. However as a generalized object all those objects are still the same base class.

    Generics on the other hand provide the exact same functionality but allow that same code to work on different data / types. When you actually make a concrete type of a generic type (by providing types in the generic arguments) you actually create completely separate and incompatible types. The best example is a generic List. The
    List<T>
    class provides a fix functionality and allows you to use that same functionality with different types. It should be clear that a
    List<string>
    and a
    List<int>
    are two completely separate types which are completely incompatible with each other. So generics will makes types diverge and incompatible, though they allow to reuse the same code. As I briefly mentioned covariance and contra-variance are the two exceptions when it comes to generics. Though those are quite rare cases and can only be used when the data flow is one-directional.

    I've written several post about co and contra variance. Here's one of them.

    So you should ask yourself what this interface is good for and how / where you want to use it.
     
    spiney199 likes this.
  5. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    971
    Code (csharp):
    1. public class Player : MonoBehaviour, IDamageable<CharacterStats>
    This is a trap.

    Don't define your player explicitly as a class which implements the systems you think a player should have. Instead, define the player logically as the game object that has all the systems of a player attached to it.

    So instead of
    Code (csharp):
    1. public class Player : MonoBehaviour, IDamageable, ITargetable, IMoveable, IAttack, IDefend, IEvade // etc ...
    Just create the systems you will need and attach them to a single game object
    Code (csharp):
    1. public class TypicalDamageSystem : IDamageable { // ... }
    2. public class InputController : IInput { // ... }
    3. public class MyAgilitySystem : IDamageable, IEvade { // ... }
    4. public class MyDefenseSystem : IDamageable, IDefend { // ... }
    5. // ...
     
  6. Flynn_Prime

    Flynn_Prime

    Joined:
    Apr 26, 2017
    Posts:
    383
    Appreciate this, and I definitely do plan on modularising these components. This Player class is purely for testing at the moment. Only thing currently in there is the IDamageable implementations, so it will probably just end up becoming the DamageSystem class.

    Thanks for such a solid write up. Makes alot more sense after your explanations. I'll read through your link when I get home tonight.