前言

前几天在群里看到群友写了一个基础框架,其中设计到关于同一个词语可以添加多个近义词的一个场景。当时群友的设计是类似字典的设计,直接添加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类型的访问。

    新年伊始,聊一点个人针对学习的看法。学习最理想的结果就是把接触到的知识进行一定的抽象,转换为概念或者一种思维方式,然后细化这种思维,让它成为细颗粒度的知识点,然后我们通过不断的接触不断的积累,后者不同领域的接触等,不断吸收壮大这个思维库。然后当看到一个新的问题的时候,或者需要思考的时候,能达到快速的多角度的整合这些思维碎片,得到一个更好的思路或解决问题的办法,这也许是一种更行之有效的状态。类比到我们架构设计上来说,以前的思维方式是一种类似单体应用的方式,灵活性差扩展性更差,后来微服务概念大行其道,更多独立的服务相互协调工作,形成一种更强大的聚合力。

欢迎扫码关注我的公众号

浅谈C#可变参数params的更多相关文章

  1. .NET框架- in ,out, ref , paras使用的代码总结 C#中in,out,ref的作用 C#需知--长度可变参数--Params C#中的 具名参数 和 可选参数 DEMO

    C#.net 提供的4个关键字,in,out,ref,paras开发中会经常用到,那么它们如何使用呢? 又有什么区别? 1 in in只用在委托和接口中: 例子: 1 2 3 4 5 6 7 8 9 ...

  2. C#需知--长度可变参数--Params

    Params用于参数的数量可变的情况下,即参数的个数是未知数. 使用Params需要知道以下几点: 1.如果函数传递的参数含有多个,使用Params标记的参数数组需要放在最后 图上显示的很明确,不需要 ...

  3. c#中可变参数params关键字学习

    引用 https://www.cnblogs.com/maowp/p/8134342.html 基础知识 1.概念 params 是C#开发语言中关键字, params主要的用处是在给函数传参数的时候 ...

  4. 方法的可变参数 params

    当你写了一个方法,这个方法需要对传进来的参数进行加工,但是不确定传递的参数的数量的时候 比如,public void int jiafa(int a,int b){a+b;} jiafa(1,2) 但 ...

  5. 浅谈.net中的params关键字

    先举个例子: 代码如下: class Program { static void Main(string[] args) { Console.WriteLine(Sum(1)); Console.Wr ...

  6. c#中可变参数(params关键字的使用)

    一.params 是C#开发语言中关键字, params主要的用处是在给函数传参数的时候用,就是当函数的参数不固定的时候. 在方法声明中的 params 关键字之后不允许任何其他参数,并且在方法声明中 ...

  7. 浅谈jmeter请求参数获取的方式

    一.传统的web端请求参数我们在浏览器url栏看到传递的参数是什么,比如百度: 1.我们假如百度有一个这样的地址: https://www.baidu.com/s?wd=jmeter&name ...

  8. C#可变参数params

    using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestPara ...

  9. 坑爹的 Java 可变参数,把我整得够惨。。

    最近在写一个功能点,用了 Java 中的可变参数,真是把我搞得够惨.. 什么是可变参数? 就是方法参数用 Object... args 三个点形式,一个参数可以接收多个参数. 实际的代码就不帖了,来看 ...

随机推荐

  1. 第十五个知识点:RSA-OAEP和ECIES的密钥生成,加密和解密

    第十五个知识点:RSA-OAEP和ECIES的密钥生成,加密和解密 1.RSA-OAEP RSA-OAEP是RSA加密方案和OAEP填充方案的同时使用.现实世界中它们同时使用.(这里介绍的只是&quo ...

  2. CS5210完全替代AG6202|HDMI转VGA不带音频输出的芯片+原理图|替代兼容AG6202

    CS5210完全替代AG6202|HDMI转VGA不带音频输出的芯片+原理图|替代兼容AG6202 安格AG6202是一个HDMI转VGA不带音频解决方案,用于实现HDMI1.4高分辨率视频转VGA转 ...

  3. SpringBoot集成Actuator监控管理

    1.说明 本文详细介绍Spring Boot集成Actuator监控管理的方法, 基于已经创建好的Spring Boot工程, 然后引入Actuator依赖, 介绍监控管理相关功能的使用. Sprin ...

  4. frontend-maven-plugin插件问题解决

    1.插件介绍 frontend-maven-plugin为项目本地下载/安装Node和NPM,运行npm install命令 . 它适用于Windows,OS X和Linux. 这个插件也可以下载No ...

  5. Swoole 中使用 Table 内存表实现进程间共享数据

    背景 在多进程模式下进程之间的内存是相互隔离的,在一个工作进程中的全局变量和超全局变量,在另一个工作进程中是无法读取和操作的. 如果只有一个工作进程,则不存在进程隔离问题,可以使用全局变量和超全局变量 ...

  6. Spring 处理请求和响应相关的注解

    @Controller 默认返回 templates 目录下的 string.html 页面内容. 在方法中加上 @ResponseBody 注解,可以返回JSON.XML或自定义mediaType的 ...

  7. 手写RPC-简陋版

    前言 最近不小心被隔离,放假思考一番,决定开始在手写序列.这个序列在之前看Nacous和网关源码的时候就有想法,只是一直没落实下来,趁着隔离行动起来. 必备知识介绍 序列化与反序列化 序列化是把对象的 ...

  8. web自动化,下拉滚动到底部/顶部和下拉滚动到指定的元素

    在web自动化,经常会遇到页面显示内容太多的时候,页面就会出现滚动条,一般有两种方式进行下拉,一种是直接下拉到底部/顶部/中部,或者直接给定元素,直接下拉到指定元素的位置. 两种方式的共同点: 两种方 ...

  9. Python_获取全部异常信息

    import traceback try: os.getcwd('exc') except Exception: exc = traceback.format_exc() print(exc)

  10. spring cloud --- Zuul --- 心得

    spring boot      1.5.9.RELEASE spring cloud    Dalston.SR1 1.前言 什么是 Zuul? Zuul是微服务网关,与Gateway类似 ,根据请 ...