C# 中的可变参数方法(VarArgs)

首先需要明确一点:这里提到的可变参数方法,指的是具有 CallingConventions.VarArgs 调用约定的方法,而不是包含 params 参数的方法。可以通过MethodBase.CallingConvention 属性来获取某个方法的调用约定。

举个常见的例子来说,C 语言的 printf 方法大多数人应该都知道,它的作用是向标准输出流(stdout)写入格式化字符串,printf 的方法签名是:

int printf(const char * format, ...);

方法签名中的 ...,就表示这个方法是可变参数的,可以根据需要传递任意个数的参数,参数的类型也可以互不相同。

C# 中的 params 参数则具有更强的约束,虽然参数个数可以不固定,但参数的类型必须都是相同的。而实际上,C# 中也可以声明如 C 语言的那种可变参数,只不过大多用于调用非托管 dll 提供的方法,而不是用于托管方法。本文会从 P/Invoke、C# 中可变参数方法的声明、IL 代码和 RuntimeArgumentHandle 四个方面介绍可变参数方法。

一、可变参数方法的 P/Invoke

如果一个非托管 dll 提供了一个可变参数方法,该如何在 C# 中调用它?

最简单的办法显然是按需调用——尽管提供的方法是可变参数的,但我可能并不需要那么多的自由,只需要一种或几种固定的参数就好。这种情况下,方法的签名直接按照需要去写就好,还是以 printf 为例:

1

2

3

4

5

6

7

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]

public static extern int printf(string format, string text);

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]

public static extern int printf(string format, int num, int x, int y);

// 调用方法

printf("Hello %s!\n""World"); // Hello World!

printf("Hello %d! is %d x %d\n", 42, 6, 7); // Hello 42! is 6 x 7

需要注意的是,DllImport 需要显式指定 CallingConvention = CallingConvention.Cdecl,这样会由调用方清理堆栈,才能支持可变参数的方法。

如果的确需要完整的可变参数方法呢?可以使用一些特殊的关键字来做到这一点,这些关键字并未给出官方文档,但确实存在于 C# 编译器中。如下定义printf 方法,注意参数使用的是 __arglist 关键字,并未指定任何参数类型和参数名称:

1

2

[DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]

public static extern int printf(string format, __arglist);

调用方法时,也必须将可变参数用 __arglist() 括起来:

1

2

printf("Hello %s!\n", __arglist("World")); // Hello World!

printf("Hello %s! is %d x %c\n", __arglist("World", 6, '7')); // Hello World! is 6 x 7

这里要区分 __arglist 和 __arglist()__arglist 是用于可变参数方法的声明和方法体内引用可变参数的,而 __arglist() 是用与可变参数方法的调用的。注意第二个示例,三个参数的类型各不相同,分别是字符串、整数和字符。

可变参数方法在调用时,也要特别注意即使不传递任何可选参数,也必须写 __arglist(),而不能省略掉。例如,上面的 printf 方法,即使没有参数,也要这样写才可以:printf("Hello World!", __arglist());

二、C# 中的可变参数方法

上面说到了非托管 dll 能够提供可变参数方法,C# 也能调用这样的方法,那么 C# 自身是否能声明这样的方法?

答案其实很明显,既然 __arglist 关键字能在 P/Invoke 方法中使用,显然也能在普通的方法中使用。只不过这时需要使用 ArgIterator 结构和TypedReference 结构来访问参数,而不是普通的参数访问方法。

先来看一段简单的示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

public static void printf(string format, __arglist) {

    Console.Write(format);

    ArgIterator args = new ArgIterator(__arglist);

    while (args.GetRemainingCount() > 0) {

        Console.WriteLine("{0}: {1}", Type.GetTypeFromHandle(args.GetNextArgType()),

            TypedReference.ToObject(args.GetNextArg()));

    }

}

printf("Hello %s! is %d x %c\n", __arglist("World", 6, '7'));

// Hello %s! is %d x %c

// System.String: World

// System.Int32: 6

// System.Char: 7

这段示例中,已经把可变参数的用法基本都展示出来了,下面再来简单介绍一下。

首先是构造 ArgIterator 实例,就是通过调用构造函数 new ArgIterator(__arglist)

然后是遍历可变参数,ArgIterator.GetRemainingCount 方法能够返回可变参数列表中剩余的参数个数,并且每次调用 ArgIterator.GetNextArg 方法获取下一个参数时都会自动减一。

下一个参数的目标类型可以利用 ArgIterator.GetNextArgType 方法获取,这个方法不会使迭代前进到下一个参数(比较类似于 Peek 方法)。需要注意得到的结果是 RuntimeTypeHandle 结构,需要使用 Type.GetTypeFromHandle 方法才能拿到能用的类型;而且并非是参数的实际类型,仅仅是调用方法时__arglist() 中指定的目标参数类型。例如:

1

2

3

4

5

printf("Hello %s! is %d x %c\n", __arglist((IEnumerable<char>)"World", (object)6, (IComparable<char>)'7'));

// Hello %s! is %d x %c

// System.Collections.Generic.IEnumerable`1[System.Char]: World

// System.Object: 6

// System.IComparable`1[System.Char]: 7

得到的参数值的类型是 TypedReference,需要使用 TypedReference.ToObject 静态方法才能得到参数的实际值。需要注意 TypedReference 的另外两个静态方法:GetTargetType 和 TargetTypeToken,它们与 ArgIterator.GetNextArgType 方法一样只能得到调用时的目标参数类型。

关于 TypedReference 还有一些未公布的关键字,但它们并不建议使用,因为一般用不到这些功能,或者有可替代的托管方法。

__makeref,用于创建 TypedReference 实例:

1

2

string str = "any value";

TypedReference typeRef = __makeref(str);

__refvalue,用于获取或设置 TypedReference 实例的值,要求类型必须与 TypedReference 的目标类型完全相同,而且用法完比较怪异:

1

2

__refvalue(typeRef, string) = "other value";

Console.WriteLine(__refvalue(typeRef, string));

注意这里仍然是目标类型,并非是值的实际类型:

1

2

3

4

object str = "any value";

TypedReference typeRef = __makeref(str);

__refvalue(typeRef, object) = "other value";

Console.WriteLine(__refvalue(typeRef, object));

__reftype,用于获取 TypedReference 的目标类型,与 TypedReference.GetTargetType 等价:

1

Console.WriteLine(__reftype(typeRef));

这里再强调一遍,除了 __arglist 关键字之外,其它关键字不建议使用。Visual Studio 2013 的语法检查可以识别 __arglist 关键字,其它关键字会提示语法错误(但能够编译通过)。

C# 中的可变参数方法具有以下特点:

  • 可变参数方法是不符合 CLS 的。
  • 接口可以声明可变参数方法,可变参数方法也可以是 virtual 方法,并能够由子类重写。
  • 通过反射获取的参数个数,只会包含固定参数(__arglist 之前的参数)。因为 __arglist 仅仅代表方法的调用约定,并不是实际的参数。
  • 可变参数方法可以包含 0 个固定参数,即声明类似 void MyMethod(__arglist) 的方法。
  • __arglist 不能用在委托中。

三、可变参数方法的 IL 代码

上面从 C# 语言的角度介绍了可变参数方法,最后来剖析一下它的 IL 原理。

可变参数方法的调用,同样是使用 call 指令和 callvirt 指令,但需要明确指定参数类型。例如printf("Hello %s! is %d x %c\n", __arglist("World", 6, '7')); 对应的 IL 代码如下所示:

IL_0000: ldstr "Hello %s! is %d x %c\n"

IL_0005: ldstr "World"

IL_000A: ldc.i4.6

IL_000B: ldc.i4.s 55

IL_000D: call void Cyjb.TestProgram::printf(string, string, int32, char)

简单解释一下,就是按顺序将四个参数(一个固定参数和三个可变参数)推送到堆栈上,最后调用方法。可以看到 __arglist() 的作用就是展开方法参数,并且填充参数类型。注意这里将所有四个参数的类型都写入了 IL,才能正确调用可变参数的方法,这也是为什么特别提供了 ILGenerator.EmitCall 方法来调用可变参数的方法。

public static void printf(string format, __arglist) 方法声明的 IL 代码如下所示:

.method public hidebysig static vararg void printf (string format) cil managed

注意这里方法的参数实际上只有一个固定参数 format,只不过在方法的签名部分多了一个 vararg,表示方法是可变参数的,与反射得到的结果相同。

方法体中倒没有什么特殊的地方,同样是调用 ArgIterator 和 TypedReference 的相关方法,不过用到了 arglist 指令来为 ArgIterator 构造函数 提供参数,该指令就是由 __arglist 关键字而来的,其作用是返回指向可变参数列表的非托管指针。

上面提到的 __makeref__refvalue 和 __reftype 关键字,则分别对应于 mkrefany、refanyval 和 refanytype 指令,这里不再详述。

四、RuntimeArgumentHandle

前面说到,委托中是不能使用 __arglist 关键字的,那么如果为可变参数方法创建委托呢?如果注意看 ArgIterator 的构造函数,可以发现它的参数是一个RuntimeArgumentHandle 结构,这个结构中包含一个指向可变参数的参数列表的指针。

因此,完全可以使用 RuntimeArgumentHandle 来代替方法声明中的 __arglist 关键字,如下所示:

1

2

3

4

public static void printf(string format, RuntimeArgumentHandle handle) {

    ArgIterator args = new ArgIterator(handle);

    // 其它代码

}

与 public static void printf(string format, __arglist) 声明具有完全相同的效果,而且 RuntimeArgumentHandle 完全可以用在任何地方。

但是这个 printf 方法的调用却是个很大的问题,因为我们无法创建有效的 RuntimeArgumentHandle 结构的实例(它没有含带参数的构造函数),而且__arglist("World", 6, '7') 这样使用也是不可以的(从上面的 IL 代码可以看出,__arglist() 的作用是将参数展开)。

要调用这样的方法,必须再包装一层包含 __arglist 的方法:

1

2

3

public static void Wrap(string format, __arglist) {

    printf(format, __arglist);

}

可以认为,方法体中的 __arglist 关键字就是一个隐式创建的 RuntimeArgumentHandle 实例,甚至可以直接 RuntimeArgumentHandle handle = __arglist;这样使用。

这样做看起来的确是多此一举,但如果要调用包含 RuntimeArgumentHandle 参数的委托,也只有这一种办法了,普通方法更适合继续使用 __arglist

作者:CYJB 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438319.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring Boot Actuator监控关闭

可以使用如下属性:management.endpoints.enabled-by-defaultfalse * 在YAML中有特殊的含义&#xff0c;所以如果想使用include或者exclude包含所有的端点时要加上引号&#xff0c;如下示例&#xff1a; # 暴露监控端点 management:endpoints:enabled-by-default: false #关闭…

Javascript获取类名方法

函数&#xff1a; entity.getClassName function(obj) { if (obj && obj.constructor && obj.constructor.toString()) { if(obj.constructor.name) { return obj.constructor.name; } let str…

mysql把一个数据库中的数据复制到另一个数据库中的表 2个表结构相同

1。表结构相同的表&#xff0c;且在同一数据库&#xff08;如&#xff0c;table1,table2) Sql &#xff1a;insert into table1 select * from table2 (完全复制) insert into table1 select distinct * from table2(不复制重复纪录&#xff09; insert into table1 select …

!Spring Aop中四个重要概念,切点,切面,连接点,通知

一、基本概念 1. 通知&#xff1a; 就是我们编写的希望Aop时额外执行的那个方法。我们通过Aop希望我们编写的方法在目标方法执行前执行&#xff0c;或者执行后执行。 2. 切点&#xff1a;切点就是我们配置的满足我们条件的目标方法。比如我们规定&#xff1a;名字前面是select…

Spring AOP(通知、连接点、切点、切面)

一、AOP术语 通知&#xff08;Advice&#xff09;   切面的工作被称为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作&#xff0c;通知还解决了何时执行这个工作的问题。 5种通知类型&#xff1a;前置通知&#xff08;Before&#xff09;&#xff1a;在…

Map集合根据key,value排序

/*** 根据map的key排序* * param map 待排序的map* param isDesc 是否降序&#xff0c;true&#xff1a;降序&#xff0c;false&#xff1a;升序* return 排序好的map*/public static <K extends Comparable<? super K>, V> Map<K, V> sortByKey(Map<K, …

C#中几种常用的集合的用法

集合:将一推数据类型相同的数据放入到一个容器内&#xff0c;该容器就是数组&#xff1a;内存中开辟的一连串空间。 非泛型集合 ArrayList集合&#xff1a; ArrayList是基于数组实现的&#xff0c;是一个动态数组&#xff0c;其容量能自动 增长 ArrayList的命名空间System.…

oracle新增,删除字段,设置默认值

增加字段 alter table 表 Add 字段 varchar2(128); 设置字段默认值为时间戳 alter table 表 modify 字段 default sysdata; 删除字段 alter table 表 drop column 字段;

C#使用Redis的基本操作

一&#xff0c;引入dll 1.ServiceStack.Common.dll 2.ServiceStack.Interfaces.dll 3.ServiceStack.Redis.dll 4.ServiceStack.Text.dll 二&#xff0c;修改配置文件 在你的配置文件中加入如下的代码&#xff1a; <appSettings><add key"RedisPath" value…

Navicat将mysql表结构导成oracle表结构

1&#xff0c;选中对应的表右键逆向表到模型 2.点击右上角文件转换模型为 3.模型选择物理&#xff0c;数据库oracle&#xff0c;选择对应的版本 4.新弹出的模型点击右上角文件&#xff0c;导出sql 5.选择路径导出sql

程序员们的三高:高并发、高性能、高可用

你们知道淘宝&#xff0c;京东这些购物商场吗&#xff1f;他们到了双11&#xff0c;双12为什么能支持全国14亿人口同时购物下单呢&#xff0c;因为他们的程序做到了高并发、高性能、高可用。那么你对程序员的三高了解多少呢&#xff1f; 高并发 一. 高并发 高并发是现在互联…

C#接口归纳总结实例详解

本篇文章通过实例代码对接口做了详解&#xff0c;需要的朋友可以参考下 C#接口的学习&#xff0c;在编程中&#xff0c;我们经常会用到接口&#xff0c;那什么是接口呢&#xff1f; 接口描述的是可属于任何类或结构的一组相关功能&#xff0c;所以实现接口的类或结构必须实现…

char 和 varchar 的区别,数据库索引B+树

char 和 varchar 的区别 char(n) &#xff1a;固定长度&#xff0c;效率高&#xff1b;缺点&#xff1a;占用空间&#xff1b;存储固定长度的&#xff0c;使用 char 非常合适。 varchar(n) &#xff1a;可变长度&#xff0c;存储的值是每个值占用的字节再加上一个用来记录其长…

日常收集C#接口知识(知识全面)

本文分为七章节给大家介绍c#接口知识&#xff0c;内容比较详细&#xff0c;特此分享脚本之家平台&#xff0c;供大家参考 第一节 接口慨述 接口&#xff08;interface&#xff09;用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。有了这个协定&#xff…

mysql按照中文首字母拼音排序

order by convert(排序字段 using gbk)

C#基础操作符详解

本节内容&#xff1a; 1.操作符概览&#xff1b; 2.操作符的本质&#xff1b; 3.操作符与运算顺序 4.操作符详解。 1.操作符概览&#xff1a; 操作符&#xff08;Operator&#xff09;也译为”运算符” 操作符是用来操作数据的&#xff0c;被操作符操作的数据称为操作数&a…

查询sql语句是否使用索引

使用 Explain 命令来查看语句的执行计划 例&#xff1a;Explain select * from table 除过select&#xff0c;其他比如insert&#xff0c;update和delete均可以使用explain查看执行计划 概要描述&#xff1a; id:选择标识符 select_type:表示查询的类型。 table:输出结果集的…

C# 有什么惊艳到你的地方?

作者&#xff1a;皮皮关 链接&#xff1a;https://www.zhihu.com/question/335137780/answer/786853293 来源&#xff1a;知乎 著作权归作者所有。商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处。 很多游戏开发者都是由于Unity而“被迫”使用C#的。但用过一段…

Redis使用场景

缓存&#xff1a;减轻查询压力&#xff0c;提升系统性能&#xff1b; 排行榜&#xff1a;利用 Redis 的 SortSet&#xff08;有序集合&#xff09;实现&#xff1b; 计数器/限速器&#xff1a;利用 Redis 中原子性的自增操作&#xff0c;我们可以统计类似用户点赞数、用户访问…

SqlServer学习之存储过程

前言&#xff1a;对于存储过程一直有一种抵触的心理&#xff0c;因为毕业至今所在的公司开发组都不是很规范&#xff0c;对于开发的一些注意事项并没有很多的规定&#xff0c;只是在知乎上查找相关知识的时候&#xff0c;看到很多人对于在程序里使用存储过程的不好之处都有很多…