C#小心使用 params

小心使用 params object[]

Intro

最近发现可能会不小心遇到的 params 的一个 bug,记录一下,避免以后踩坑

Sample

示例代码如下:.

private static void StringFormatTest()
{
    InvokeHelper.OnInvokeException = ex =>
    {
        ConsoleHelper.InvokeWithConsoleColor(() => Console.WriteLine(ex), ConsoleColor.Red);
    };

    var format = "{0} 123 {1}";
    InvokeHelper.TryInvoke(() =>
    {
        var str = string.Format(format, 1, 2);
        Console.WriteLine(str);
    });

    InvokeHelper.TryInvoke(() =>
    {
        var str = string.Format(format, new[] { "1", "2" });
        Console.WriteLine(str);
    });

    InvokeHelper.TryInvoke(() =>
    {
        var str = string.Format(format, new[] { 1, 2 });
        Console.WriteLine(str);
    });

    // InvokeHelper.TryInvoke(() =>
    // {
    //     var str = string.Format(format, new[] { 1, 2 }.Cast<object>().ToArray());
    //     Console.WriteLine(str);
    // });

    InvokeHelper.TryInvoke(() =>
    {
        var str = string.Format(format, new object[] { 1, 2 });
        Console.WriteLine(str);
    });
}

这几种写法分别能否正常输出,输出的结果是什么呢?

InvokeHelper.OnInvokeException = ex =>
{
    ConsoleHelper.InvokeWithConsoleColor(() => Console.WriteLine(ex), ConsoleColor.Red);
};

这段指定了发生异常时,以红色字体输出异常信息

输出结果如下:

C#小心使用 params

第三种方式报了异常,我们可以在 IDE 里看到方法对应的参数信息,如下:

C#小心使用 params

param info

可以看到实际调用的方法是一个参数的方法,我们看生成的对应的低版本的代码会是下面这样

Console.WriteLine(string.Format(this.format, new int[2]
        {
          1,
          2
        }.Cast<object>().ToArray<object>()));

这个两个参数的 int[2] 会被当成是一个 object,也就是最终会是一个参数,而我们的 format 里需要两个参数,从而导致了报错

那为什么第二种方式不会报错呢,我们可以看下第二种方式对应的参数信息和低版本的代码会是什么样子:

C#小心使用 params

string array
Console.WriteLine(string.Format(this.format, (object[]) new string[2]
        {
          "1",
          "2"
        }));

这里的 string[] 直接转成了 object[] 这就是主要的差异,string[] 发生了数组协变,变成了 object[]

int 是值类型,不能发生协变为 object[],被隐式转换为了 object

第一种方式则是,每个 int 都换成为了 object,变成了 string.Format(string format, object args0, object args1)

Console.WriteLine(string.Format(this.format, (object) 1, (object) 2));

Params object[]

有的小伙伴可能会认为是一个 string.Format 有单个或者多个的 object 参数 override 方法,我们也可以自己写一个只有 params 的方法来进行测试,示例如下:

private static void ParamsTest()
{
    ParamsMethod(1, 2, 3);
    ParamsMethod(new[] { 1, 2, 3 });
    ParamsMethod(new[] { "1", "2", "3" });
    ParamsMethod(new object[] { 1, 2, 3 });
}

private static void ParamsMethod(params object[] args)
{
    Console.WriteLine(args.Length);
}

我们这个示例直接输出参数的数量,输出结果如下

C#小心使用 params

params test

可以看到和我们前面 string.Format 的结果是类似的,我们看下生成的低版本的 C# 代码是什么样的

ParamsSample.ParamsMethod(new object[3]
{
  (object) 1,
  (object) 2,
  (object) 3
});
ParamsSample.ParamsMethod(new object[1]
{
  (object) new int[3]{ 1, 2, 3 }
});
ParamsSample.ParamsMethod((object[]) new string[3]
{
  "1",
  "2",
  "3"
});
ParamsSample.ParamsMethod(new object[3]
{
  (object) 1,
  (object) 2,
  (object) 3
});

More

发生协变有的时候会很方便,但是有时候可能会导致一些问题,比如说下面这个示例:

object[] array = new [] { "Hello" , "World" };
array[1] = 123;

array 虽然声明的是 object[] ,但是实际上是一个 string[],在给它赋值一个 int 的成员时就会有类似下面的报错:

C#小心使用 params

总而言之,在使用 params object[] 参数的时候需要小心一下自己传递的参数,避免发生一些运行时的错误。