Estoy tratando de automatizar la visualización (recopilación a través de la reflexión) de mis variables que se encuentran en scripts específicos en Unity. El problema es asignar valores personalizados (por ejemplo: "string DisplayName", "bool DisplayMe", "bool WriteMe", etc.). Cuando se trata de mis clases personalizadas, entiendo cómo lo haría, pero me gustaría evitar rehacer tipos como float, string, int, etc. para este propósito.

Por ejemplo, digamos que tengo:

public class myBaseClass
{
    public string Name = "Display Name";
    public bool AmReadable = true;
    public bool AmWritable = true;
}

Entonces:

public class myDoubleFloat: myBaseClass
{
    public float ValueFirst;
    public float ValueSecond;
}

Entonces en algunos scripts en Unity lo defino:

public class SomeScriptOnGameObject : MonoBehaviour
{
    public myDoubleFloat myFirstVariable{get; set;}
    public float mySecondVariable{get; set;}
}

Entonces, más tarde, con la reflexión, puedo verificar si se debe leer "myFirstVariable", su nombre para mostrar, etc., mientras que para "mySecondVariable" no puedo realizar esta verificación. ¿Cómo hago esto sin reinventar la rueda y hacer una clase para cada uno de estos tipos como float, string, int, List, etc.?

3
user7323531 16 oct. 2018 a las 23:04

2 respuestas

La mejor respuesta

Envolver objetos de valor (int, float, etc.) probablemente no sea el mejor enfoque. Además de la complejidad adicional (y la posibilidad de errores), ahora está inflando la huella de memoria de su juego.

(Estoy evitando intencionalmente la sintaxis de C # más nueva en estos ejemplos)

Como ya se encuentra en un contexto de reflexión, en lugar de envolver sus objetos de valor, sugeriría un enfoque basado en atributos. Por ejemplo:

public class SomeScriptOnGameObject
{
    [DisplayName("First Variable"), Writable]
    public float FirstVariable { get; set; }

    [DisplayName("Second Variable")]
    public float SecondVariable { get; set; }

    [DisplayName("Some Field")]
    public float Field;

    public float FieldWithNoAttributes;
}

Esto tiene la ventaja de mantener los metadatos de los campos en los metadatos, en lugar de llevar una copia de todo con cada instancia que cree.

Los atributos reales también son fáciles de crear. Comenzaré con el más simple, WritableAttribute:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class WritableAttribute : Attribute
{
}

Esta clase vacía es todo lo que se necesita para marcar un campo o propiedad como "escribible". El AttributeUsage marca esto como solo válido en campos y propiedades (no, por ejemplo, una clase).

El otro atributo, DisplayName, es solo un poco más complejo:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class DisplayNameAttribute : Attribute
{
    public string DisplayName { get; private set; }

    public DisplayNameAttribute(string displayName)
    {
        DisplayName = displayName;
    }
}

La principal diferencia es el constructor con el argumento displayName y la propiedad DisplayName. Esto obliga al compilador a esperar un argumento para el atributo.

Con algunos métodos de extensión, puede hacer las cosas muy claras:

public static class AttributeExtensions
{
    public static bool IsWritable(this MemberInfo memberInfo)
    {
        return memberInfo.GetCustomAttributes(typeof(WritableAttribute)).Any();
    }

    public static string DisplayName(this MemberInfo memberInfo)
    {
        var displayNameAttribute =
            memberInfo.GetCustomAttributes(typeof(DisplayNameAttribute))
                .FirstOrDefault() as DisplayNameAttribute;
        return displayNameAttribute == null ? null : displayNameAttribute.DisplayName;
    }

    public static PropertyInfo Property<T>(this T _, string propertyName)
    {
        return typeof(T).GetProperty(propertyName);
    }

    public static FieldInfo Field<T>(this T _, string fieldName)
    {
        return typeof(T).GetField(fieldName);
    }
}

(Como mencionó que ya está usando la reflexión, es posible que no necesite los dos últimos métodos allí).

Finalmente, una simple prueba de XUnit para demostrar:

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        var obj = new SomeScriptOnGameObject();

        Assert.True(obj.Property("FirstVariable").IsWritable());
        Assert.False(obj.Property("SecondVariable").IsWritable());
        Assert.False(obj.Field("Field").IsWritable());

        Assert.Equal("First Variable", obj.Property("FirstVariable").DisplayName());
        Assert.Equal("Second Variable", obj.Property("SecondVariable").DisplayName());
        Assert.Equal("Some Field", obj.Field("Field").DisplayName());

        Assert.Null(obj.Field("FieldWithNoAttributes").DisplayName());
    }
}
2
Steve Czetty 17 oct. 2018 a las 15:24

Puede definir un contenedor genérico:

public class MyProperty<T>
{
    private T _value;

    public T Get() => _value;

    public T Set(T newValue) => _value = newValue;

    public string Name { get; set; }

    public bool AmReadable { get; set; }

    public bool AmWritable { get; set; }
}

Y haga que los captadores y definidores de sus propiedades se asignen a algunos campos de respaldo de tipo MyProperty<T>:

public class SomeScriptOnGameObject : MonoBehaviour
{
    private MyProperty<MyDoubleFloat> _myFirstVariable;

    private MyProperty<float> _mySecondVariable;

    public MyDoubleFloat MyFirstVariable
    {
        get => _myFirstVariable.Get();
        set => _myFirstVariable.Set(value);
    }

    public float MySecondVariable
    {
        get => _mySecondVariable.Get();
        set => _mySecondVariable.Set(value);
    }

    public SomeScriptOnGameObject()
    {
        _myFirstVariable = new MyProperty<MyDoubleFloat>
        {
            //configuration
        };

        _mySecondVariable = new MyProperty<float>
        {
            //configuration
        };
    }
}

Si quieres ser elegante, incluso puedes agregar un operador implícito para deshacerte de Get() y hacer que cualquier T sea asignable desde MyProperty<T>:

    public class MyProperty<T>
    {
        private T _value;

        public T Set(T newValue) => _value = newValue;

        public string Name { get; set; }

        public bool AmReadable { get; set; }

        public bool AmWritable { get; set; }

        public static implicit operator T(MyProperty<T> myProperty) => 
            myProperty != null ? myProperty._value : default;
    }

Y:

  public MyDoubleFloat MyFirstVariable
    {
        get => _myFirstVariable;
        set => _myFirstVariable.Set(value);
    }
4
taquion 16 oct. 2018 a las 20:39