问题 将泛型函数作为参数传递


我知道我正在做的事情可以用不同的方式完成,但我对事情的运作方式很好奇。以下是一个不编译的简化代码,但它应该显示我的目标。

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 是从字符串到某个对象的通用转换(想想反序列化)。 GeneralizedFunction 使用两个转换特化:一个用于类型A,一个用于类型B.我知道我可以通过许多其他方式执行此操作(例如通过引入对象类型的参数),但我正在寻找解释是否有可能或不可能使用泛型/ lambdas。如果Transform在作为参数传递给GeneralizedFunction之前是专用的,那么它是不可能的。那么问题是为什么这种可能性受到限制。


4276
2018-02-23 11:48


起源

你想要做的是什么?既然你不想给GeneralizedFunction任何关于Transform函数的类型信息,为什么不再接受一个func再次获取一个字符串并返回一个对象(其中每个人都知道每个人都是*) - Polity
问题是,你的“做A和B的事情”占位符隐藏了有问题的部分。 A和B总是特定的类型吗?那你就不需要泛型了。它们是任意的(可能有约束)类型吗?然后 GeneralizedFunction 需要在它们中是通用的。 - AakashM
A和B是具体类型,但Transform是一个通用函数。 - Max


答案:


您要求做的事情不可能单独使用仿制药。编译器需要生成两个类型版本的 Transform 功能:一个返回类型 A 和一个类型 B。编译器无法知道在编译时生成它;只有运行代码才能知道A和B是必需的。

解决它的一种方法是传递两个版本:

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);
}

在这种情况下,编译器确切地知道它需要生成什么。


4
2018-02-23 12:09



是的我知道我可以传递两个实例,但我希望可以传递一个GENERIC函数(因此我的问题的标题)并在GeneralizedFunction中创建该泛型函数的两个特化。 - Max
我知道代码不是你想要的。你的问题是为什么可能性受到限制。希望您现在明白为什么您想要做的事情不适用于编译器。 - Tim Rogers
如果我查看为Transform函数生成的IL代码,它似乎只有一个版本。即使我将它应用于两类对象。所以似乎泛型函数的特化是在运行时完成的。不是吗? - Max
IL不是编译代码。这是中间的。 - Tim Rogers
@Max函数的特化是由即时编译器完成的,所以是的,这是在运行时完成的。但是,JIT编译器仅创建 一 专业版的功能 所有 参考类型。 C#编译器保证可以通过它来实现类型安全 编译时间 类型分析,因此不需要运行时类型检查。你可能已经意识到你可以通过反射实现你想要的东西,但解决方案比仅仅传递函数两次更麻烦。 - phoog


这个答案没有解释原因 为什么,只是 怎么样 解决限制。

您可以传递具有此类功能的对象,而不是传递实际函数:

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);
}

对于您的特定用例,可以简化为:

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");
}

4
2018-06-20 20:30





请尝试以下签名:

void GeneralizedFunction<T>(string aStringA, string aStringB, Func<string, T> aAction)

(注意,GeneralizedFunction必须是通用的;编译器会在调用方法时自动猜测类型参数)。


1
2018-02-23 11:57



我确实尝试过。问题是你试图在这里用T引用A和B两种类型。我打算从函数声明中删除<T>。 - Max
然后你必须用T替换A和B. - Matthias
为简单起见,我需要对GeneralizedFunction内的对象执行特定操作。我没有在我的示例代码中写任何细节,但所有编写的内容都是必要的。 - Max
我想我现在明白你想要的:“void GeneralizedFunction(string aStringA,string aStringB,Func <string,A> aAction,Func <string,B> bAction)”。但是,您必须两次传递该方法。 - Matthias


似乎答案是“不”。

你打电话时 Transform 直接,你必须指定一个类型参数:

int i = Transform<int>("");

所以假设,如果你可以像你想要的那样传递一个不完整构造的泛型函数,你还需要指定类型参数:

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
}

所以在我看来,如果C#有这样的语法,你可以假设这样做。

但是用例是什么?除了将字符串转换为任意类型的默认值之外,我认为没有太多用处。你怎么能定义一个函数,它可以使用同一系列的语句在两种不同类型中的任何一种中提供有意义的结果?

编辑

分析为什么不可能:

在代码中使用lambda表达式时,它将被编译为委托或表达式树;在这种情况下,它是一个代表。您不能拥有“开放”泛型类型的实例;换句话说,要从泛型类型创建对象,必须指定所有类型参数。换句话说,如果没有为其所有类型参数提供参数,就无法拥有委托实例。

C#编译器的一个有用功能是隐式方法组转换,其中方法的名称(“方法组”)可以隐式转换为表示该方法的一个重载的委托类型。类似地,编译器隐式地将lambda表达式转换为委托类型。在这两种情况下,编译器都会发出代码来创建委托类型的实例(在这种情况下,将其传递给函数)。但是该委托类型的实例仍然需要为每个类型参数都有一个类型参数。

传递泛型函数 作为通用功能看起来,编译器需要能够通过 方法组 要么 lambda表达式 方法 没有转换, 所以 aAction 参数会以某种方式具有“方法组”或“lambda表达式”的类型。然后,隐式转换为委托类型可能发生在呼叫站点 A result1 = aAction<A>(aStringA); 和 B result2 = aAction<B>(aStringB);。当然,在这一点上,我们很好地融入了反事实和假设的世界。

假设有一个功能,我在午餐时提出的解决方案就是这个 Deserialize<T> 采用包含序列化数据的字符串并返回类型的对象 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));
}

1
2018-02-23 17:05



一个用例是反序列化。字符串是对象的表示,Transform从其字符串表示形式创建该对象的实例 - Max
@Max但是传递的用例是什么 Transform 至 GeneralizedFunction 而不是直接调用它?无论如何,不​​会是通用的 T Transform<T>(string) 只是一个方便的方法 object Deserialize(type, string) 喜欢 T Transform<T>(string s) { return (T)Deserialize(typeof(T), s); } - phoog
例如,Transform1可能会将字符串转换为对象,Transform2可能会将字符串指向的文件转换为对象 - Max
@Max啊,好的,我明白了。午餐会想到它:) - phoog
问题不在于实施。有很多方法可以做到这一点。我只是好奇是否有可能在C#中以特定的方式做到这一点。为什么不可能呢。 - Max


void GeneralizedFunction<T>(string aStringA, string aStringB, Func<string, T> aAction)
{
    A result1 = aAction(aStringA);
    B result2 = aAction(aStringB);
}

T Transform<T>(string aString)
{
    return default(T);
}

0
2018-02-23 11:59