为什么我不能传递对函数的各种引用?

时间:2011-12-07 14:16:07

标签: c# .net params ref

我想做这样的事情:

double a, b, c, d, e;
ParseAndWrite("{1, 2, 3}", ref a, ref b, ref c);
ParseAndWrite("{4, 5}", ref d, ref e);

-> a = 1, b = 2, c = 3, d = 4, e = 5

但是,我不能写这样的函数:

private void ParseAndWrite(string leInput, params ref double[] targets)
{
   (...)
}

这不起作用,由于某种原因,不能同时使用ref和params。为什么这样?

编辑好的,这里有一些关于我为什么需要这个的更多信息:通过一个接口,我得到了很多包含值的字符串,语法如下:

inputConfig : " step, stepHeight, rStep, rStepHeight, (nIterations, split, smooth) "
outputConfig : " dataSelection, (corrected, units, outlierCount, restoreOriginalRange) "

(括号中的名称是可选的)。需要解析这些值并将其存储在所有特定变量中。那就是 - 它们根本就不是阵列。它们更像是命令行参数,但大约有20个。我当然可以按顺序执行所有操作,但这会生成数百行包含冗余模式且无法维护的代码。

4 个答案:

答案 0 :(得分:16)

  

由于某种原因,不能同时使用ref和params。为什么这样?

考虑相机的这三个理想特征:

  1. 轻质

  2. 高品质镜头

  3. 便宜的

  4. 你可以在任何相机中获得最多两个。你永远不会得到这三个。你可以买到便宜的重型大镜头相机,或者昂贵的轻型大镜头相机,或者价格便宜的轻便快照相机,但是没有廉价,轻便的相机和大镜头。

    现在回到你的问题。考虑运行时的这三个理想特征:

    1. 引用数组的ref-to-variable类型是合法的

    2. 类型系统是安全的

    3. 已经快速分配和解除分配的局部变量

    4. 你有两个,但你不能拥有这三个。你想要哪两个?

      你可以使用ref param数组和一个安全类型系统,代价是在垃圾收集堆上分配ref的局部变量。据我所知,没有人这样做,但肯定是可能的。

      您可以拥有ref param数组,在临时池上分配的所有局部变量,以及在使用错误时崩溃的类型系统。这是“C / C ++”方法;可以获取引用并将其存储在生命周期比被引用事物的生命周期更长的位置。如果你这样做,你可能会让你的C ++程序崩溃并以最可怕的方式死掉,因为它容易出现难以察觉的错误。欢迎来到绝望之坑。

      或者我们可以使ref param数组非法,在临时池上分配所有局部变量,并且具有可验证内存安全的类型系统。这是我们在构建C#和CLR时所做的选择;欢迎来到质量的坑。


      现在,我上面所说的实际上是一个很大的谎言。这是谎言,因为运行时和C#语言实际上支持类似于(但不完全相同)refs参数数组的功能。这是一个愉快的谎言,相信我,你想生活在你相信谎言的世界,所以我建议你服用蓝色药丸并继续这样做。

      如果你想拿红色药丸并找出兔子洞有多深,你可以使用C#的未记录的__arglist功能来构建 C风格的可变方法,然后创建TypedReference个引用任意字段或结构类型本地的引用的对象,然后将其中任意多个传递给可变方法。使用TypedReference的工厂方法或C#的未记录的__makeref功能。

      我们在过去十年中保留这些功能未被记录的原因是因为它们在设计上仅用于极少数情况下,您必须编写C#代码,这些代码与使用其他语言编写的可变方法完全互操作。

      正确使用TypedReference对象不适合胆小的人,我强烈建议不要这样做。在编写C#多年的时间里,我从未在生产代码中这样做过。如果要传递对任意多个变量的引用,传递一个数组。根据定义,数组是变量的集合。这比传递代表实例或局部变量的托管地址的一堆对象要安全得多。

答案 1 :(得分:3)

有很多“为什么不用语言X做Y”类型的问题可以问。大多数情况下,答案是因为可能不太需要该功能。

有一些关于可变参数集的东西感觉非常奇怪,我无法想象除了在一些有限的情况下它会有用,并且通常有更好的抽象来返回项目列表。

当然,您可以传入一个预先填充的double[]方法来填充,但我的偏好只是返回IEnumerable<double>

答案 2 :(得分:2)

我不知道这是否是唯一的原因,但这是一个可能的解释:

实际参数最终仍然是double[],所以为了做你想做的事,编译器实际需要把它写成:

{
    double[] arr = new double[] {a, b, c, d}; // copy in
    YourMethod(arr);
    a = arr[0]; // copy back out
    b = arr[1];
    c = arr[2];
    d = arr[3];
}

然而!这改变了语义。想象一下调用方法实际上是:

void SomeCallingMethod(ref double a)
{
    double b = ..., c = ... d = ...;
    YourMethod(ref a, ref b, ref c, ref d);
    /*  which, say, is translated to
    {
        double[] arr = new double[] {a, b, c, d}; // copy in
        YourMethod(arr);
        a = arr[0]; // copy back out
        b = arr[1];
        c = arr[2];
        d = arr[3];
    }  */      
}

预期的行为是我们一直在直接更新a,但实际上我们不是。这可能会导致奇怪和混乱的错误。实际上,要正确地执行此操作,“复制退出”将需要进入finally,因为实际上ref值应该显示更改,直到异常,但仍然不会更改更新的语义方法调用后 而不是。

你也遇到了一个非常奇怪的问题:更新数组需要多长时间来更新调用者?毕竟,数组可以存储在任何地方,因为它不在堆栈中。


另一种发送方式,作为引用的数组到值,但可能真的致命:

  • 它会使代码基本上unsafe
  • 如果数组存储在任何地方(可以是哪个数组),则调用方法可以返回并解除当前正在使用的本地的范围,这意味着将存在实际指向垃圾的有效查找 - 主要损坏嗨

总而言之,做这件事并没有好事,还有很多坏事。

答案 3 :(得分:0)

原因可能是技术性的。 'params'只是数组参数的语法糖。因此ParseAndWrite(string leInput, params double[] targets)编译为ParseAndWrite(string leInput, params double[] targets)

然后,ParseAndWrite(string leInput, params ref double[] targets)将编译为ParseAndWrite(string leInput, ref double[] targets)。基本上,它将是作为引用传递的数组,而不是数组的实际内容。这显然不是开发人员所期望的行为,因此容易出错。