方法的数据类型

时间:2018-10-25 20:44:52

标签: c# function methods types

问题

委托如何存储对函数的引用?源代码似乎将其称为对象,并且似乎从源代码中删除了它调用该方法的方式。谁能解释C#如何处理这个问题?


原始帖子

似乎我一直在与C#强加给程序员的抽象作斗争。使我烦恼的是函数/方法的混淆。据我了解,所有方法实际上都是分配给类属性的匿名方法。这就是为什么没有函数以数据类型作为前缀的原因。例如...

void foo() { ... }

...将用Javascript编写为...

Function foo = function():void { ... };

以我的经验,匿名函数通常是错误的形式,但是在这里,它在整个语言标准中都很丰富。因为您无法使用其数据类型定义函数(并且显然隐含/处理由编译器承担),所以如果从不声明类型,如何存储对方法的引用

我正在努力避免使用Delegates及其变体(ActionFunc),这都是因为...

  1. 这是对实际情况的另一种抽象
  2. 实例化这些类所需的不必要的开销(这些类又携带它们自己的被调用方法的指针)。

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#不允许您编写真正的匿名函数,并将该功能围在DelegatesLambda expressions之后。

@ 500内部服务器错误:我已经解释了我想做的事情。我什至大胆了。您认为这里有别有用心的动机;我只是想了解C#如何存储对方法的引用。我什至提供了源代码的链接,以便其他人可以自己阅读代码并帮助回答问题。

@Dialecticus:显然,如果我已经在Google上找到了典型的答案,那么找到我正在寻找的答案的唯一其他地方就是这里。我意识到这是大多数C#开发人员所不了解的,这就是为什么我提供了源代码链接。如果您不知道答案,则不必回复。

5 个答案:

答案 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()

注意那些callcallvirt指令:这些是方法调用。

要实际执行被调用的方法,需要将其IL代码编译为机器代码(CPU指令)。这发生在称为.NET Runtime的虚拟机中。它们有几种,例如.NET Framework,.NET Core,Mono等。

.NET运行时包含一个JIT(即时)编译器。它将在程序执行期间将IL代码转换为实际的可执行代码

.NET运行时首次遇到IL代码“来自类StaticMethod的调用方法Example时,它将首先在已编译方法的内部缓存中查找。当没有匹配项时(这是该方法的第一个调用),运行时会要求JIT编译器使用IL代码创建这种已编译并可以运行的方法。 IL代码将转换为一系列CPU操作并存储在进程的内存中。指向该已编译代码的指针存储在缓存中,以备将来重用。

这一切都将在callcallvirt 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#中不是这样的

  

如何存储对方法的引用?

取决于您指的是什么,可以是

  1. MethodInfo类的实例,用于引用方法的反射信息,
  2. RuntimeMethodHandle(可通过RuntimeMethodInfo.MethodHandle获取)存储指向JITed方法代码的实存储器指针
  3. 与内存指针完全不同的委托,但在逻辑上可以用于“将方法引用传递给另一个方法”。

我相信您正在寻找 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中存储位置的地址。