C#中的ref关键字类似于C/C++中的指针还是C++中的引用?

42

我正在处理 ref 的相关工作,但并不理解其是否类似于C/C++的指针,还是更像C++中的引用?

为什么会提出这样一个问题呢?因为当我阅读C#/.NET书籍、MSDN或与C#开发人员交流时,我会因以下原因而感到困惑:

  • C#开发人员建议不要在函数的参数中使用ref,例如:...(ref Type someObject) 对他们来说不太好,而且他们建议使用...(Type someObject),但我真的不太明白这个建议。我听到的原因是:最好使用对象的副本,然后将其用作返回值,避免通过引用损坏内存等等……通常我会听到有关数据库连接对象的解释。根据我的普通C/C ++经验,我真的不明白为什么在C#中使用引用是不良做法?我控制对象的生命周期及其内存分配/重新分配等等……我只在书籍和论坛中看到建议它是不好的,因为你可能会通过引用失去它来破坏您的连接并导致内存泄漏,但实际上我可以手动地控制我想要的内容,所以为什么会是不良做法呢?
现在我阅读不同的书籍并与不同的人交谈,我不清楚在C#中ref是指针(*)还是类似于C++中的引用(&)? 我记得在C/C++中指针总是分配一个大小为void*类型的空间 - 4个字节(有效大小取决于架构),其中包含结构或变量的地址。在C++中通过传递引用&时,没有从堆栈分配新的空间,而是直接使用已经定义好的对象,并且没有像在纯C中那样对指针进行外部子分配内存。那么C#中的ref是什么? .NET VM是否像在纯C/C++中的指针一样来处理它,并且它的GC为指针分配临时空间,还是像C ++中的引用那样工作? ref只能正确地与托管类型一起使用,还是对于像bool,int这样的值类型,最好切换到unsafe代码并以非托管方式通过指针传递?

2
https://dev59.com/VHVD5IYBdhLWcg3wL4mM?rq=1 - Meds
1
请查看以下内容:https://dev59.com/-0vSa4cB1Zd3GeqPfpZF - Arpit Kulsreshtha
1
有趣的问题。我每天都在C中使用指针/引用,并且以前也在C#中使用过ref,但从未真正详细考虑过它们之间的相似之处。顺便说一句,指针大小当然是平台相关的,不总是像你所说的4个字节。 - Rev
7
如果你熟悉 C++,可以把 C# 的引用类型变量看作是 C++ 的指针,将 C# 的 ref 参数看作是指向指针的 C++ 指针,至少对于 C# 中的“引用”类型(即所有类)来说是这样。对于 C# 值类型(如结构体和整数),它们就像 C++ 的值类型,而 C# 中值类型的 ref 参数则相当于 C++ 中的指针。 - Matthew Watson
2
Jon Skeet写了一篇关于参数传递的好文章,详细描述了它们之间的区别。你可以在这里阅读它:http://www.yoda.arachsys.com/csharp/parameters.html - default
显示剩余3条评论
6个回答

42
在C#中,当你看到某个东西引用了一个引用类型(也就是使用class而不是struct声明的类型)时,你实际上总是通过指针来操作对象。在C++中,默认情况下所有东西都是值类型,而在C#中默认情况下所有东西都是引用类型。
当你在C#参数列表中说“ref”时,你实际上更像是在说“指向指针的指针”。你说,在这个方法中,你想要替换的不是对象本身的内容,而是调用该方法的代码中对象本身的引用。
除非这是你的意图,否则你应该直接传递引用类型;在C#中,传递引用类型很便宜(类似于在C++中传递引用)。
学习/理解C#中值类型和引用类型之间的区别。它们是该语言中的一个重要概念,如果你试图使用C++对象模型来思考,事情将变得非常混乱。
以下两个程序在语义上基本相当:
#include <iostream>

class AClass
{
    int anInteger;
public:
    AClass(int integer)
        : anInteger(integer)
    {  }

    int GetInteger() const
    {
        return anInteger;
    }

    void SetInteger(int toSet)
    {
        anInteger = toSet;
    }
};

struct StaticFunctions
{
    // C# doesn't have free functions, so I'll do similar in C++
    // Note that in real code you'd use a free function for this.

    static void FunctionTakingAReference(AClass *item)
    {
        item->SetInteger(4);
    }

    static void FunctionTakingAReferenceToAReference(AClass **item)
    {
        *item = new AClass(1729);
    }
};

int main()
{
    AClass* instanceOne = new AClass(6);
    StaticFunctions::FunctionTakingAReference(instanceOne);
    std::cout << instanceOne->GetInteger() << "\n";

    AClass* instanceTwo;
    StaticFunctions::FunctionTakingAReferenceToAReference(&instanceTwo);
    // Note that operator& behaves similar to the C# keyword "ref" at the call site.
    std::cout << instanceTwo->GetInteger() << "\n";

    // (Of course in real C++ you're using std::shared_ptr and std::unique_ptr instead,
    //  right? :) )
    delete instanceOne;
    delete instanceTwo;
}

对于C#:

using System;

internal class AClass
{
    public AClass(int integer)
        : Integer(integer)
    {  }

    int Integer { get; set; }
}

internal static class StaticFunctions
{
    public static void FunctionTakingAReference(AClass item)
    {
        item.Integer = 4;
    }

    public static void FunctionTakingAReferenceToAReference(ref AClass item)
    {
        item = new AClass(1729);
    }
}

public static class Program
{
    public static void main()
    {
        AClass instanceOne = new AClass(6);
        StaticFunctions.FunctionTakingAReference(instanceOne);
        Console.WriteLine(instanceOne.Integer);

        AClass instanceTwo  = new AClass(1234); // C# forces me to assign this before
                                                // it can be passed. Use "out" instead of
                                                // "ref" and that requirement goes away.
        StaticFunctions.FunctionTakingAReferenceToAReference(ref instanceTwo);
        Console.WriteLine(instanceTwo.Integer);
    }
}

4
如果 C# 中的变量实际上是值类型(也就是声明为 struct),那么使用 ref 关键字只需要进行一次间接引用。否则需要进行两次间接引用。 - Billy ONeal
2
值得注意的是,ref 在类参数中(十有八九)没有用处 - 但在使用值类型时,ref int x 确实会有所不同。 - Dan Puzey
@MatthewWalton 你误解了。整数并没有作为指针传递 - 如果可能的话,你是直接使用调用者的整数。语义就像你传递了一个引用,但实际上并不是这样。因此,即使是 ref int 也可能会有所不同 - 尽管这不是你可以依赖的一部分合同。尽管新编译器支持尾调用,但这使得它变得不那么有用。当然,值类型的大小并不是真正限制的 - 你可以传递一个100字节的 struct,或者1000字节的... 即使它被传递为指针(它不是),这也会有所不同。 - Luaan
1
我特别是在谈论 ref int。当然,效率问题会随着更大的值类型而改变。而且我不确定“直接使用调用者的整数”与“编译器传递指向调用者整数的指针”有什么区别,因为被调用方必须能够找到它。 - Matthew Walton
1
  1. “默认情况下,所有东西都是引用类型”:在C#中没有默认值,所以我不知道这句话的意思。
  2. “你真正想说的更像是“指向指针”的概念。”:这是不正确的。当你使用“ref”修饰符传递参数时,你是通过引用传递参数。这与指针不同。
  3. 在C#中,所有参数都是按值传递的。当你传递一个引用类型的实例时,你是通过值传递它的指针。当你使用“ref”修饰符传递引用类型的实例时,你是通过引用传递它的指针。
- Orestis P.
显示剩余5条评论

25

C#中的ref与C++中的引用类似:

  • 它们的意图是通过引用传递
  • 没有空引用
  • 没有未初始化的引用
  • 您不能重新绑定引用
  • 当您拼写引用时,实际上是指代被引用的变量

一些C++代码:

void foo(int& x)
{
    x = 42;
}
// ...
int answer = 0;
foo(answer);

等效的 C# 代码:

void foo(ref int x)
{
    x = 42;
}
// ...
int answer = 0;
foo(ref answer);

+1 是因为这里包含了一个 C# 中值类型的代码示例。 - Billy ONeal
这个答案不完整 - 那么对于引用类型的 ref 呢? - Maciej
9
在引用类型中,一个 ref 没有什么特别的地方。在 C++ 中相当于指向指针的引用���T*& - fredoverflow

3

C#中的每个引用都是指向堆上对象的指针,就像C++中的指针一样,而C#中的ref与C++中的&相同。

应该避免使用ref的原因是,C#基于这样一个基本原则:方法不应改变传递的对象,因为对于没有方法源代码的人来说,无法确定是否会导致数据丢失。

String a = "  A  ";
String b = a.Trim();

在这种情况下,我相信a保持不变。在数学中,改变应被视为一项任务,它可以清晰地告诉我们程序员已经同意在此更改b。
a = a.Trim();

这段代码将修改自身,编码人员已经意识到这一点。

为了保留这种通过赋值进行更改的方法,应避免使用ref,除非是特殊情况。


1
因此,为了防止数据丢失,您可以使用一些锁模型,例如信号量,或者在C++中使用类似于可变对象的东西,并使用正确的模式,如果这是唯一的担忧。 - Secret
5
在C#中,每个东西都是指向堆上对象的指针,就像在C++中一样。这是不正确的。这只适用于引用类型,而不适用于值类型。 - Matthew Watson
2
此外,我不同意永远不应该更改传递参数的状态。当然,不可变设计很好,但大多数C# API并非不可变的。例如,考虑任何接受流的API。向流中写入数据肯定需要对其进行修改。 - Billy ONeal
@BillyONeal 请再检查一下,它们是“out”而不是“ref”,这是一个特别设计的关键字,让用户知道它将要修改它。它们是不可变设计中的异常情况,适用于成功模式。 - Akash Kava
1
@Akash:就CLR而言,out和ref是相同的东西。无论如何,我上面的观点与out或ref参数无关。普通的引用类型传递并不意味着接受这些引用类型的函数不会调用任何可变方法。 - Billy ONeal
显示剩余4条评论

1
C#没有C++指针,而是使用引用。使用ref可以增加一层间接性。它可以将值类型参数变成引用,并且当与引用类型一起使用时,可以将其变成引用的引用。
简而言之,它允许在方法调用外部携带对值类型所做的任何更改。对于引用类型,它允许替换原始引用为完全不同的对象(而不仅仅是更改对象内容)。如果您想在方法内重新初始化一个对象,并且唯一的方法是重新创建它,则可以使用它。尽管我会尝试避免这种方法。
因此,回答您的问题,ref就像C++中的引用的引用。
编辑
对于安全代码,以上内容是正确的。指针在不安全的C#中确实存在,并且在某些特定情况下被使用。

http://msdn.microsoft.com/en-us/library/y31yhkeb.aspx 可能对你很有趣。 - AakashM
1
谢谢提供链接。当然你可以在不安全的代码中做几乎任何事情,但这并不是 C# 的一般用途。 - Maciej
1
指针不一定是“用于互操作性”。说“C#没有C++指针的等价物”就是错误的。 - AakashM

1
这似乎是一个处理/事件的噩梦。如果我有一个对象,它的事件已经注册并通过引用传递到一个函数中,然后重新分配该引用,那么应该调用dispose或者内存将一直分配,直到程序关闭。如果调用dispose,则所有注册到对象事件的内容都将不再注册,所有注册到它的内容也将不再注册。有什么方法可以保持这一点?我想你可以比较内存地址,如果你不疯掉的话,试着把事情恢复正常。

-1
在C#中,您可以在项目属性中检查“允许不安全代码运行”,然后您就可以运行此代码。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Exercise_01
{
    public struct Coords
    {
        public int X;
        public int Y;
        public override string ToString() => $"({X}, {Y})";
    }
    class Program
    {


        static unsafe void Main(string[] args)
        {
            int n = 0;
            SumCallByRefPointer(1, 2, &n);
            Console.Clear();
            Console.WriteLine("call by refrence {0}",n);

            n = 0;
            SumCallByValue(3, 4, n);
            Console.WriteLine("call by Value {0}", n);

            n = 0;
            SumCallByRef(5, 6, ref n);
            Console.WriteLine("call by refrence {0}", n);

            Pointer();
            Console.ReadLine();
        } 


        private static unsafe void SumCallByRefPointer(int a, int b, int* c)
        {
            *c = a + b;
        }

        private static unsafe void SumCallByValue(int a, int b, int c)
        {
            c = a + b;
        } 

        private static unsafe void SumCallByRef(int a, int b, ref int c)
        {
            c = a + b;
        }


        public static void Pointer()
        {
            unsafe
            {
                Coords coords;
                Coords* p = &coords;
                p->X = 3;
                p->Y = 4;
                Console.WriteLine(p->ToString());  // output: (3, 4)
            }
        }
    }
}

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