expresiones - genericos c#
Emular delegados con parámetros genéricos gratuitos en C# (3)
Esta es una pregunta difícil sobre el diseño del lenguaje, los patrones y la semántica. Por favor, no vote solo porque no ve el valor práctico.
Primero, pensemos en las funciones y sus parámetros. Luego veremos las analogías entre funciones con sus parámetros / argumentos y clases / funciones genéricas con sus parámetros de tipo / argumentos de tipo.
Las funciones son bloques de código con algunos valores no especificados llamados " parámetros ". Usted suministra los argumentos y recibe el resultado.
Las clases genéricas son clases con algunos " parámetros de tipo " no especificados. Proporciona los argumentos de tipo y luego puede trabajar con la clase: llamar al constructor o invocar métodos estáticos.
Las funciones genéricas en clases no genéricas son funciones con algunos " parámetros de tipo " no especificados y algunos " parámetros de valor " no especificados. Usted suministra los argumentos de tipo y los argumentos de valor para recibir el resultado.
Los delegados son indicadores de funciones específicas. Cuando crea un delegado, no especifica los argumentos de la función, sino que los suministra más tarde.
El problema es que .Net no tiene equivalente de delegados para funciones genéricas con parámetros de tipo genéricos no especificados. No puede suministrar valores de tipo para los parámetros tipo más adelante. Podemos imaginar delegados que no solo tienen parámetros de valor libre, sino también parámetros de tipo gratuitos.
static class SomeClass {
//generic function
public static T GetValue<T>() {
return default(T);
}
}
//creating delegate to generic function or method group
Func{TFree}<TFree> valueFactory = SomeClass.GetValue;
//creating delegate to anonymous generic function
Func{TFree}<int, List<TFree>> listFactory = {TFree}(int capacity) => new List<TFree>(capacity);
A continuación se muestra el [pseudo] código para un programa que quiero escribir en C #. Quiero saber cómo se puede lograr el comportamiento similar en un programa correcto de C #.
¿Cómo podemos emular delegados con parámetros de tipo genéricos libres en C #?
¿Cómo podemos pasar la referencia / enlace a función (es) genérica (s) con parámetros genéricos aún desconocidos a través del código no genérico?
public static class Factory { //Everything compiles fine here
public delegate ICollection<T> FactoryDelegate<T>(IEnumerable<T> values);
public static ICollection<T> CreateList<T>(IEnumerable<T> values) {
return new List<T>(values);
}
public static ICollection<T> CreateSet<T>(IEnumerable<T> values) {
return new HashSet<T>(values);
}
}
public class Worker { //non-generic class
Func{TFree}<FactoryDelegate<TFree>> _factory; //TFree is a "free" generic type paramenter
public Worker(Func{TFree}<FactoryDelegate<TFree>> factory) {
_factory = factory;
}
public ICollection<T> DoWork<T>(IEnumerable<T> values) { //generic method
return _factory{T}(values); //supplying T as the argument for type parameter TFree
}
}
public static class Program {
public static void Main() {
string[] values1 = new string[] { "a", "b", "c" };
int[] values2 = new int[] { 1, 2, 2, 2 };
Worker listWorker = new Worker(Factory.CreateList); //passing reference to generic function
Worker setWorker = new Worker(Factory.CreateSet); //passing reference to generic function
ICollection<string> result1 = listWorker.DoWork(values1);
ICollection<int> result2 = listWorker.DoWork(values2); //.Count == 4
ICollection<int> result3 = setWorker.DoWork(values2); //.Count == 2
}
}
Vea cómo pasamos las referencias a funciones genéricas (Factory.CreateList y Factory.CreateSet) al constructor de clase Worker sin especificar los argumentos de tipo ? Los argumentos de tipo se proporcionan más adelante cuando se llama a la función genérica DoWork con matrices de tipo concreto. DoWork usa los argumentos de tipo para seleccionar la función correcta, le pasa argumentos de valor y devuelve el valor recibido.
Solución final: Emular delegados con parámetros genéricos gratuitos en C #
Esto realmente no tiene sentido en el sistema de tipos de .Net.
Lo que está describiendo es un constructor de tipos : una "función" que toma uno o más tipos y devuelve un tipo concreto ( parametrizado o cerrado ).
El problema es que los propios constructores de tipos no son tipos. No puede tener un objeto o variable de tipo abierto; los constructores tipo solo se pueden usar para generar tipos concretos.
En otras palabras, no hay forma de representar una referencia a una función abierta dentro del sistema de tipos de .Net.
Lo mejor que puedes hacer es usar la reflexión; un MethodInfo
puede describir un método genérico abierto.
Puede obtener una referencia de tipo seguro en tiempo de compilación a una MethodInfo
abierta escribiendo un método genérico que toma un árbol de expresiones con un parámetro genérico falso:
public MethodInfo GetMethod<TPlaceholder>(Expression<Action> method) {
//Find the MethodInfo and remove all TPlaceholder parameters
}
GetMethod<string>(() => SomeMethod<string>(...));
El parámetro TPlaceholder
es necesario en caso de que quiera hacer referencia a un método genérico abierto con una restricción sobre ese parámetro; puede elegir un tipo de marcador de posición que cumpla con la restricción.
La solución es interfaces. Como @ mike-z escribió, las interfaces admiten métodos genéricos. Entonces, podemos crear la interfaz no genérica IFactory con un método genérico que incapsula una referencia a un método genérico en alguna clase. Para vincular el método genérico de una clase [Factory] utilizando dicha interfaz, generalmente necesitamos crear clases pequeñas que implementen la interfaz de IFactory. Actúan como cierres usados por lambdas.
No veo una gran diferencia semántica entre esto y el método genérico de delegados que pedí. La solución es muy parecida a lo que hace el compilador para lambdas [que solo llama a otros métodos] (crea cierre con el método que llama a).
¿Qué estamos perdiendo? Mayormente azúcar sintáctico.
Funciones anónimas / lambdas. No podemos crear lambdas genéricos. Ser capaz de crear clases anónimas (como en Java) habría resuelto el problema. Pero esto no es un gran problema, ya que las lambdas son solo azúcar sintáctico en .Net.
Posibilidad de crear implícitamente delegar / enlace del grupo de métodos (término C #). No podemos utilizar el grupo de métodos de ninguna manera si es genérico. Esto no afecta la semántica también.
La capacidad de definir delegados genéricos se ve impedida. No podemos crear una
IFactory<U, V>
genérica deIFactory<U, V>
con el métodoV<T> Create<T>(U<T> arg)
. Esto tampoco es un problema.
Este es el código de la solución. La clase Factory
de la pregunta no ha cambiado.
public interface IFactory {
ICollection<T> Create<T>(IEnumerable<T> values);
}
public class Worker { //not generic
IFactory _factory;
public Worker(IFactory factory) {
_factory = factory;
}
public ICollection<T> DoWork<T>(IEnumerable<T> values) { //generic method
return _factory.Create<T>(values);
}
}
public static class Program {
class ListFactory : IFactory {
public ICollection<T> Create<T>(IEnumerable<T> values) {
return Factory.CreateList(values);
}
}
class SetFactory : IFactory {
public ICollection<T> Create<T>(IEnumerable<T> values) {
return Factory.CreateSet(values);
}
}
public static void Main() {
string[] values1 = new string[] { "a", "b", "c" };
int[] values2 = new int[] { 1, 2, 2, 2 };
Worker listWorker = new Worker(new ListFactory());
Worker setWorker = new Worker(new SetFactory());
ICollection<string> result1 = listWorker.DoWork(values1);
ICollection<int> result2 = listWorker.DoWork(values2); //.Count == 4
ICollection<int> result3 = setWorker.DoWork(values2); //.Count == 2
}
}
Creo que la forma de emular esto en el idioma es no usar delegados sino interfaces. Una interfaz no genérica puede contener un método genérico, por lo que puede obtener la mayor parte del comportamiento de los delegados con argumentos de tipo abierto.
Aquí está su ejemplo re-trabajado en un programa válido de C # (tenga en cuenta que aún requiere la clase Factory que definió):
public interface IWorker
{
ICollection<T> DoWork<T>(IEnumerable<T> values);
}
public class ListCreationWorker : IWorker
{
public ICollection<T> DoWork<T>(IEnumerable<T> values)
{
return Factory.CreateList<T>(values);
}
}
public class SetCreationWorker : IWorker
{
public ICollection<T> DoWork<T>(IEnumerable<T> values)
{
return Factory.CreateSet<T>(values);
}
}
public static class Program {
public static void Main(string[] args) {
string[] values1 = new string[] { "a", "b", "c" };
int[] values2 = new int[] { 1, 2, 2, 2 };
IWorker listWorker = new ListCreationWorker();
IWorker setWorker = new SetCreationWorker();
ICollection<string> result1 = listWorker.DoWork(values1);
ICollection<int> result2 = listWorker.DoWork(values2); //.Count == 4
ICollection<int> result3 = setWorker.DoWork(values2); //.Count == 2
}
}
public static class Factory
{
public static ICollection<T> CreateSet<T>(IEnumerable<T> values)
{
return new HashSet<T>(values);
}
public static ICollection<T> CreateList<T>(IEnumerable<T> values)
{
return new List<T>(values);
}
}
Aún se obtiene la importante característica de separar la decisión de qué método llamar desde la ejecución de dicho método.
Sin embargo, una cosa que no puede hacer es almacenar cualquier estado en las implementaciones de IWorker
de forma genérica. No estoy seguro de cómo podría ser útil porque el método DoWork
podría llamarse con diferentes tipos de argumentos cada vez.