写点什么

浅谈 C#可变参数 params

作者:yi念之间
  • 2022 年 4 月 29 日
  • 本文字数:4573 字

    阅读完需:约 15 分钟

浅谈C#可变参数params

前言

    前几天在群里看到群友写了一个基础框架,其中设计到关于同一个词语可以添加多个近义词的一个场景。当时群友的设计是类似字典的设计,直接添加 k-v 的操作,本人看到后思考了一下觉得使用 c#中的 params 可以更优雅的实现一个 key 同时添加一个集合的操作,看起来会更优雅一点,这期间还有群友说道 params 和数组有啥区别的问题。本篇文章就来大致的说一下。

示例

params 是 c#的一个关键字,用用汉语来说的话叫可变参数,这里的可变,不是说的类型可变,而是指的个数可变,这是 c#的一个基础关键字,相信大家都有一定的了解,今天咱们就来进一步看一下 c#的可变参数 params。首先来看一下简单的自定义使用,随便定义一个方法

static void ParamtesDemo(string className, params string[] names){    Console.WriteLine($"{className}的学生有:{string.Join(",", names)}");}
复制代码

定义可变参数类型的时候需要有几个注意点

  • params 修饰在参数的前面且参数类型得是一维数组类型

  • params 修饰的参数默认是可以不传递的

  • params 参数不能用 ref 或 out 修饰且不能手动给默认值

调用的时候更简单了,如下所示

ParamtesDemo("小四班", "jordan", "kobe", "james", "curry");// 如果不传递值也不会报错// ParamtesDemo("小四班");
复制代码

由上面的示例可知,使用可变参数最大的优势就是你可以传递一个不确定个数的集合类型并且不用声明单独的类型去包装,这种场景特别适合传递参数不确定的场景,比如我们经常使用到的string.Format就是使用的可变参数类型。

探究本质

通过上面我们了解到的 params 的遍历性,当集合参数个数不确定的时候是使用可变参数的最佳场景,看着很神奇很便捷,本质到底是什么呢?之前楼主也没有在意这个问题,直到前几天怀揣着好奇的心情看了一下。废话不多说,我们直接借助ILSpy工具看一下反编译之后的源码

[CompilerGenerated]internal class Program{	private static void <Main>$(string[] args)	{        //声明了一个数组		ParamtesDemo("小四班", new string[4] { "jordan", "kobe", "james", "curry" });        Console.ReadKey();
//已经没有params关键字了,就是一个数组 static void ParamtesDemo(string className, string[] names) { Console.WriteLine(className + "的学生有:" + string.Join(",", names)); } }}
复制代码

通过ILSpy反编译的源码我们可以看到 params 是一个语法糖,其实就是增加了编程效率,本质在编译的时候会被具体的声明的数组类型替代,不参与到运行时。这个时候如果你怀疑反编译的代码有问题,可以直接通过ILSpy看生成的 IL 代码,由于 IL 代码比较长,首先看一下 Main 方法

// Methods.method private hidebysig static 		void '<Main>$' (			string[] args		) cil managed {	// Method begins at RVA 0x2092	// Header size: 1	// Code size: 57 (0x39)	.maxstack 8	.entrypoint
// ParamtesDemo("小四班", new string[4] { "jordan", "kobe", "james", "curry" }); IL_0000: ldstr "小四班" IL_0005: ldc.i4.4 //通过newarr可知确实是声明了一个数组类型 IL_0006: newarr [System.Runtime]System.String IL_000b: dup IL_000c: ldc.i4.0 IL_000d: ldstr "jordan" IL_0012: stelem.ref IL_0013: dup IL_0014: ldc.i4.1 IL_0015: ldstr "kobe" IL_001a: stelem.ref IL_001b: dup IL_001c: ldc.i4.2 IL_001d: ldstr "james" IL_0022: stelem.ref IL_0023: dup IL_0024: ldc.i4.3 IL_0025: ldstr "curry" IL_002a: stelem.ref // 这个地方调用了ParamtesDemo,第二个参数确实是一个数组类型 IL_002b: call void Program::'<<Main>$>g__ParamtesDemo|0_0'(string, string[]) // Console.ReadKey(); IL_0030: nop IL_0031: call valuetype [System.Console]System.ConsoleKeyInfo [System.Console]System.Console::ReadKey() IL_0036: pop // } IL_0037: nop IL_0038: ret} // end of method Program::'<Main>$'
复制代码

通过上面的 IL 代码可以看到确实是一个语法糖,编译完之后一切尘归尘土归土还是一个数组类型,类型是和 params 修饰的那个数组类型是一致的。接下来我们再来看一下 ParamtesDemo 这个方法的 IL 代码是啥样的

//names也是一个数组.method assembly hidebysig static 	void '<<Main>$>g__ParamtesDemo|0_0' (		string className,		string[] names	) cil managed {	.custom instance void System.Runtime.CompilerServices.NullableContextAttribute::.ctor(uint8) = (		01 00 01 00 00	)	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (		01 00 00 00	)	// Method begins at RVA 0x20d5	// Header size: 1	// Code size: 30 (0x1e)	.maxstack 8
// { IL_0000: nop // Console.WriteLine(className + "的学生有:" + string.Join(",", names)); IL_0001: ldarg.0 IL_0002: ldstr "的学生有:" IL_0007: ldstr "," IL_000c: ldarg.1 IL_000d: call string [System.Runtime]System.String::Join(string, string[]) IL_0012: call string [System.Runtime]System.String::Concat(string, string, string) IL_0017: call void [System.Console]System.Console::WriteLine(string) // } IL_001c: nop IL_001d: ret} // end of method Program::'<<Main>$>g__ParamtesDemo|0_0'
复制代码

一切了然,本质就是那个数组。我们上面还提到了 params 修饰的参数默认不传递的话也不会报错,这究竟是为什么呢,我们就用 IL 代码来看一下究竟进行了何等操作吧

// Methods.method private hidebysig static 	void '<Main>$' (		string[] args	) cil managed {	// Method begins at RVA 0x2092	// Header size: 1	// Code size: 24 (0x18)	.maxstack 8	.entrypoint
// ParamtesDemo("小四班", Array.Empty<string>()); IL_0000: ldstr "小四班" // 本质是编译的时候帮我们声明了一个空数组Array::Empty<string> IL_0005: call !!0[] [System.Runtime]System.Array::Empty<string>() IL_000a: call void Program::'<<Main>$>g__ParamtesDemo|0_0'(string, string[]) // Console.ReadKey(); IL_000f: nop IL_0010: call valuetype [System.Console]System.ConsoleKeyInfo [System.Console]System.Console::ReadKey() IL_0015: pop // } IL_0016: nop IL_0017: ret} // end of method Program::'<Main>$'
复制代码

原来这得感谢编译器,如果默认不传递 params 修饰的参数的话,默认它会帮我们生成一个这个类型的空数组,这里需要注意的不是null,所以代码不会报错,只是没有数据。


扩展知识

我们上面提到了string.Format也是基于 params 实现的,毕竟 Format 具体的参数依赖于前面声明的字符串的占位符个数。在翻看相关代码的时候还发现了一个ParamsArray这个类,用来包装 params 可变参数,简单的来说就是便于快速操作 params,这个我是在 Format 方法中发现的,源代码如下

public static string Format(string format, params object?[] args){    if (args == null)    {        throw new ArgumentNullException((format == null) ? nameof(format) : nameof(args));    }    return FormatHelper(null, format, new ParamsArray(args));}
复制代码

params 参数也可以为 null 值,默认不会报错,但是需要进行判断,否则程序处理 null 可能会报错。在这里我们可以看到把 params 参数传递给 ParamsArray 进行包装,我们可以看一下 ParamsArray 类本身的定义,这个类是一个 struct 类型的

internal readonly struct ParamsArray{    //定义是三个数组分别去承载当传递进来的params不同个数时的数据    private static readonly object?[] s_oneArgArray = new object?[1];    private static readonly object?[] s_twoArgArray = new object?[2];    private static readonly object?[] s_threeArgArray = new object?[3];
//定义三个值分别存储params的第0、1、2个参数的值 private readonly object? _arg0; private readonly object? _arg1; private readonly object? _arg2;
//承载最原始的params值 private readonly object?[] _args;
//params值为1个的时候 public ParamsArray(object? arg0) { _arg0 = arg0; _arg1 = null; _arg2 = null;
_args = s_oneArgArray; }
//params值为2个的时候 public ParamsArray(object? arg0, object? arg1) { _arg0 = arg0; _arg1 = arg1; _arg2 = null;
_args = s_twoArgArray; }
//params值为3个的时候 public ParamsArray(object? arg0, object? arg1, object? arg2) { _arg0 = arg0; _arg1 = arg1; _arg2 = arg2;
_args = s_threeArgArray; }
//直接包装整个params的值 public ParamsArray(object?[] args) { //直接取出来值缓存 int len = args.Length; _arg0 = len > 0 ? args[0] : null; _arg1 = len > 1 ? args[1] : null; _arg2 = len > 2 ? args[2] : null; _args = args; }
public int Length => _args.Length;
public object? this[int index] => index == 0 ? _arg0 : GetAtSlow(index);
//判断是否从承载的缓存中取值 private object? GetAtSlow(int index) { if (index == 1) return _arg1; if (index == 2) return _arg2; return _args[index]; }}
复制代码

ParamsArray 是一个值类型,目的就是为了把 params 参数的值给包装起来提供读相关的操作。根据二八法则来看,params 大部分场景的参数个数或者高频访问可能是存在于数组的前几位元素上,所以使用 ParamsArray 针对热点元素提供了快速访问的方式,略微有一点像 Java 中的 IntegerCache 的设计。这个结构体是 internal 类型的,默认程序集之外是没办法访问的,我当时看到的时候比较好奇,就多看了一眼,感觉设计思路还是考虑的比较周到的。

总结

    本文主要简单的聊一下 c#可变参数 params 的本质,了解到了其实就是一个语法糖,编译完成之后本质还是一个数组。它的好处就是当我们不确定集合个数的时候,可以灵活的使用 params 进行参数传递,不用自行定义一个集合类型。然后微软针对 params 在内部实现了一个 ParamsArray 结构体进行对 params 包装,提升 params 类型的访问。    新年伊始,聊一点个人针对学习的看法。学习最理想的结果就是把接触到的知识进行一定的抽象,转换为概念或者一种思维方式,然后细化这种思维,让它成为细颗粒度的知识点,然后我们通过不断的接触不断的积累,后者不同领域的接触等,不断吸收壮大这个思维库。然后当看到一个新的问题的时候,或者需要思考的时候,能达到快速的多角度的整合这些思维碎片,得到一个更好的思路或解决问题的办法,这也许是一种更行之有效的状态。类比到我们架构设计上来说,以前的思维方式是一种类似单体应用的方式,灵活性差扩展性更差,后来微服务概念大行其道,更多独立的服务相互协调工作,形成一种更强大的聚合力。

发布于: 刚刚阅读数: 2
用户头像

yi念之间

关注

星光不问赶路人,时光不负有心人。 2018.08.22 加入

普通程序员,主攻.net core方向,顺便学习Java和Python。喜欢架构设计,励志成为一名真正的架构师,喜欢研究新技术,喜欢阅读源码。

评论

发布
暂无评论
浅谈C#可变参数params_C#_yi念之间_InfoQ写作社区