...

淺談 C# 可變參數 params

2022-02-12

前言


群裡(lǐ)看到群友寫了一個基礎框架,其中涉及到關于同一個詞語可以添加多個近義詞的一個場景。當時(shí)群友的設計是類似字典的設計,直接添加k-v的操作,本人看到後(hòu)思考了一下覺得使用c#中的params可以更優雅的實現一個key同時(shí)添加一個集合的操作,看起(qǐ)來會(huì)更優雅一點,這(zhè)期間還(hái)有群友說(shuō)道(dào)params和數組有啥區别的問題。本篇文章就(jiù)來大緻的說(shuō)一下。


示例


params是c#的一個關鍵字,用用漢語來說(shuō)的話叫(jiào)可變參數,這(zhè)裡(lǐ)的可變,不是說(shuō)的類型可變,而是指的個數可變,這(zhè)是c#的一個基礎關鍵字,相信大家都(dōu)有一定的了解,今天咱們就(jiù)來進(jìn)一步看一下c#的可變參數params。首先來看一下簡單的自定義使用,随便定義一個方法

static void ParamtesDemo(string className, params string[] names)
{
    Console.WriteLine($"{className}的學(xué)生有:{string.Join(",", names)}");
}

定義可變參數類型的時(shí)候需要有幾個注意點•params修飾在參數的前面(miàn)且參數類型得是一維數組類型

params修飾的參數默認是可以不傳遞的

params參數不能(néng)用ref或out修飾且不能(néng)手動給默認值調用的時(shí)候更簡單了,如下所示

ParamtesDemo("小四班""jordan""kobe""james""curry");
// 如果不傳遞值也不會(huì)報錯
// ParamtesDemo("小四班");

由上面(miàn)的示例可知,使用可變參數最大的優勢就(jiù)是你可以傳遞一個不确定個數的集合類型并且不用聲明單獨的類型去包裝,這(zhè)種(zhǒng)場景特别适合傳遞參數不确定的場景,比如我們經(jīng)常使用到的string.Format就(jiù)是使用的可變參數類型。

探究本質

通過(guò)上面(miàn)我們了解到的params的遍曆性,當集合參數個數不确定的時(shí)候是使用可變參數的最佳場景,看著(zhe)很神奇很便捷,本質到底是什麼(me)呢?之前樓主也沒(méi)有在意這(zhè)個問題,直到前幾天懷揣著(zhe)好(hǎo)奇的心情看了一下。廢話不多說(shuō),我們直接借助ILSpy工具看一下反編譯之後(hòu)的源碼

[CompilerGenerated]
internal class Program
{
    private static void <Main>$(string[] args)
    {
        //聲明了一個數組
        ParamtesDemo("小四班"new string[4] { "jordan""kobe""james""curry" });
        Console.ReadKey();

        //已經(jīng)沒(méi)有params關鍵字了,就(jiù)是一個數組
        static void ParamtesDemo(string className, string[] names)
        {
            Console.WriteLine(className + "的學(xué)生有:" + string.Join(",", names));
        }
    }
}

通過(guò)ILSpy反編譯的源碼我們可以看到params是一個語法糖,其實就(jiù)是增加了編程效率,本質在編譯的時(shí)候會(huì)被具體的聲明的數組類型替代,不參與到運行時(shí)。這(zhè)個時(shí)候如果你懷疑反編譯的代碼有問題,可以直接通過(guò)ILSpy看生成(chéng)的IL代碼,由于IL代碼比較長(cháng),首先看一下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
        //通過(guò)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
    // 這(zhè)個地方調用了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>$' 

通過(guò)上面(miàn)的IL代碼可以看到确實是一個語法糖,編譯完之後(hòu)一切塵歸塵土歸土還(hái)是一個數組類型,類型是和params修飾的那個數組類型是一緻的。接下來我們再來看一下ParamtesDemo這(zhè)個方法的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 + "的學(xué)生有:" + string.Join(",", names));
    IL_0001: ldarg.0
    IL_0002: ldstr "的學(xué)生有:"
    IL_0007: ldstr ","
    IL_000c: ldarg.1
    IL_000d: call string [System.Runtime]System.String::Join(stringstring[])
    IL_0012: call string [System.Runtime]System.String::Concat(stringstringstring)
    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'

一切了然,本質就(jiù)是那個數組。我們上面(miàn)還(hái)提到了params修飾的參數默認不傳遞的話也不會(huì)報錯,這(zhè)究竟是爲什麼(me)呢,我們就(jiù)用IL代碼來看一下究竟進(jìn)行了何等操作吧

// 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 "小四班"
        // 本質是編譯的時(shí)候幫我們聲明了一個空數組Array::Empty<string>
    IL_0005: call !!0[] [System.Runtime]System.Array::Empty<string>()
    IL_000a: call void Program::'<<Main>$>g__ParamtesDemo|0_0'(stringstring[])
    // 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>$'

原來這(zhè)得感謝編譯器,如果默認不傳遞params修飾的參數的話,默認它會(huì)幫我們生成(chéng)一個這(zhè)個類型的空數組,這(zhè)裡(lǐ)需要注意的不是null,所以代碼不會(huì)報錯,隻是沒(méi)有數據。

擴展知識

我們上面(miàn)提到了string.Format也是基于params實現的,畢竟Format具體的參數依賴于前面(miàn)聲明的字符串的占位符個數。在翻看相關代碼的時(shí)候還(hái)發(fā)現了一個ParamsArray這(zhè)個類,用來包裝params可變參數,簡單的來說(shuō)就(jiù)是便于快速操作params,這(zhè)個我是在Format方法中發(fā)現的,源代碼如下

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值,默認不會(huì)報錯,但是需要進(jìn)行判斷,否則程序處理null可能(néng)會(huì)報錯。在這(zhè)裡(lǐ)我們可以看到把params參數傳遞給ParamsArray進(jìn)行包裝,我們可以看一下ParamsArray類本身的定義,這(zhè)個類是一個struct類型的

internal readonly struct ParamsArray
{
    //定義是三個數組分别去承載當傳遞進(jìn)來的params不同個數時(shí)的數據
    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個的時(shí)候
    public ParamsArray(object? arg0)
    {
        _arg0 = arg0;
        _arg1 = null;
        _arg2 = null;

        _args = s_oneArgArray;
    }

    //params值爲2個的時(shí)候
    public ParamsArray(object? arg0, object? arg1)
    {
        _arg0 = arg0;
        _arg1 = arg1;
        _arg2 = null;

        _args = s_twoArgArray;
    }

    //params值爲3個的時(shí)候
    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 objectthis[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是一個值類型,目的就(jiù)是爲了把params參數的值給包裝起(qǐ)來提供讀相關的操作。根據二八法則來看,params大部分場景的參數個數或者高頻訪問可能(néng)是存在于數組的前幾位元素上,所以使用ParamsArray針對(duì)熱點元素提供了快速訪問的方式,略微有一點像Java中的IntegerCache的設計。這(zhè)個結構體是internal類型的,默認程序集之外是沒(méi)辦法訪問的,我當時(shí)看到的時(shí)候比較好(hǎo)奇,就(jiù)多看了一眼,感覺設計思路還(hái)是考慮的比較周到的。

總結

本文主要簡單的聊一下c#可變參數params的本質,了解到了其實就(jiù)是一個語法糖,編譯完成(chéng)之後(hòu)本質還(hái)是一個數組。它的好(hǎo)處就(jiù)是當我們不确定集合個數的時(shí)候,可以靈活的使用params進(jìn)行參數傳遞,不用自行定義一個集合類型。然後(hòu)微軟針對(duì)params在内部實現了一個ParamsArray結構體進(jìn)行對(duì)params包裝,提升params類型的訪問。

新年伊始,聊一點個人針對(duì)學(xué)習的看法。學(xué)習最理想的結果就(jiù)是把接觸到的知識進(jìn)行一定的抽象,轉換爲概念或者一種(zhǒng)思維方式,然後(hòu)細化這(zhè)種(zhǒng)思維,讓它成(chéng)爲細顆粒度的知識點,然後(hòu)我們通過(guò)不斷的接觸不斷的積累,後(hòu)者不同領域的接觸等,不斷吸收壯大這(zhè)個思維庫。然後(hòu)當看到一個新的問題的時(shí)候,或者需要思考的時(shí)候,能(néng)達到快速的多角度的整合這(zhè)些思維碎片,得到一個更好(hǎo)的思路或解決問題的辦法,這(zhè)也許是一種(zhǒng)更行之有效的狀态。類比到我們架構設計上來說(shuō),以前的思維方式是一種(zhǒng)類似單體應用的方式,靈活性差擴展性更差,後(hòu)來微服務概念大行其道(dào),更多獨立的服務相互協調工作,形成(chéng)一種(zhǒng)更強大的聚合力。


來源:DotNet