【问题标题】:Why can't I pass a various numbers of references to a function?为什么我不能将不同数量的引用传递给函数?
【发布时间】:2012-01-15 01:09:42
【问题描述】:

我想做这样的事情:

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 个。我当然可以按顺序执行所有这些操作,但这会产生数百行包含冗余模式且不易维护的代码。

【问题讨论】:

  • 我通过一个接口得到了很多逗号分隔的字符串。它们包含需要填充到大量目标参数中的不同数量的值。为了减少代码冗余,我正在寻找这样的函数。
  • 我可以理解这种困境,但返回值序列的标准方法是返回 IEnumerable<T>,这是大多数 BCL 所做的。
  • @Dennis Traub:谢谢。这 IS 是重复的,尽管上下文略有不同。答案又长又复杂,可以在这里找到:stackoverflow.com/questions/1776020/…
  • 我总是觉得人们会竭尽全力避免真正做好 OO,这很有趣。

标签: c# .net params ref


【解决方案1】:

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

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

  1. 轻量级

  2. 高品质镜头

  3. 便宜

在任何相机中,您最多可以拥有其中两个。你永远不会得到所有三个。你可以买到便宜的重型大镜头相机,或者昂贵的轻量级大镜头相机,或者便宜的轻量级快拍相机,但没有便宜的、轻量级的大镜头相机。

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

  1. 引用变量类型的参数数组是合法的

  2. 类型系统是安全的

  3. 具有引用的局部变量仍然可以快速分配和解除分配

你可以拥有任何两个,但你不能拥有所有三个。你喜欢哪两个?

您可以拥有 ref 参数数组和安全类型系统,但代价是在垃圾收集堆上分配 ref'd 局部变量。据我所知,没有人这样做,但肯定有可能。

您可以拥有 ref 参数数组、在临时池上分配的所有局部变量,以及当您使用错误时崩溃的类型系统。这是“C/C++”方法;可以获取引用并将其存储在生命周期比被引用事物的生命周期更长的位置。如果你这样做,你可能会因为犯难以检测的简单错误而导致你的 C++ 程序以最可怕的方式崩溃和死亡。欢迎来到绝望的深渊。

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


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

如果你想吃红色药丸,想知道兔子洞有多深,你可以使用 C# 中未记录的__arglist 特性构建一个C风格的可变参数方法,然后制作引用任意字段或结构类型局部变量的TypedReference 对象,然后将其中任意多个传递给可变参数方法。使用 TypedReference 的工厂方法或 C# 中未记录的 __makeref 功能。

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

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

【讨论】:

  • 难道你不能像使用 lambdas 那样使用类似于闭包的东西吗?即,局部变量的分配和释放速度很快,除非要使用引用变量类型的数组。我并不是说这个特性值得实现它的成本。我只是想知道我最初的想法是否可行。
  • @Brian:你说得对,因为我们可以将仅引用本地人实现为闭包的字段。但是,我们必须将 all 引用的局部变量实现为闭包的字段,而不仅仅是那些碰巧最终出现在数组中的局部变量。没有立即出现在数组中的 ref local 可能会在以后出现在数组中;它可能会被传递给一个方法,然后将它存储在一个数组中。我会澄清一下答案,谢谢。
【解决方案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
  • 如果数组存储在任何地方(可以是哪些数组),调用方法可以返回并取消范围正在使用的本地,这意味着会有一个看起来有效的引用实际上指向垃圾 - 严重损坏哎哟

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

【讨论】:

  • 为什么我们实际上不更新一个?
  • @Efrain 不在第一个示例中;在所讨论的第二种情况(仅文本)中,您,但这将是疯狂不安全
【解决方案3】:

原因可能是技术性的。 '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)。基本上,它将是作为引用传递的数组,而不是数组的实际内容。这显然不是开发人员所期望的行为,因此容易出错。

【讨论】:

    【解决方案4】:

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

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

    当然,您可以传入一个预先确定大小的double[] 来填充该方法,尽管我更喜欢只返回一个IEnumerable<double>

    【讨论】:

    • 返回一个值意味着我需要解压这些结果并分配这些值。但这正是我需要该功能的目的。我在问“为什么”,因为当我使用 param object[](不是内置类型)时,我实际上确实传递了对可以更改的对象的引用。为什么这不适用于内置类型的字段?
    • @Efrain:因为是引用类型和值类型的区别,所以当你传入可变参数列表时,参数是不可变的。您必须记住,当您传入引用(而不是值类型)时,引用仍然无法更改,只能更改它所引用的对象(不是直接传入的对象)。
    • @Efrain:这有意义吗?这就像问为什么调用 Func(myObject) 可以更改对象的内容,但 Func(myInt) 不能更改 int 的值。这只是 C# 中值和引用类型的区别。
    • 呃,是吗?但是 'ref' 关键字正是为了这个目的:改变实际的值类型变量,如下所示:Func(ref myInt)
    • 关闭,但不仅仅是值类型的变量,它是修改任何变量,包括引用类型。如果您想通过引用将引用参数传递给方法,您还可以使用ref(例如,如果您希望通过方法将引用更改为null 或新引用(例如,如果解析string 并返回参数,您需要ref)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-09-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多