委托如何存储对函数的引用?源代码似乎将其称为对象,并且似乎从源代码中删除了它调用该方法的方式。谁能解释C#如何处理这个问题?
似乎我一直在与C#强加给程序员的抽象作斗争。使我烦恼的是函数/方法的混淆。据我了解,所有方法实际上都是分配给类属性的匿名方法。这就是为什么没有函数以数据类型作为前缀的原因。例如...
void foo() { ... }
...将用Javascript编写为...
Function foo = function():void { ... };
以我的经验,匿名函数通常是错误的形式,但是在这里,它在整个语言标准中都很丰富。因为您无法使用其数据类型定义函数(并且显然隐含/处理由编译器承担),所以如果从不声明类型,如何存储对方法的引用?
我正在努力避免使用Delegates
及其变体(Action
和Func
),这都是因为...
从source code for the Delegate.cs
看,它似乎将函数的引用简称为Object
(参见第23-25行)。
如果这些确实是对象,我们如何称呼它们?根据{{1}}路径,它在以下路径上无尾:
delegate.cs
> Delegate.cs:DynamicInvoke()
> DynamicInvokeImpl()
> methodinfo.cs:UnsafeInvoke()
> UnsafeInvokeInternal()
> RuntimeMethodHandle.InvokeMethod()
runtimehandles.cs:InvokeMethod()
如果方法确实是一个对象,这真的不能解释它是如何被调用的。感觉这根本不是代码,并且已从源存储库中删除了实际调用的代码。
感谢您的帮助。
@Amy:在声明之后,我立即举了一个例子来解释我的意思。如果函数以数据类型为前缀,则可以编写一个真正的匿名函数,并将其作为属性存储到对象,例如:
internal extern static object InvokeMethod(object target, object[] arguments, Signature sig, bool constructor);
就目前而言,C#不允许您编写真正的匿名函数,并将该功能围在Delegates
和Lambda expressions
之后。
@ 500内部服务器错误:我已经解释了我想做的事情。我什至大胆了。您认为这里有别有用心的动机;我只是想了解C#如何存储对方法的引用。我什至提供了源代码的链接,以便其他人可以自己阅读代码并帮助回答问题。
@Dialecticus:显然,如果我已经在Google上找到了典型的答案,那么找到我正在寻找的答案的唯一其他地方就是这里。我意识到这是大多数C#开发人员所不了解的,这就是为什么我提供了源代码链接。如果您不知道答案,则不必回复。
答案 0 :(得分:6)
虽然我不完全了解您对“真正的匿名函数”,“没有数据类型前缀”等方面的见解,但我可以向您解释如何用C#编写应用程序。
首先,在C#中没有这样的“功能”。实际上,C#中的每个可执行实体都是一个方法,也就是说,它属于class
。即使您定义了lambda或类似这样的匿名函数:
collection.Where(item => item > 0);
C#编译器在幕后创建编译器生成的类,并将lambda体return item > 0
放入编译器生成的方法。
因此,假设您具有以下代码:
class Example
{
public static void StaticMethod() { }
public void InstanceMethod() { }
public Action Property { get; } = () => { };
}
static class Program
{
static void Main()
{
Example.StaticMethod();
var ex = new Example();
ex.InstanceMethod();
ex.Property();
}
}
C#编译器将从中创建一个IL代码。 IL代码不可立即执行,需要在虚拟机中运行。
IL代码将包含一个具有两个方法的类Example
(实际上是四个-默认构造函数和属性getter方法将自动生成),以及一个 compiler-generation 类,其中包含一种方法,其主体是lambda表达式的主体。
Main
的IL代码如下所示(简化):
call void Example::StaticMethod()
newobj instance void Example::.ctor()
callvirt instance void Example::InstanceMethod()
callvirt instance class [mscorlib]System.Action Example::get_Prop()
callvirt instance void [mscorlib]System.Action::Invoke()
注意那些call
和callvirt
指令:这些是方法调用。
要实际执行被调用的方法,需要将其IL代码编译为机器代码(CPU指令)。这发生在称为.NET Runtime的虚拟机中。它们有几种,例如.NET Framework,.NET Core,Mono等。
.NET运行时包含一个JIT(即时)编译器。它将在程序执行期间将IL代码转换为实际的可执行代码 。
.NET运行时首次遇到IL代码“来自类StaticMethod
的调用方法Example
时,它将首先在已编译方法的内部缓存中查找。当没有匹配项时(这是该方法的第一个调用),运行时会要求JIT编译器使用IL代码创建这种已编译并可以运行的方法。 IL代码将转换为一系列CPU操作并存储在进程的内存中。指向该已编译代码的指针存储在缓存中,以备将来重用。
这一切都将在call
或callvirt
IL指令之后进行(再次简化)。
一旦发生这种情况,运行时就可以执行该方法了。 CPU获取编译后的代码的第一个操作地址作为下一个要执行的操作,然后继续执行直到代码返回。然后,运行时再次接管并继续执行下一条IL指令。
委托的DynamicInvoke
方法执行相同的操作:它指示运行时调用方法(在进行一些其他参数检查后)。您提到的{dead end} RuntimeMethodHandle.InvokeMethod
是对运行时的直接调用。此方法的参数为:
object target
-委托在其上调用实例方法(this
参数)的对象。object[] arguments
-传递给方法的参数。Signature sig
-实际的调用方法,Signature
是一个内部类,用于提供托管IL代码和本机可执行代码之间的连接。bool constructor
-true
(如果这是构造函数调用)。因此,总而言之,方法不会在C#中表示为object
(尽管您当然可以拥有一个delegate
实例,而是是一个object
,但是它不代表 executable 方法,而是为其提供了可调用的引用。
方法由运行时调用,JIT编译器使方法可执行。
您不能在C#中的类之外定义全局“函数”。您可以直接获得指向已编译(加急)方法代码的本机指针,甚至可能通过直接操作自己进程的内存来手动调用它。但是为什么呢?
答案 1 :(得分:3)
您显然误解了脚本语言,C / C ++和C#之间的主要区别。
我想主要的困难是 C#中没有这样的功能。完全没有 C#7引入了新功能“本地函数”,但这不是JS中的函数。 所有代码都是方法。 该名称故意与 function 或 procedure 不同,以强调C#中的所有可执行代码都属于一个类的事实。
匿名方法和lambda只是语法糖。 编译器将在同一类(或嵌套的类)中生成一个真实的方法,带有匿名方法声明的方法属于该类。
此simple article对其进行了说明。您可以举个例子,进行编译,然后亲自检查生成的IL代码。
所以所有方法(无论是否匿名)都属于一个类。除了说它不存储对函数的引用之外,不可能回答更新的问题。在C#中不是这样的。
如何存储对方法的引用?
取决于您指的是什么,可以是
我相信您正在寻找 MethodInfo 选项,它具有MethodInfo.Invoke方法,该方法与JS中的Function..apply函数非常相似。您已经在Delegate源代码中看到了如何使用该类。
如果“引用”是指C样式的函数指针,则它位于RuntimeMethodHandle结构中。在没有扎实了解特定的.Net平台实现和C#编译器如何工作的情况下,切勿使用它。
希望它能使事情澄清一些。
答案 2 :(得分:1)
委托仅是指向具有指定参数和返回类型的方法的指针(要跳转到的内存位置)。与签名(参数和返回类型)匹配的任何方法都可以执行该角色,而与定义的对象无关。匿名仅表示未命名委托。
大多数情况下隐含该类型(如果不是,则将出现编译器错误): C#是一种强类型语言。这意味着每个表达式(包括委托)都必须具有返回类型(包括void)以及强类型参数(如果有)。创建泛型是为了允许在通用环境(例如列表)中使用显式类型。
换句话说,委托是C ++回调的类型安全托管版本。
通过允许代码跳转到适当的处理程序而无需测试任何条件,委托有助于消除switch语句。
委托类似于Java语言中的闭包。
在对Amy的回复中,您试图将一种松散类型的语言(例如JS)和一种强类型语言C#等同起来。在C#中,不可能在任何地方传递任意(松散类型的)函数。 Lambda和委托是保证类型安全的唯一方法。
如果您希望传递函数,我建议尝试F#。
编辑:
如果您试图模仿Javascipt的行为,我将尝试通过接口使用继承。我可以模仿多重继承,并且可以同时输入安全类型。但是,请注意,它不能完全取代Javascript的依赖项注入模型。
答案 3 :(得分:0)
您可能已经发现C#不像JavaScript示例中那样具有函数的概念。
C#是一种静态类型的语言,使用函数指针的唯一方法是使用内置类型(Func,Action)或自定义委托。(我说的是安全的,强类型的指针)
Javascript是一种动态语言,因此您可以按照自己的描述进行操作
如果您愿意放弃类型安全性,则可以使用C#或refection的“动态”功能来实现以下示例中想要的功能(不要这样做,请使用Func / Action)< / strong>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace ConsoleApp1
{
class Program
{
private static Dictionary<string, Func<int, int, int>> FuncOps = new Dictionary<string, Func<int, int, int>>
{
{"add", (a, b) => a + b},
{"subtract", (a, b) => a - b}
};
//There are no anonymous delegates
//private static Dictionary<string, delegate> DelecateOps = new Dictionary<string, delegate>
//{
// {"add", delegate {} }
//};
private static Dictionary<string, dynamic> DynamicOps = new Dictionary<string, dynamic>
{
{"add", new Func<int, int, int>((a, b) => a + b)},
{"subtract", new Func<int, int, int>((a, b) => a - b)},
{"inverse", new Func<int, int>((a) => -a )} //Can't do this with Func
};
private static Dictionary<string, MethodInfo> ReflectionOps = new Dictionary<string, MethodInfo>
{
{"abs", typeof(Math).GetMethods().Single(m => m.Name == "Abs" && m.ReturnParameter.ParameterType == typeof(int))}
};
static void Main(string[] args)
{
Console.WriteLine(FuncOps["add"](3, 2));//5
Console.WriteLine(FuncOps["subtract"](3, 2));//1
Console.WriteLine(DynamicOps["add"](3, 2));//5
Console.WriteLine(DynamicOps["subtract"](3, 2));//1
Console.WriteLine(DynamicOps["inverse"](3));//-3
Console.WriteLine(ReflectionOps["abs"].Invoke(null, new object[] { -1 }));//1
Console.ReadLine();
}
}
}
不应使用的另一个示例
delegate object CustomFunc(params object[] paramaters);
private static Dictionary<string, CustomFunc> CustomParamsOps = new Dictionary<string, CustomFunc>
{
{"add", parameters => (int) parameters[0] + (int) parameters[1]},
{"subtract", parameters => (int) parameters[0] - (int) parameters[1]},
{"inverse", parameters => -((int) parameters[0])}
};
Console.WriteLine(CustomParamsOps["add"](3, 2)); //5
Console.WriteLine(CustomParamsOps["subtract"](3, 2)); //1
Console.WriteLine(CustomParamsOps["inverse"](3)); //-3
答案 4 :(得分:0)
与其他人相比,我将提供一个非常简短的答案。 C#中的所有内容(类,变量,属性,结构等)都具有程序可以挂钩的大量内容。与诸如C ++之类的“更深”的语言相比,这种后端的东西网络稍微降低了C#的速度,但是它也为程序员提供了更多的使用工具,并使该语言更易于使用。在此后端中包含“垃圾回收”之类的功能,该功能可以在没有引用变量的变量时自动从内存中删除对象。说到引用,按引用传递对象的整个系统(在C#中是默认设置)也由后端管理。在C#中,由于该后端中的功能允许进行“反射”操作,因此可以使用“委托”。
来自维基百科:
反射是计算机程序检查, 自省,并在运行时修改其自身的结构和行为。
因此,当C#编译并找到一个Delegate时,它将只是创建一个函数,然后将对该函数的反射引用存储在变量中,从而使您可以将其传递给它并执行各种有趣的工作。不过,您实际上并没有将函数本身存储在变量中,而是存储了一个引用,这有点像一个指向您指向函数在RAM中存储位置的地址。