浅谈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类型的访问。
新年伊始,聊一点个人针对学习的看法。学习最理想的结果就是把接触到的知识进行一定的抽象,转换为概念或者一种思维方式,然后细化这种思维,让它成为细颗粒度的知识点,然后我们通过不断的接触不断的积累,后者不同领域的接触等,不断吸收壮大这个思维库。然后当看到一个新的问题的时候,或者需要思考的时候,能达到快速的多角度的整合这些思维碎片,得到一个更好的思路或解决问题的办法,这也许是一种更行之有效的状态。类比到我们架构设计上来说,以前的思维方式是一种类似单体应用的方式,灵活性差扩展性更差,后来微服务概念大行其道,更多独立的服务相互协调工作,形成一种更强大的聚合力。
浅谈C#可变参数params的更多相关文章
- .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 ...
- C#需知--长度可变参数--Params
Params用于参数的数量可变的情况下,即参数的个数是未知数. 使用Params需要知道以下几点: 1.如果函数传递的参数含有多个,使用Params标记的参数数组需要放在最后 图上显示的很明确,不需要 ...
- c#中可变参数params关键字学习
引用 https://www.cnblogs.com/maowp/p/8134342.html 基础知识 1.概念 params 是C#开发语言中关键字, params主要的用处是在给函数传参数的时候 ...
- 方法的可变参数 params
当你写了一个方法,这个方法需要对传进来的参数进行加工,但是不确定传递的参数的数量的时候 比如,public void int jiafa(int a,int b){a+b;} jiafa(1,2) 但 ...
- 浅谈.net中的params关键字
先举个例子: 代码如下: class Program { static void Main(string[] args) { Console.WriteLine(Sum(1)); Console.Wr ...
- c#中可变参数(params关键字的使用)
一.params 是C#开发语言中关键字, params主要的用处是在给函数传参数的时候用,就是当函数的参数不固定的时候. 在方法声明中的 params 关键字之后不允许任何其他参数,并且在方法声明中 ...
- 浅谈jmeter请求参数获取的方式
一.传统的web端请求参数我们在浏览器url栏看到传递的参数是什么,比如百度: 1.我们假如百度有一个这样的地址: https://www.baidu.com/s?wd=jmeter&name ...
- C#可变参数params
using System.Collections; using System.Collections.Generic; using UnityEngine; public class TestPara ...
- 坑爹的 Java 可变参数,把我整得够惨。。
最近在写一个功能点,用了 Java 中的可变参数,真是把我搞得够惨.. 什么是可变参数? 就是方法参数用 Object... args 三个点形式,一个参数可以接收多个参数. 实际的代码就不帖了,来看 ...
随机推荐
- Codeforces 888C: K-Dominant Character(水题)
You are given a string s consisting of lowercase Latin letters. Character c is called k-dominant iff ...
- wiodows /linux CMD
windows : netstat -ano 查看所有端口使用情况 netstat -ano |findstr "端口号" 查看特定端口号 tasklist |f ...
- [数学]高数部分-Part V 多元函数微分学
Part V 多元函数微分学 回到总目录 Part V 多元函数微分学 多元函数微分的极限定义 多元函数微分的连续性 多元函数微分的偏导数 z=f(x, y) 多元函数微分-链式求导规则 多元函数-高 ...
- 使用 JavaScript 的 HTML 页面混合、JavaScript 文件引用和 HTML 代码嵌入 3 种方式在 HTML 页面上打印出“点击我进入到百度首页”的超链接
查看本章节 查看作业目录 需求说明: 使用 JavaScript 的 HTML 页面混合.JavaScript 文件引用和 HTML 代码嵌入 3 种方式在 HTML 页面上打印出"点击我进 ...
- 【SpringBoot】No 'Access-Control-Allow-Origin' header is present on the requested resource.
关键字:跨域,Access-Control-Allow-Origin,转码,解码 在做一个前后端分离项目,本来前端项目都可以正常访问后端接口,跨域是这么设置的,接口可以正常访问 @Configurat ...
- Go语言系列之标准库ioutil
ioutil标准库中提供了一些常用.方便的IO操作函数 一.相关方法 func ReadAll(r io.Reader) ([]byte, error) func ReadDir(dirname st ...
- Sentry 企业级数据安全解决方案 - Relay 操作指南
内容整理自官方文档 本篇回顾了我们在自托管外部使用 Relay 时的操作指南,即在您的硬件上运行的 Relay 并将事件转发到 sentry.io. 系列 Sentry 企业级数据安全解决方案 - R ...
- elasticsearch在linux上的安装,Centos7.X elasticsearch 7.6.2安装
本文环境:Elasticsearch7.6.2目前最先版本 centos7.X JDK1.8 elasticsearch介绍 官网:https://www.elastic.co/cn/pr ...
- gopher协议在SSRF漏洞中的作用
1.什么是gopher协议?2.如何使用gopher协议反弹shell?3.在SSRF中如何使用gopher协议反弹shell? 一.什么是gopher协议?定义:Gopher是Internet上一个 ...
- 一文看懂B端产品和C端产品
大纲 什么是B端产品 什么是C端产品 为什么会产生B端产品和C端产品 怎么判断一个产品是B端还是C端 B端产品和C端产品存在哪些差异 C端产品经理如何向B端产品经理转型 写在最后 什么是B, Bu ...