Passing a generic function as a parameter
Solution 1
This answer doesn't explain the reason why, just how to work around the limitation.
Instead of passing an actual function, you can pass an object that has such a function:
interface IGenericFunc
{
TResult Call<TArg,TResult>(TArg arg);
}
// ... in some class:
void Test(IGenericFunc genericFunc)
{
// for example's sake only:
int x = genericFunc.Call<String, int>("string");
object y = genericFunc.Call<double, object>(2.3);
}
For your specific use case, it can be simplified to:
interface IDeserializerFunc
{
T Call<T>(string arg);
}
// ... in some class:
void Test(IDeserializerFunc deserializer)
{
int x = deserializer.Call<int>("3");
double y = deserializer.Call<double>("3.2");
}
Solution 2
What you're asking to do isn't possible using generics alone. The compiler needs to generate two typed versions of your Transform
function: one to return type A
and one for type B
. The compiler has no way of knowing to generate this at compile time; only by running the code would it know that A and B are required.
One way to solve it would be to pass in the two versions:
private void Execute()
{
GeneralizedFunction("1", "2", i => Transform<A>(i), i => Transform<B>(i));
}
void GeneralizedFunction(string aStringA, string aStringB, Func<string, A> aAction, Func<string, B> bAction)
{
A result1 = aAction(aStringA);
B result2 = bAction(aStringB);
}
The compiler knows exactly what it needs to generate in this case.
Solution 3
Try the following signature:
void GeneralizedFunction<T>(string aStringA, string aStringB, Func<string, T> aAction)
(Note that GeneralizedFunction has to be generic; the compiler will automatically guess the type parameter when calling the method).
Solution 4
It seems the answer is "no".
When you call Transform
directly, you have to specify a type parameter:
int i = Transform<int>("");
So hypothetically, if you could pass an incompletely-constructed generic function like you want to, you'd need to specify the type parameters as well:
void GeneralizedFunction(string aStringA, string aStringB, Func<string, T> aAction)
{
A result1 = aAction<A>(aStringA);
B result2 = aAction<B>(aStringB);
// Do something with A and B here
}
So it seems to me that you could hypothetically do this, if C# had a syntax like that.
But what's the use case? Aside from transforming strings to the default value of an arbitrary type, I don't see much use for this. How could you define a function that would provide a meaningful result in either of two different types using the same series of statements?
EDIT
An analysis of why it's not possible:
When you use a lambda expression in your code, it is compiled into either a delegate or an expression tree; in this case, it's a delegate. You can't have an instance of an "open" generic type; in other words, to create an object from a generic type, all type parameters must be specified. In other words, there's no way to have an instance of a delegate without providing arguments for all of its type parameters.
One of the C# compiler's helpful features is implicit method group conversions, where the name of a method (a "method group") can be implicitly converted to a delegate type representing one of the overloads of that method. Similarly, the compiler implicitly converts a lambda expression to a delegate type. In both cases, the compiler emits code to create an instance of the delegate type (in this case, to pass it to the function). But the instance of that delegate type still needs to have a type argument for each of its type parameters.
To pass the generic function as a generic function, it seems, the compiler would need to be able to pass the method group or the lambda expression to the method without conversion, so the aAction
parameter would somehow have a type of "method group" or "lambda expression." Then, the implicit conversion to a delegate type could happen at the call sites A result1 = aAction<A>(aStringA);
and B result2 = aAction<B>(aStringB);
. Of course, at this point, we are well into the universe of contrafactuals and hypotheticals.
The solution I came up with over lunch was this, assuming a function Deserialize<T>
that takes a string containing serialized data and returns an object of type T
:
void GeneralizedFunction<T>(string aStringA, string aStringB, Func<T, string> stringGetter)
{
A result1 = Deserialize<A>(stringGetter(aStringA));
B result2 = Deserialize<B>(stringGetter(aStringB));
}
void Example(string serializedA, string serializedB, string pathToA, string pathToB, FileInfo a, FileInfo b)
{
GeneralizedFunction(serializedA, serializedB, s => s);
GeneralizedFunction(pathToA, pathToB, File.ReadAllText);
GeneralizedFunction(a, b, fi => File.ReadAllText(fi.FullName));
}
Related videos on Youtube
Max
Updated on June 04, 2022Comments
-
Max almost 2 years
I know that what I'm doing can be done in a different way, but I'm curious about how things work. The following is a simplified code which doesn't compile, but it supposed to show my goal.
private void Execute() { GeneralizedFunction("1", "2", i => Transform(i)); } void GeneralizedFunction(string aStringA, string aStringB, Func<string, T> aAction) { A result1 = aAction(aStringA); B result2 = aAction(aStringB); // Do something with A and B here } T Transform<T>(string aString) { return default(T); }
Transform
is a generic convertion from string to some object (think deserialization).GeneralizedFunction
uses two specializations of transform: one for type A and one for type B. I know I can do this in a number of other ways (say by introducing a parameter for the type of the object), but I'm looking for explanations of whether it is possible or impossible to do this with generics/lambdas. If Transform is specialized before it is passed as a parameter to GeneralizedFunction, then it's impossible. Then the question is why this possibility is restricted. -
Max about 12 yearsI did try it. The problem is you are trying to refer to both types A and B with T here. It was my intention to remove <T> from function declaration.
-
Matthias about 12 yearsThen you'll have to replace both A and B with T.
-
Max about 12 yearsI need to perform specific operations on the objects inside GeneralizedFunction for simplicity. I haven't written any details in my sample code, but everything that is written is necessary.
-
Max about 12 yearsYes I know that I could pass two instances, but I hoped that it is possible to pass a GENERIC function (hence the title of my question) and create two specializations of that generic function inside the GeneralizedFunction.
-
Matthias about 12 yearsI think I now understand what you want: "void GeneralizedFunction(string aStringA, string aStringB, Func<string, A> aAction, Func<string, B> bAction)". However, you will have to pass the method twice.
-
Tim Rogers about 12 yearsI know the code isn't what you want. Your question was why the possibility is restricted. Hopefully you understand now why what you want to do won't work with the compiler.
-
Max about 12 yearsIf I look at the IL code generated for Transform function it seems that there's only one version of it. Even when I apply it to two classes of objects. So it seems that the specialization of a generic function is done at runtime. Isn't it?
-
phoog about 12 years@Max The specialization of the function is done by the just-in-time compiler, so yes, that's done at run time. However, the JIT compiler creates only one specialized version of the function for all reference types. The C# compiler guarantees that this can be done with type safety by virtue of its compile-time type analysis, so run-time type checks are not needed. You're probably aware that you could achieve what you want with reflection, but the solution would be messier than just passing the function twice.
-
Max about 12 years@phoog Does it meant that hypothetically this could be implemented if type constraints are specified (so A and B are guaranteed to be reference types)?
-
phoog about 12 years@Max, hmm, perhaps, but that also means that you have to declare both type parameters, in order to constrain them to be reference types.
-
Max about 12 yearsOne use case is deserialization. The string is a representation of an object and Transform creates an instance of that object from its string representation
-
phoog about 12 years@Max But what's the use case for passing
Transform
toGeneralizedFunction
rather than just calling it directly? Anyway, wouldn't a genericT Transform<T>(string)
just be a convenience method aroundobject Deserialize(type, string)
likeT Transform<T>(string s) { return (T)Deserialize(typeof(T), s); }
-
Max about 12 yearsFor instance Transform1 might convert a string to an object, Transform2 might convert a file which a string points to to an object
-
phoog about 12 years@Max, ah, ok, I understand. Will think on it over lunch :)
-
Max about 12 yearsThe problem is not the implementation. There are many ways to do this. I was just curious about whether it was possible to do it in a particular way in C#. And why it isn't possible.
-
phoog about 12 years@Max Why it isn't possible: see edited answer. I also included my solution from lunch, in case you hand't already thought of that approach.
-
sinelaw almost 5 yearsTechnically, the requested feature is rank-2 parametric polymorphism. C# (and Java, etc.) support only rank-1.