Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

246
Views
¿Cómo reemplazo una declaración de cambio sobre una enumeración con un envío genérico basado en tipo dinámico en tiempo de ejecución en C#?

Antecedentes :

Estoy creando una extensión de editor para Unity (aunque esta pregunta no está estrictamente relacionada con la unidad). El usuario puede seleccionar una operación binaria de un menú desplegable y la operación se realiza en las entradas, como se ve en el diagrama:

Operación binaria seleccionada por el usuario

El código se toma de un tutorial y usa una enumeración aquí en combinación con una declaración de cambio aquí para lograr el comportamiento deseado.

La siguiente imagen demuestra la relación entre el código y el comportamiento en la interfaz de usuario del gráfico:

ingrese la descripción de la imagen aquí

Problema

Basado en mi experiencia previa programando en otros lenguajes, y mi deseo de permitir operaciones extensibles por el usuario que no requieran que los usuarios editen una declaración de cambio en el código central, ME GUSTARÍA que el código resultante se vea así (no válido) Código C#:

 ... snip ... // OperatorSelection.GetSelections() is automagically populated by inheritors of the GenericOperation class // So it would represent a collection of types? // so the confusion is primarily around what type this should be public GenericOperations /* ?? */ MathOperations = GenericOperation.GetOperations(); // this gets assigned by the editor when the user clicks // the dropdown, but I'm unclear on what the type should // be since it can be one of several types // from the MathOperations collection public Operation /* ?? */ operation; public override object GetValue(NodePort port) { float a = GetInputValue<float>("a", this.a); float b = GetInputValue<float>("b", this.b); result = 0f; result = operation(a, b); return result; } ... snip ...

Comportamiento de referencia Para ser muy claro sobre el tipo de comportamiento que espero lograr, aquí hay una implementación de referencia en Python.

 class GenericOperation: @classmethod def get_operations(cls): return cls.__subclasses__() class AddOperation(GenericOperation): def __call__(self, a, b): return a + b if __name__ == '__main__': op = AddOperation() res = op(1, 2) print(res) # 3 print(GenericOperation.get_operations()) # {<class '__main__.AddOperation'>}

Preguntas específicas Entonces, en última instancia, esto se reduce a tres preguntas interrelacionadas:

  1. ¿Qué tipo de tipo asigno a MathOperations para que pueda contener una colección de los subtipos de GenericOperation ?

  2. ¿Cómo obtengo los subtipos de GenericOperation ?

  3. ¿Qué tipo de operation asigno, que puede ser de varios tipos?

trabajo hasta ahora

He estado investigando los genéricos y la reflexión de algunas de las siguientes fuentes, pero hasta ahora ninguna parece proporcionar exactamente la información que estoy buscando.

  1. https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/types/generics
  2. https://igoro.com/archive/fun-with-c-generics-down-casting-to-a-generic-type/
  3. Uso de enumeración como parámetro de tipo genérico en C#
  4. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/generics-and-reflection

Editar : edité los comentarios en el psuedocódigo de C# para reflejar que la confusión principal se reduce a cuáles deberían ser los tipos para MathOperations y operation , y para señalar que el editor mismo selecciona la operation de MathOperations cuando el usuario hace clic en el menú desplegable. También cambié la pregunta para que puedan responderse de manera objetiva.

over 4 years ago · Santiago Trujillo
1 answers
Answer question

0

Por lo general, diría que su pregunta es bastante amplia y el caso de uso es muy complicado y requiere muchos pasos no tan triviales para abordar. Pero veo que también te has esforzado bastante en la investigación y en tu pregunta, así que intentaré hacer lo mismo (pequeño regalo de Navidad);)

En general, creo que los genéricos no son lo que quieres usar aquí. Los genéricos siempre requieren parámetros constantes de tiempo de compilación.

Como solo estoy al teléfono y no sé, no puedo brindarle una solución completa en este momento, pero espero poder guiarlo por el camino correcto.


1. Interfaz común o clase base

Creo que lo más simple sería una interfaz común como, por ejemplo,

 public interface ITwoFloatOperation { public float GetResult(float a, float b); }

Por supuesto, también serviría una clase base abstract común. (Incluso podría optar por un determinado atributo en los métodos)

Y luego tener algunas implementaciones como por ejemplo

 public class Add : ITwoFloatOperation { public float GetResult(float a, float b) => a + b; } public class Multiply : ITwoFloatOperation { public float GetResult(float a, float b) => a * b; } public class Power : ITwoFloatOperation { public float GetResult(float a, float b) Mathf.Pow(a, b); } ... etc

2. Encuentra todas las implementaciones usando Reflection

Luego puede usar Reflection (ya estaba en el camino correcto allí) para encontrar automáticamente todas las implementaciones disponibles de esa interfaz como, por ejemplo, esta

 using System.Reflection; using System.Linq; ... var type = typeof(ITwoFloatOperation); var types = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(s => s.GetTypes()) .Where(p => type.IsAssignableFrom(p));

3. Almacenar/Serializar un tipo seleccionado en Unity

Ahora tienes todos los tipos...

Sin embargo, para poder usarlos realmente dentro de Unity, necesitará una clase especial adicional que sea [Serializable] y pueda almacenar un tipo, por ejemplo, como

 [Serializable] // See https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.html public class SerializableType : ISerializationCallbackReceiver { private Type type; [SerializeField] private string typeName; public Type Type => type; public void OnBeforeSerialize() { typeName = type != null ? type.AssemblyQualifiedName : ""; } public void OnAfterDeserialize() { if(!string.NullOrWhiteSpace(typeName)) type = Type.GetType(typeName); } }

4. Selección del tipo de interfaz y dibujo del menú desplegable

Luego, dado que no desea escribir los nombres manualmente, necesitará un cajón especial para el menú desplegable con los tipos dados que implementan su interfaz (verá que estamos conectando los puntos).

Probablemente usaría un atributo como, por ejemplo

 [AttributeUsage(AttributeTarget.Field)] public ImplementsAttribute : PropertyAttribute { public Type baseType; public ImplementsAttribute (Type type) { baseType = type; } }

A continuación, podría exponer el campo como, por ejemplo

 [Implements(typeof (ITwoFloatOperation))] public SerializableType operationType;

y luego tener un cajón personalizado. Esto depende, por supuesto, de sus necesidades. Honestamente, mi conocimiento del editor de secuencias de comandos se basa más en MonoBehaviour, etc., así que solo espero que de alguna manera puedas traducir esto en tu gráfico.

Algo como por ejemplo

 [CustomPropertyDrawer(typeof(ImplementsAttribute))] public class ImplementsDrawer : PropertyDrawer { // Return the underlying type of s serialized property private static Type GetType(SerializedProperty property) { // A little bit hacky we first get the type of the object that has this field var parentType = property.serializedObject.targetObject.GetType(); // And then once again we use reflection to get the field via it's name again var fi = parentType.GetField(property.propertyPath); return fi.FieldType; } private static Type[] FindTypes (Type baseType) { var type = typeof(ITwoFloatOperation); var types = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(s => s.GetTypes()) .Where(p => type.IsAssignableFrom(p)); return types.OrderBy(t => t.AssemblyQualifiedName).ToArray(); } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { label = EditorGUI.BeginProperty(position, label, property); var implements = attribute as ImplementsAttribute; if (GetType(property) != typeof (SerializableType)) { EditorGUI.HelpBox(position, MessageType.Error, "Implements only works for SerializableType!"); return; } var typeNameProperty = property.FindPropertyRelative("typeName"); var options = FindTypes (implements.baseType); var guiOptions = options.Select(o => o.AssemblyQualifiedName).ToArray(); var currentType = string.IsNullOrWhiteSpace(typeNameProperty.stringValue) ? null : Type.GetType(typeNameProperty.stringValue); var currentIndex = options.FindIndex(o => o == curtentType); var newIndex = EditorGUI.Popup(position, label.text, currentIndex, guiOptions); var newTypeName = newIndex >= 0 ? options[newIndex] : ""; property.stringValue = newTypeName; EditorGUI.EndProperty(); } }

5. Usar el tipo para crear una instancia

Una vez que de alguna manera pueda almacenar y obtener el tipo deseado como último paso, queremos usarlo ^^

Nuevamente, la solución sería la reflexión y el Activator , que nos permite crear una instancia de cualquier tipo dinámico dado usando Activator.CreateInstance

así que una vez que tenga el campo, por ejemplo, haría

 var instance = (ITwoFloatOperation) Activator.CreateInstance(operationType.Type)); var result = instance.GetResult(floatA, floatB);

Una vez que todo esto esté configurado y funcione correctamente ( ^^ ), sus "usuarios"/desarrolladores pueden agregar nuevas operaciones tan simples como implementar su interfaz.




Enfoque alternativo: "Comportamientos programables"

Pensándolo más, creo que tengo otro, tal vez un enfoque un poco más simple.

¡Esta opción tal vez no sea lo que estaba buscando originalmente y no es un menú desplegable, sino que simplemente usaremos la ventana emergente de selección de objetos ya existente para los activos!


Podría usar algo que me gusta llamar "Comportamientos programables" y tener un ScriptableObject base como

 public abstract class TwoFloatOperation : ScriptableObject { public abstract float GetResult(float a, float b); }

Y luego implementaciones múltiples (nota: ¡todo esto tiene que estar en archivos diferentes!)

 [CreateAssetMenu (fileName = "Add", menuName = "TwoFloatOperations/Add")] public class Add : TwoFloatOperation { public float GetResult(float a, float b) => a + b; } [CreateAssetMenu (fileName = "Multiply", menuName = "TwoFloatOperations/Multiply")] public class Multiply : TwoFloatOperation { public float GetResult(float a, float b) => a * b; } [CreateAssetMenu (fileName = "Power", menuName = "TwoFloatOperations/Power"] public class Power : TwoFloatOperation { public float GetResult(float a, float b) Mathf.Pow(a, b); }

Luego, crea una instancia de cada vis ProjectView -> Haga clic con el botón derecho -> Create -> TwoFloatOperations

Una vez que hizo esto para cada tipo, simplemente puede exponer un campo de tipo

 public TwoFloatOperation operation;

y deja que Unity haga todo el trabajo de reflexión para encontrar instancias que implementen esto en los recursos.

Simplemente puede hacer clic en el pequeño punto junto al campo del objeto y Unity le mostrará una lista de todas las opciones disponibles e incluso puede usar la barra de búsqueda para encontrar una por nombre.

Ventaja:

  • No se requiere una reflexión sucia, costosa y propensa a errores
  • Básicamente, todo se basa en la funcionalidad ya incorporada del editor -> menos preocupaciones con la serialización, etc.

Desventaja:

  • Esto rompe un poco con el concepto real detrás de ScriptableObject , ya que normalmente habría varias instancias con diferentes configuraciones, no solo una.
  • Como puede ver, sus desarrolladores no solo tienen que heredar un determinado tipo, sino también agregar el atributo CreateAssetMenu y crear una instancia para poder usarlo.

Como dije al escribir esto en el teléfono, pero espero que esto ayude con su caso de uso y le dé una idea de cómo abordaría esto.

over 4 years ago · Santiago Trujillo Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!