使用C#中的ref与类

21

我想把一个链表传递给我正在创建的一个类。 我希望该类能够写入该链表(例如通过.addLast())。

我应该使用ref关键字吗?

我有些困惑在C#中何时使用refout关键字,因为所有都是在堆上动态分配的,并且我们实际上对大多数操作使用指针。
当然,对于基元和结构体,outref关键字是有意义的。

另外,如果我不直接发送列表,而是发送包含列表的类? (它是internal并且是必需的),我仍然需要使用ref吗?或者如果我在函数之间传递它,例如:

void A(ref LinkedList<int> list){
    B(list);
}

void B(ref LinkedList<int> list){
    _myList = list;
}
7个回答

33

这是使用C#中ref关键字的一个常见误解。它的作用是通过引用传递值类型或引用类型,并且你只需要在特定情况下使用它,那些情况需要直接引用实际参数,而不是参数的副本(无论它是值类型还是引用类型)。在任何情况下都不要混淆引用类型按引用传递

Jon Skeet写了一篇优秀文章,讲述了C#中的参数传递,其中比较和对比了值类型、引用类型、按值传递、按引用传递(ref)和输出参数(out)。我建议您花些时间全文阅读一遍,您的理解应该会更加清晰。

引用该页面的最重要部分如下:

值参数:

默认情况下,参数是值参数。这意味着在函数成员声明中为变量创建一个新的存储位置,并且它从函数成员调用中指定的值开始。如果您更改该值,则不会更改涉及调用的任何变量。

引用参数:

引用参数不传递函数成员调用中使用的变量的值-它们使用变量本身。与在函数成员声明中为变量创建新的存储位置不同,使用相同的存储位置,因此函数成员的变量值和引用参数的值始终相同。引用参数需要ref修饰符作为声明和调用的一部分-这意味着当您按引用传递时,始终清楚地知道正在传递什么。让我们看看先前的示例,只是将参数更改为引用参数:

总之,经过阅读我的回复和Jon Skeet的文章,我希望你能明白,在你的问题上根本没有必要使用ref关键字。


答案很好,但是它给了我相反的印象。我现在正在阅读Jon Skeet的文章,谢谢。 - Nefzen
给你相反的印象...你是什么意思,怎么会这样? - Noldorin
我曾经认为按值传递类会调用复制构造函数,因为它是“按值”复制的。但这甚至没有必要使用ref,所以最终我得到了正确的答案。 - Nefzen
6
按值传递确实会复制该值。关键在于,这个“值”实际上是一个指向对象的引用——因此你实际上只是创建了一个指向堆上相同对象(引用类型)的新引用。 - Noldorin
我觉得将class-type变量Foo想象成持有第9,421个被创建的对象的引用,并称其为“Object #9421”会很有帮助。因此,语句Foo.Bar()的意思是“在对象#9421上执行Bar”。语句Boz.Bar(Foo)告诉Bar它感兴趣的对象是#9421,但不允许Bar更改Foo - 它仍将保持“Object #9421”。相比之下,Boz.Bar(ref Foo)则允许Bar更改Foo以持有“Object #24601”,如果它选择这样做的话。 - supercat
显示剩余2条评论

23

对于你所做的事情,你不需要使用ref。如果你使用ref传递列表,你将允许调用者更改你引用的列表,而不仅仅是更改列表的内容。


哦,所以复制的值是引用而不是类似于.Clone()的东西。这很有道理。 - Nefzen

17

仅当您要在函数内创建一个新对象时,才需要使用带有引用类型的 ref 关键字。

示例 #1:不需要使用 ref 关键字。

// ...
   List myList = new List();
   PopulateList(myList);
// ...
void PopulateList(List AList)
{
   AList.Add("Hello");
   AList.Add("World");
}

示例 #2:需要使用ref关键字。

// ...
   List myList;
   PopulateList(ref myList);
// ...
void PopulateList(ref List AList)
{
   AList = new List();
   AList.Add("Hello");
   AList.Add("World");
}

示例2,第2行:PopulateList(ref myList) - Jason
1
@SpencerRuport 你的解释让我彻底明白了,谢谢。 - MaYaN

3

我知道这是一个老问题,但在我看来,没有任何答案给出了一个好的直接原因。

在这种情况下,您不需要使用 ref ,以下是原因。考虑这个函数:

void Foo(MyClass a1, ref MyClass a2, out MyClass b1, int c1, MyStruct d1, ref MyStruct d2)
{
}

现在将此函数称为:
MyClass  a = new MyClass();
MyClass  b = null
int      c = 3;
MyStruct d = new MyStruct();

Foo(a, ref a, b, c, d, ref d);

以下是该函数内部的内容:

void Foo(MyClass a1, ref MyClass a2, 
         out MyClass b1, 
         int c1, 
         MyStruct d1, ref MyStruct d2)
{
   a1 is a copy in memory of the pointer to the instantiated class a;
   a2 is the pointer to the instantiated class a;

   b1 is the pointer to b, but has the additional check of having to be set within this function - and cannot be used before being set;

   c1 is a copy in memory of the variable c;

   d1 is a copy in memory of the struct d;
   d2 is the struct d;
}

重要的事情需要注意:
  1. a1 设置为 null 不会将 a 设置为 null
  2. a2 设置为 null 会将 a 设置为 null
  3. 必须设置 b1
  4. c1 设置为 null 不会改变 c
  5. d1 设置为 null 不会改变 d
  6. d2 设置为其他值会改变 d
这样可以实现一些奇怪的操作,例如:
void Foo(MyClass x, ref MyClass y)
{
    x = null;
    y.Bar("hi");
}

被称为:

MyClass a = new MyClass();
Foo(a, ref a);

您正在使用一个类,因此您的情况更像是函数调用中的变量a1。这意味着ref并不是严格必需的。
Jon Skeet的文章对您帮助不大,因为他的IntHolder示例是一个struct而不是classStruct是值类型,就像int一样,必须以相同的方式处理。

2

在您发布的两个代码片段中,没有必要通过引用传递列表。引用类型的对象是按值传递的,引用本身也是一个对象。这意味着,当方法将或可能更改对象引用并且您希望此新引用返回到调用方法时,您需要使用引用类型。例如:

void methodA(string test)
{
    test = "Hello World";
}

void methodB(ref string test)
{
    test = "Hello World";
}

void Runner()
{
    string first= "string";
    methodA(first);
    string second= "string";
    methodB(ref second);
    Console.WriteLine((first == second).ToString()); //this would print false
}

2

我为像我一样习惯于C++的程序员添加这个答案。

类、接口、委托和数组是引用类型,意味着它们有一个底层指针。普通函数调用通过值复制此指针(引用),而通过引用发送会发送对该引用的引用:

//C# code:
void Foo(ClassA     input)
void Bar(ClassA ref input)

//equivalent C++ code:
void Foo(ClassA*  input)
void Bar(ClassA*& input)

像int、double等基本类型,以及结构体和字符串(字符串是一个例外,但工作方式类似),都在堆上分配,因此情况有些不同:

//C# code:
void Foo(StructA     input)
void Bar(StructA ref input)

//equivalent C++ code:
void Foo(StructA  input)
void Bar(StructA& input)

ref 关键字需要在方法声明和调用时都使用,以便清楚表明它是被引用的:

//C# code:
void Foobar(ClassB ref input)
...
ClassB instance = new ClassB();
Foobar(ref instance);

//equivalent C++ code:
void Foobar(ClassB*& input)
...
ClassB instance* = new ClassB();
Foobar(instance);

如前所述,请阅读此文以获取详细解释。其中还有关于字符串的说明。



值得注意的是,按引用调用与底层指针一起使用,因此我们可以得到以下代码:

//C# code:
void Foo(ClassA input){
    input = input + 3;
}
void Bar(ClassA ref input){
    input = input + 3;
}
//equivalent C++ code:
void Foo(ClassA&  input){
    input = input + 3;
}
void Bar(ClassA*&  input){
    *input = *input + 3;
}
//equivalent pure C code:
void Fun(ClassA* input){
    *input = *input + 3;
}
void Fun(ClassA** input){
    *(*input) = *(*input) + 3;
}

这个翻译并不精确,但基本上是正确的。


1

不,您不需要使用ref。

LinkedList是一个对象,因此它已经是引用类型。参数list是对LinkedList对象的引用。

请参阅此MSDN文章以了解值类型的描述。值类型通常是您将使用refout关键字的参数。

您还可以通过ref传递引用类型。这将允许您将引用指向另一个对象。

任何时候您传递一个object o,您实际上是传递对该对象的引用。当您传递一个`ref object o'时,您传递对引用的引用。这使您可以修改引用。

传递引用类型参数也可能有助于您理解。


5
这可能有些误导,因为它暗示了 ref 只能用于非引用类型(即值类型)。事实上,它同样适用于引用类型,并且经常被使用! - Noldorin
3
这实际上是有些误导性的。 "ref"关键字与其是否为引用类型无关。它用于能够更改传递的引用。 - Hans Van Slooten
谢谢提供笔记,我会在文本中尽量澄清的。 - dss539
@HVS:嗯,这取决于你如何解释这个陈述。它非常模糊,因此有很大的空间。我可能在“稍微”方面比较友善。实际上,使用ref和引用类型是完全独立的,正如我在我的答案中所解释的那样。 - Noldorin
确实,这是非常误导人的。"不,你不需要使用ref。它已经是一个引用类型了。"这意味着在使用引用类型时,ref关键字没有任何作用,而且永远不应该使用。在我看来,这是一个糟糕的答案。 - configurator
@configurator,那不是预期的意思,但我可以理解你可能会这样解释它。 - dss539

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接