65.9K
CodeProject 正在变化。 阅读更多。
Home

Ref 和 Out(内部故事)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (56投票s)

2013年2月21日

CPOL

5分钟阅读

viewsIcon

156665

downloadIcon

1057

本文将力求简化对 ref 和 out 的理解,并侧重于它们内部的逻辑。

引言

在方法调用中,我们可以采用多种方式来返回或传递参数。大多数时候,开发人员会尽量避免使用 C# 的按引用传递参数的特性。如果他们了解 ref 和 out 的强大之处,一定会充分利用这种参数传递的特性。本文将力求简化对 ref 和 out 的理解,并侧重于它们内部的逻辑。

值类型与引用类型(快速概览)

我们知道 C# 中有两种“类型”:引用类型和值类型。由于它们的行为方式各不相同,因此必须根据实际需要使用,而不是强行使用。

引用类型变量的值是对相应数据的引用,而不是数据本身。这相当于 VB6 中的 ByRef,C++ 中的 &。

值类型直接包含数据,而不是引用。在赋值时,会将该数据的副本复制到新变量中。具体来说,在函数成员声明中会为变量创建一个新的存储空间,并从成员方法调用中指定的初始值开始。如果我们更改该值,它不会影响调用中的任何变量。

为什么要按引用传递?

在编写代码时,我们经常会遇到需要从单个函数/方法返回多个值的情况。但一个方法只能返回一个值。问题是如何克服这种情况?答案很简单,使用引用类型,但如何使用呢?

让我们深入探讨何时使用这种方法。默认情况下,当您将变量传递给方法时,该变量的值会被复制到方法中。对于值类型,这意味着对象本身会被复制;对于引用类型,这意味着只有指向对象的那个引用会被复制。

这是一种节省性能的方式,否则引用类型越大,性能损耗越大。因此,我们也可以称之为“共享调用”。在共享调用情况下,如果在方法中更改了引用类型变量,调用方的变量也会受到影响。如果我们更改了传递给方法的某个值类型的变量,它将永远不会影响调用方的变量。

我们的目标(Ref 和 Out)

默认情况下,参数总是按值传递给方法的。如果我们要按引用传递它们,可以使用 out 或 ref 关键字。

Reference 参数基本上不传递方法调用中使用的变量的值,而是直接使用该变量本身。在方法声明中,不创建新的存储空间,而是使用同一个存储空间,因此在成员方法中的变量值与引用参数的值将始终相同。引用参数要求在声明和调用时都使用 ref 修饰符。

Output 参数与引用参数非常相似。调用时指定的变量在传递给被调用方法之前不需要已赋值。当方法完全调用完毕后,我们可以读取该变量,因为它此时已经被赋值。

与引用参数一样,输出参数不会创建新的存储位置,而是使用调用时指定的变量的存储位置。输出参数要求在声明和调用时都使用 out 修饰符——这意味着在将某个内容作为输出参数传递时,总是很清楚的。

参考: http://www.yoda.arachsys.com/csharp/parameters.html

考虑以下场景

我开发了一个简单的控制台应用程序来阐明逻辑,在 Program.cs 类中有以下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ref_and_out
{
    class Program
    {
        static void Main(string[] args)
        {
            string name1 = "Akhil";
            string name2="Akhil";
            Program program=new Program();
            Console.WriteLine("Name Before Calling RefMethod : "+ name1);
            program.RefMethod(ref name1);
            Console.WriteLine("Name After Calling RefMethod : " + name1);
            Console.WriteLine("Name Before Calling OutMethod : " + name2);
            program.OutMethod(out name2);
            Console.WriteLine("Name After Calling OutMethod : " + name2);
            Console.ReadLine();
        }
        private void RefMethod(ref string nameRef)
        {
            nameRef = "Akhil Mittal";
        }
        private void OutMethod(out string nameOut)
        {
            Console.WriteLine(nameOut);
        }
    }
}

从上面易于理解的代码可以看出,我创建了两个方法 RefMethodOutMethod 来处理传递给它们的参数,并在赋值前后检查变量的值。当我编译代码时,遇到了以下编译时错误:

当然,错误帮助我发现了一些关于 out 和 ref 的新事实。

  • out 的情况下,参数最初被认为是未赋值的。
  • 调用时指定的变量在传递给函数成员之前不需要已赋值。由被调用方法负责在执行完成前为其赋值,以便我们可以读取它。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ref_and_out
{
    class Program
    {
        static void Main(string[] args)
        {
            string name1 = "Akhil";
            string name2;
            Program program=new Program();
            Console.WriteLine("Name Before Calling RefMethod : "+ name1);
            program.RefMethod(ref name1);
            Console.WriteLine("Name After Calling RefMethod : " + name1);
            program.OutMethod(out name2);
            Console.WriteLine("Name After Calling OutMethod : " + name2);
            Console.ReadLine();
        }
 
        private void RefMethod(ref string nameRef)
        {
            nameRef = "Akhil Mittal";
        }
 
        private void OutMethod(out string nameOut)
        {
            nameOut = "Akhil Mittal in out method";
        }
    }
}

正如预期的那样,这奏效了。

然后我检查 ref 是否也一样。再次进行一些修改:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ref_and_out
{
    class Program
    {
        static void Main(string[] args)
        {
            string name1;
            string name2;
            Program program=new Program();
            Console.WriteLine("Name Before Calling RefMethod : "+ name1);
            program.RefMethod(ref name1);
            Console.WriteLine("Name After Calling RefMethod : " + name1);
            program.OutMethod(out name2);
            Console.WriteLine("Name After Calling OutMethod : " + name2);
            Console.ReadLine();
        }
        private void RefMethod(ref string nameRef)
        {
            nameRef = "Akhil Mittal";
        }
        private void OutMethod(out string nameOut)
        {
            nameOut = "Akhil Mittal in out method";
        }
    }
}

是的,我遇到了编译时错误。

这意味着与 out 类型不同,在 ref 中:

  • 参数必须在调用函数之前初始化。

因此,Out 和 ref 通过指定变量必须被初始化并将被修改(ref),或者将在函数内部被初始化(out),为“按引用传递”的含义增添了新的内容。

内部揭秘(需要记住的一些点)

  • 许多内置方法,如“TryParse”(我最喜欢的方法之一),使用 out 而不是 ref。因为在内部实现中,库主要使用 ref。因此,out 是 ref 的一种特殊形式,其中引用的内存不应在调用前初始化。
  • 方法定义和调用方法都必须显式使用 ref / out 关键字。
  • 当值类型按引用传递时,不会发生“装箱”。
  • 属性不能通过 out 或 ref 传递,因为属性实际上是方法。
  • ref / out 在编译时并未被视为方法签名的一部分,因此方法无法重载,如果方法之间唯一的区别是其中一个方法接受 ref 参数而另一个接受 out 参数。

最终(可运行的)代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace ref_and_out
{
    class Program
    {
        static void Main(string[] args)
        {
            string name1="Akhil";
            string name2;
            Program program=new Program();
            Console.WriteLine("Name Before Calling RefMethod : "+ name1);
            program.RefMethod(ref name1);
            Console.WriteLine("Name After Calling RefMethod : " + name1);
            program.OutMethod(out name2);
            Console.WriteLine("Name After Calling OutMethod : " + name2);
            Console.ReadLine();
        }
        private void RefMethod(ref string nameRef)
        {
            nameRef = "Akhil Mittal";
        }
        private void OutMethod(out string nameOut)
        {
            nameOut = "Akhil Mittal";
        }
    }
}

输出:

以下是 @Shivprasad-koirala 撰写的一篇优秀文章。他以一种更简单、更简洁的方式解释了该概念:https://codeproject.org.cn/Articles/1033981/Out-and-REF-in-Csharp

© . All rights reserved.