C# 中的可变参数方法(VarArgs)
首先需要明确一点:这里提到的可变参数方法,指的是具有 CallingConventions.VarArgs 调用约定的方法,而不是包含 params 参数的方法。可以通过MethodBase.CallingConvention 属性来获取某个方法的调用约定。
举个常见的例子来说,C 语言的 printf
方法大多数人应该都知道,它的作用是向标准输出流(stdout)写入格式化字符串,printf
的方法签名是:
|
方法签名中的 ...
,就表示这个方法是可变参数的,可以根据需要传递任意个数的参数,参数的类型也可以互不相同。
C# 中的 params 参数则具有更强的约束,虽然参数个数可以不固定,但参数的类型必须都是相同的。而实际上,C# 中也可以声明如 C 语言的那种可变参数,只不过大多用于调用非托管 dll 提供的方法,而不是用于托管方法。本文会从 P/Invoke、C# 中可变参数方法的声明、IL 代码和 RuntimeArgumentHandle 四个方面介绍可变参数方法。
一、可变参数方法的 P/Invoke
如果一个非托管 dll 提供了一个可变参数方法,该如何在 C# 中调用它?
最简单的办法显然是按需调用——尽管提供的方法是可变参数的,但我可能并不需要那么多的自由,只需要一种或几种固定的参数就好。这种情况下,方法的签名直接按照需要去写就好,还是以 printf
为例:
1 2 3 4 5 6 7 |
|
需要注意的是,DllImport 需要显式指定 CallingConvention = CallingConvention.Cdecl
,这样会由调用方清理堆栈,才能支持可变参数的方法。
如果的确需要完整的可变参数方法呢?可以使用一些特殊的关键字来做到这一点,这些关键字并未给出官方文档,但确实存在于 C# 编译器中。如下定义printf
方法,注意参数使用的是 __arglist
关键字,并未指定任何参数类型和参数名称:
1 2 |
|
调用方法时,也必须将可变参数用 __arglist()
括起来:
1 2 |
|
这里要区分 __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 |
|
这段示例中,已经把可变参数的用法基本都展示出来了,下面再来简单介绍一下。
首先是构造 ArgIterator
实例,就是通过调用构造函数 new ArgIterator(__arglist)
。
然后是遍历可变参数,ArgIterator.GetRemainingCount 方法能够返回可变参数列表中剩余的参数个数,并且每次调用 ArgIterator.GetNextArg 方法获取下一个参数时都会自动减一。
下一个参数的目标类型可以利用 ArgIterator.GetNextArgType 方法获取,这个方法不会使迭代前进到下一个参数(比较类似于 Peek
方法)。需要注意得到的结果是 RuntimeTypeHandle 结构,需要使用 Type.GetTypeFromHandle 方法才能拿到能用的类型;而且并非是参数的实际类型,仅仅是调用方法时__arglist()
中指定的目标参数类型。例如:
1 2 3 4 5 |
|
得到的参数值的类型是 TypedReference,需要使用 TypedReference.ToObject 静态方法才能得到参数的实际值。需要注意 TypedReference 的另外两个静态方法:GetTargetType 和 TargetTypeToken,它们与 ArgIterator.GetNextArgType 方法一样只能得到调用时的目标参数类型。
关于 TypedReference 还有一些未公布的关键字,但它们并不建议使用,因为一般用不到这些功能,或者有可替代的托管方法。
__makeref
,用于创建 TypedReference 实例:
1 2 |
|
__refvalue
,用于获取或设置 TypedReference 实例的值,要求类型必须与 TypedReference 的目标类型完全相同,而且用法完比较怪异:
1 2 |
|
注意这里仍然是目标类型,并非是值的实际类型:
1 2 3 4 |
|
__reftype
,用于获取 TypedReference 的目标类型,与 TypedReference.GetTargetType 等价:
1 |
|
这里再强调一遍,除了 __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 代码如下所示:
|
简单解释一下,就是按顺序将四个参数(一个固定参数和三个可变参数)推送到堆栈上,最后调用方法。可以看到 __arglist()
的作用就是展开方法参数,并且填充参数类型。注意这里将所有四个参数的类型都写入了 IL,才能正确调用可变参数的方法,这也是为什么特别提供了 ILGenerator.EmitCall 方法来调用可变参数的方法。
public static void printf(string format, __arglist)
方法声明的 IL 代码如下所示:
|
注意这里方法的参数实际上只有一个固定参数 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, __arglist)
声明具有完全相同的效果,而且 RuntimeArgumentHandle 完全可以用在任何地方。
但是这个 printf
方法的调用却是个很大的问题,因为我们无法创建有效的 RuntimeArgumentHandle 结构的实例(它没有含带参数的构造函数),而且__arglist("World", 6, '7')
这样使用也是不可以的(从上面的 IL 代码可以看出,__arglist()
的作用是将参数展开)。
要调用这样的方法,必须再包装一层包含 __arglist
的方法:
1 2 3 |
|
可以认为,方法体中的 __arglist
关键字就是一个隐式创建的 RuntimeArgumentHandle 实例,甚至可以直接 RuntimeArgumentHandle handle = __arglist;
这样使用。
这样做看起来的确是多此一举,但如果要调用包含 RuntimeArgumentHandle 参数的委托,也只有这一种办法了,普通方法更适合继续使用 __arglist
。
作者:CYJB
C# 中的可变参数方法(VarArgs)相关推荐
- Java中的可变参数方法
1.一个小例子: 为了比较直观地说明Java的可变参数方法,我们举个例子: //求若干个整型数中的最大值 public int getMax(int... items){ //定义可变参数itemsi ...
- java 可变参数方法_Java方法中的参数太多,第7部分:可变状态
java 可变参数方法 在我的系列文章的第七篇中,有关解决Java方法或构造函数中过多参数的问题 ,我着眼于使用状态来减少传递参数的需要. 我等到本系列的第七篇文章来解决这个问题的原因之一是,它是我最 ...
- java 变参 使用数组调用_java中的可变参数使用方法
java中的可变参数使用方法 可变参数时Java 1.5新增的方法,可变参数方法接收0个或者多个指定类型的参数,可变参数机制通过先创建一个数组,数组的大小为在调用位置所传递的参数数量,然后将参数值传到 ...
- C,C++中使用可变参数
可变参数即表示参数个数可以变化,可多可少,也表示参数的类型也可以变化,可以是int,double还可以是char*,类,结构体等等.可变参数是实现printf(),sprintf()等函数的关键之处, ...
- 14.1 常见数据结构、List集合(ArrayList、LinkedList)、Set集合(HashSet、LinkedHashSet)、可变参数方法
目录 常见的数据结构 栈 队列 数组 链表 红黑树 二叉树binary tree List集合 List接口中常用方法 列:List接口常用方法练习 ArrayList集合 LinkedList集合 ...
- c语言va_start函数,va_start和va_end,以及c语言中的可变参数原理
FROM:http://www.cnblogs.com/hanyonglu/archive/2011/05/07/2039916.html 本文主要介绍va_start和va_end的使用及原理. 在 ...
- java 可变参数方法不支持多个可变参数以及多种类型的替代方法
记录了 java 可变参数方法不支持多个可变参数以及多种类型的替代方法. java 可变参数方法的实现 关于 java 可变参数方法的实现,阅读了一下两篇博客: java中可变参数方法:http:// ...
- Golang中支持可变参数
Golang中支持可变参数 (如果你希望函数带有可变数量的参数) package main import "fmt" //定义一个函数,函数的参数为:可变参数 ... 参数的数量可 ...
- java 使用反射调用可变参数方法
使用反射操作对象-调用可变参数方法 要把可变参数都当做是其对应的数组类型参数; 如 show(XX... is)作为show(XX[] is)调用; 若可变参数元素类型是引用类型: JDK内部接收到参 ...
最新文章
- 『一本通』差分约束系统
- 密码学基础知识(一)信息安全与密码学
- java线程池_Java多线程并发:线程基本方法+线程池原理+阻塞队列原理技术分享...
- c语言字符密码验证码,c语言下的学生管理系统(含密码加密和验证码).docx
- SAP Spartacus internationalization ( i18n ) 翻译问题的排错指南
- unityui等比例缩放_Unity 4.6-如何针对每种分辨率将GUI元素缩放到合适的大小
- 碎片化学前端,融入到积极上进的环境,我推荐~
- Oracle入门(十四.5)之识别数据类型
- 俱乐部通知[即日起启用微软Live Meeting]
- Scala 隐式转换和隐式参数
- EEPlat的元数据驱动的运行引擎
- 小程序测试关注点之一-登录授权
- ubuntu linux 批量删除文件
- 计算机网络怎么连接两台机器,两台电脑怎么连接局域网,小编教你两台电脑怎么连接局域网...
- 泰禾智能:智能改变未来,成就工业设备行业佼佼者
- php 获取header头信息并显示网址,php 获取远程网址header头信息的方法
- php 各种经典算法
- 编程基础---java Servlet 学习
- python求和函数详解_python的sum求和函数详解
- 云服务器到底是什么?云服务器的优势有哪些