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)
        _arg1 = arg1;
        _args = s_twoArgArray;
    //params值为3个的时候
    public ParamsArray(object? arg0, object? arg1, object? arg2)
        _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示例详解的文章就介绍到这了,更多相关C#可变参数params内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C# 运用params修饰符来实现变长参数传递的方法

    可变数目参数的好处就是在某些情况下可以方便地对参数个数不确定情况的实现,例如计算任意数字的加权和,链接任意字符串为一个字符串等.看下例子: 复制代码 代码如下: public class Test2 {     public static void Main()     {         ShowName("小A"); //这里可以指定任意长度的参数也可以传递不同类型的参数,但要改参数类型为object        ShowName("小A", "小B

  • C# params可变参数的使用注意详析

    今天在一个 .NET Core 项目中调用一个自己实现的使用 params 可变参数的方法时触发了 null 引用异常,原以为是方法中没有对参数进行 null 值检查引起的,于是加上 check null 代码: public static void BuildBlogPostLinks(params BlogPostDto[] blogPosts) { if (blogPosts == null) return; foreach (var blogPost in blogPosts) { //

  • c#的params参数使用示例

    复制代码 代码如下: class 参数    {        public void doSome(string str,params int[] values){            if (values != null && values.Length > 0)            {                for (var i = 0; i < values.Length; i++)                {                    C

  • c# 可变数目参数params实例

    一般来说,参数个数都是固定的,定义为集群类型的参数可以实现可变数目参数的目的,但是.NET提供了更灵活的机制来实现可变数目参数,这就是使用params修饰符.可变数目参数的好处就是在某些情况下可以方便地对参数个数不确定情况的实现,例如计算任意数字的加权和,链接任意字符串为一个字符串等.看下例子: 复制代码 代码如下: public class Test2 { public static void Main() { ShowName("小兵"); ShowName("小王&qu

  • C#中的out参数、ref参数和params可变参数用法介绍

    out参数: out关键字 通过引用来传递参数,在定义方法和调用方法的时候都必须使用out关键字 简单来讲out可以用来返回多个参数类型. static void Main(string[] args) { string s = "123"; int result; bool b = MyTest(s,out result); } public static bool MyTest(string s, out int result) { bool isTrue; try { resul

  • C#可变参数params示例详解

    目录 前言 示例 探究本质 扩展知识 总结 前言 前几天在群里看到群友写了一个基础框架,其中设计到关于同一个词语可以添加多个近义词的一个场景.当时群友的设计是类似字典的设计,直接添加k-v的操作,本人看到后思考了一下觉得使用c#中的params可以更优雅的实现一个key同时添加一个集合的操作,看起来会更优雅一点,这期间还有群友说道params和数组有啥区别的问题.本篇文章就来大致的说一下. 示例 params是c#的一个关键字,用用汉语来说的话叫可变参数,这里的可变,不是说的类型可变,而是指的个

  • Go语言中函数可变参数(Variadic Parameter)详解

    目录 基本语法 示例一:函数中获取可变参数 示例二:将切片传给可变参数 示例三:多参数 基本语法 在Python中,在函数参数不确定数量的情况下,可以使用如下方式动态在函数内获取参数,args实质上是一个list,而kwargs是一个dict def myFun(*args, **kwargs): 在Go语言中,也有类似的实现方式,只不过Go中只能实现类似*args的数组方式,而无法实现**kwargs的方式.实现这种方式,其实也是利用数组的三个点表达方式,我们这里来回忆一下. 关于三个点(…)

  • Java数组传递及可变参数操作实例详解

    本文实例讲述了Java数组传递及可变参数操作.分享给大家供大家参考,具体如下: 方法可以操作传递和返回基本数据类型,但是方法中也可用来传递和返回数组.如果要向方法中传递一个数组,则方法的接收参数处必须是符合其类型的数组.而且数组属于引用数据类型,所以在把数组传递进方法之后,如果方法对数组本身做了任何修改,修改结果都是会保存下来的. 向方法中传递数组 在java中,所有对象都是通过引用进行操作的.而数组也是一种对象,当把数组作为参数传递给方法时,传递的实际上就是数组对象的引用.在方法中对数组的所有

  • C语言的可变参数函数实现详解

    目录 1.简介 2.简单的使用方式 总结 1.简介 今天看到一个有趣的东西C语言的可变参数函数 众所周知,C语言的函数不能重载,那么你printf和scanf是怎么可以输入多个参数的 例如查看到的printf的定义为 printf(const char *_Restrict, ...); 这称为可变参数函数.这种函数需要固定数量的强制参数,后面是数量可变的可选参数 这种函数必须至少有一个强制参数.可选参数的类型可以变化.可选参数的数量由强制参数的值决定,或由用来定义可选参数列表的特殊值决定. C

  • Python函数的默认参数设计示例详解

    在Python教程里,针对默认参数,给了一个"重要警告"的例子: def f(a, L=[]): L.append(a) return L print(f(1)) print(f(2)) print(f(3)) 默认值只会执行一次,也没说原因.会打印出结果: [1] [1, 2] [1, 2, 3] 因为学的第一门语言是Ruby,所以感觉有些奇怪. 但肯定的是方法f一定储存了变量L. 准备知识:指针 p指向不可变对象,比如数字.则相当于p指针指向了不同的内存地址. p指向的是可变对象,

  • 如何在spring boot中进行参数校验示例详解

    上文我们讨论了spring-boot如何去获取前端传递过来的参数,那传递过来总不能直接使用,需要对这些参数进行校验,符合程序的要求才会进行下一步的处理,所以本篇文章我们主要讨论spring-boot中如何进行参数校验. lombok使用介绍 在介绍参数校验之前,先来了解一下lombok的使用,因为在接下来的实例中或有不少的对象创建,但是又不想写那么多的getter和setter,所以先介绍一下这个很强大的工具的使用. Lombok 是一个可以通过简单的注解形式来帮助我们简化消除一些必须有但显得很

  • C语言函数基础教程分类自定义参数及调用示例详解

    目录 1.  函数是什么? 2.  C语言中函数的分类 2.1 库函数 2.1.1 为什么要有库函数 2.1.2 什么是库函数 2.1.3 主函数只能是main()吗 2.1.4常见的库函数 2.2 自定义函数 2.2.1自定义函数是什么 2.2.2为什么要有自定义函数 2.2.3函数的组成 2.2.4 举例展示 3. 函数的参数 3.1 实际参数(实参) 3.2  形式参数(形参) 4. 函数的调用 4.1 传值调用 4.2  传址调用 4.3 练习 4.3.1. 写一个函数判断一年是不是闰年

  • Python脚本开发中的命令行参数及传参示例详解

    目录 sys模块 argparse模块 Python中的正则表达式 正则表达式简介 Re模块 常用的匹配规则 sys模块 在使用python开发脚本的时候,作为一个运维工具,或者是其他工具需要在接受用户参数运行时,这里就可以用到命令行传参的方式,可以给使用者一个比较友好的交互体验. python可以使用 sys 模块中的 sys.argv 命令来获取命令行参数,其中返回的参数是一个列表 在实际开发中,我们一般都使用命令行来执行 python 脚本 使用终端执行python文件的命令:python

  • React.memo函数中的参数示例详解

    目录 React.memo?这是个啥? React.memo的第一个参数 父组件 子组件 React.memo优化 React.memo的第二个参数 父组件 子组件 React.memo优化 父组件 子组件 小结 React.memo?这是个啥? 按照官方文档的解释: 如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现.这意味着在这种情况下,React 将跳过渲染组件的操作并直

  • JavaScript代码优化技巧示例详解

    目录 引言 提炼函数 函数参数化 使用策略模式替换“胖”分支 提炼变量 内联变量 封装变量 拆分阶段 拆分循环 拆分变量 分解条件表达式 合并条件表达式 以卫语句取代嵌套条件表达式 将查询函数和修改函数分离 引言 我们先引入一句话: 代码主要是为了写给人看的,而不是写给机器看的,只是顺便也能用机器执行而已. 代码和语言文字一样是为了表达思想.记载信息,所以写得清楚能更有效地表达.本文多数总结自<重构:改善既有代码的设计(第2版)>我们直接进入正题,上代码! 提炼函数 what 将一段代码提炼到

随机推荐